📱

【Swift】Live Activityを理解する

2024/12/28に公開

はじめに

ポモドーロタイマーアプリを作っていたのですが、バックグランド状態になるとタイマーが進まず、どうしたものか...と思っていたらLive Activityというしくみがあることを知りました。
iOS16.2以降が前提となるのと、Dynamic Island[1]がないと使えない?というのがネックでしたが、25年4月に発売されるとされているiPhone SE4でも搭載されそうだし、実現しておくか、という感じでまずは勉強することにしてみました。

Live Activityとは

iOS16.1以降から使えるようになった、アプリの最新情報をロック画面やDynamic Islandに表示して、ユーザーがリアルタイムに情報を把握できるようにするものです。

Dynamic Islandのイメージ画像
公式サイトサポートページより借用

しくみ

Live Activityは以下の4つの要素から構成されます。

1. Activity Attributes

どんなデータをロック画面やDynamic Islandに表示するかを定義する。

カウントダウンの時間、再生中の音楽、直近の案内情報

2. Activity State

1で定義したデータのリアルタイム情報を管理する。

残り時間、再生中の音楽の経過時間、現在地

3. UI

ロック画面やDynamic Islandに表示されるデザインを定義する。

4. Activity Lifecycle

Live Activityの開始、更新、終了ライフサイクルを管理する。

実装サンプル

Activity Attribute, Activity State

import ActivityKit

struct PomodoroAttributes: ActivityAttributes { // タイマーのセッションを保持
  struct ContentState: Codable, Hashable { // 動的な情報を保持
    var remainingTime: Int
    var isWorkSession: Bool
  }

  var sessionName: String
}

UI

import ActivityKit
import SwiftUI
import WidgetKit

struct PomodoroLiveActivity: Widget {
  var body: some WidgetConfiguration {
    ActivityConfiguration(for: PomodoroAttributes.self) { context in
      VStack {
        Text(context.attributes.sessionName)
          .font(.headline)
        Text("Time Remaining: \(formatTime(context.state.remainingTime))")
          .font(.title2)
          .bold()
        Text(context.state.isWorkSession ? "Work Session" : "Break Session")
          .font(.subheadline)
          .foregroundColor(.gray)
      }
      .padding()
    } dynamicIsland: { context in
      DynamicIsland {
        DynamicIslandExpandedRegion(.center) {
          Text("Time: \(formatTime(context.state.remainingTime))")
            .font(.title2)
        }
      } compactLeading: {
        Text("⌛")
      } compactTrailing: {
        Text(formatTime(context.state.remainingTime))
      } minimal: {
        Text("⌛")
      }
    }
  }

  private func formatTime(_ seconds: Int) -> String {
    let minutes = seconds / 60
    let seconds = seconds % 60
    return String(format: "%02d:%02d", minutes, seconds)
  }
}
属性 説明
WidgetConfiguration ロック画面、通知センターの内容
DynamicIsland 展開状態時
compactLeading コンパクト状態の左側
compactTrailing コンパクト状態の右側
minimal 最小化状態

Activity Lifecycle

func startLiveActivity() {
  guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }

  let attributes = PomodoroAttributes(sessionName: isWorkSession ? "Work" : "Break")
  let initialContentState = PomodoroAttributes.ContentState(
    remainingTime: timeRemaining,
    isWorkSession: isWorkSession
  )

  do {
    _ = try Activity.request(
      attributes: attributes,
      content: .init(state: initialContentState, staleDate: nil),
      pushType: nil
    )
  } catch {
    print("Failed to start live activity: \(error)")
  }
}

func updateLiveActivity() {
  guard let activity = Activity<PomodoroAttributes>.activities.first else { return }

  let updatedContentState = PomodoroAttributes.ContentState(
    remainingTime: timeRemaining,
    isWorkSession: isWorkSession
  )

  Task {
    await activity.update(.init(state: updatedContentState, staleDate: nil))
  }
}

func endLiveActivity() {
  guard let activity = Activity<PomodoroAttributes>.activities.first else { return }

  let updatedContentState = PomodoroAttributes.ContentState(
    remainingTime: 0,
    isWorkSession: isWorkSession
  )

  Task {
    await activity.end(.init(state: updatedContentState, staleDate: nil), dismissalPolicy: .immediate)
  }
}

メモ

  • iOS16.2以降では、Live Activity関連で非推奨コードへの対応としてrequestメソッドが見直された。

参考

https://developer.apple.com/jp/design/human-interface-guidelines/live-activities

https://zenn.dev/naoya_maeda/articles/d4c7e0d29afd2f

https://qiita.com/waiiioss/items/c27303879050c58abdc8

脚注
  1. iPhone 14 Pro以降の機種に搭載した機能で、iPhoneの上部にある黒い楕円型の領域を使って情報を表示する機能。ユーザー操作に応じてアニメーションで展開 ↩︎

Discussion