📱
【Swift】Live Activityを理解する
はじめに
ポモドーロタイマーアプリを作っていたのですが、バックグランド状態になるとタイマーが進まず、どうしたものか...と思っていたらLive Activityというしくみがあることを知りました。
iOS16.2以降が前提となるのと、Dynamic Island[1]がないと使えない?というのがネックでしたが、25年4月に発売されるとされているiPhone SE4でも搭載されそうだし、実現しておくか、という感じでまずは勉強することにしてみました。
Live Activityとは
iOS16.1以降から使えるようになった、アプリの最新情報をロック画面や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
メソッドが見直された。
参考
-
iPhone 14 Pro以降の機種に搭載した機能で、iPhoneの上部にある黒い楕円型の領域を使って情報を表示する機能。ユーザー操作に応じてアニメーションで展開 ↩︎
Discussion