今回は、iPhone 14 Proの新エリア「Dynamic Island」にウィジェットを追加する方法についてご紹介します。また、iOS 16.1以上のデバイスで利用可能なライブ・アクティビティを追加する方法についても説明します。
利用可能な環境
iOS 16のリリースでは。ライブ・アクティビティは除外されています。
ライブアクティビティ機能は、パブリックベータテストプログラムで一般公開されているiOS 16.1でも利用可能です。
ライブアクティビティは、ロック画面に表示されます。
iPhone 14 Proでは、Dynamic Islandエリアにもライブアクティビティが表示されます。
ライブ・アクティビティはiPhoneのみです。
Dynamic Islandの形態
Dynamic Islandには、いくつかの形態があります。例えば、基本情報のみを表示するコンパクト(compact)な形態もあります。
コンパクト(compact)
コンパクトな形状では、左側(leading)と右側(trailing)に領域があります。フレーム幅の目安はCGFloatで50単位程度です。
拡大表示
Dynamic Islandを長押しすると展開される、エクスパンデッド(expanded)フォームもあります。
ミニフォーム
2つのウィジェットを同時に表示する場合、ミニ(minimal)サイズもあります(その場合、両方ともミニサイズを使用します)。
ロック画面のライブ活動
ライブアクティビティを有効にすることで、iOS 16.1以降を搭載する他のすべてのデバイスでも、ロック画面にライブアクティビティを表示できるようになります。
さっそくこの機能を実装してみましょう。
ライブアクティビティの有効化
まず、iOS アプリに widget 拡張ターゲットが作成されていることを確認します。
それから、最小deployment targetを iOS 16.1 に設定します。もし、あなたのコードを古い iOS デバイスでも動作させたい場合は、コード内で if #available(iOS 16.1, *)
ステートメントも使用することができます。
メインの iOS アプリとウィジェット拡張の両方で、Info.plist
ファイル内の NSSupportsLiveActivities
を YES
に設定し、
アプリがライブアクティビティをサポートしていることを示します。
ライブアクティビティの属性を作成する
ライブ活動を開始するには、ActivityAttributes
属性が必要です。
アクティビティ属性には2つの部分があります。
静的変数は、ライブ・アクティビティの間、同じ状態を維持するデータです。
また、コンテンツ (ContentState) も含まれており、セッション中に更新される可能性があります。
例えば、スポーツスコアを表示するウィジェットでは、チーム名や選手名は静的で、スコアは動的(状態)であるべきです。
import Foundation
import ActivityKit
struct TripAppAttributes: ActivityAttributes {
enum TripStatus: String {
case predeparture
case inflight
case landed
}
public struct ContentState: Codable, Hashable {
var tripStatus: String
var arrivalTime: Date
}
var shipNumber: String
var departureTime: Date
var userStopPlanetName: String
var userCabinNumber: String
}
ここで、上記の船舶宇宙旅行の例では、船舶番号、出発時刻、目的地、ユーザーの室番号は固定されており、変化することはない。
しかし、旅行状況や到着時刻は変化する。したがって、それらを ContentState
という別の構造体に格納する。
ライブアクティビティの設定を作成する
ウィジェットコードで、Widget
に適合する構造体で、
静的なウィジェットの設定を削除し、新しい ActivityConfiguration
を追加します。
これは、ライブアクティビティの設定を表します。
@main
struct LiveActivitiesTestWidget: Widget {
let kind: String = "LiveActivitiesTestWidget"
var body: some WidgetConfiguration {
+ ActivityConfiguration(for: TripAppAttributes.self) { context in
+ LiveActivitiesTestWidgetEntryView(attribute: context.attributes,
+ state: context.state)
+ } dynamicIsland: { context in
+ // ToDo
+ }
}
}
ActivityConfiguration
の初期化関数に、属性を格納する構造体の名前(例えば、TripAppAttributes
)を入力します。
アクティビティ設定のブロック (ActivityConfiguration(for: TripAppAttributes.self) { context in
) の中で、ライブアクティビティのビューを定義します。
これは、ライブアクティビティの実行時に、ロック画面の下に表示されるビューです。
dynamicIsland
ブロック (dynamicIsland: { context in
) 内では、次のセクションで紹介するのDynamic Islandビューを定義します。
ライブアクティビティの SwiftUI ビューを作成する
ActivityConfiguration
の最初のブロック内で、ロック画面の下に表示するビューを定義します。
struct LiveActivitiesTestWidgetEntryView : View {
@State var attribute: TripAppAttributes
@State var state: TripAppAttributes.ContentState
var body: some View {
VStack(alignment: .center) {
HStack {
Label("Ship number", systemImage: "moon.stars")
Spacer()
Text(attribute.shipNumber)
}
HStack {
Label("Your stop", systemImage: "lanyardcard")
Spacer()
Text(attribute.userStopPlanetName)
}
switch TripAppAttributes.TripStatus(rawValue: state.tripStatus) {
case .predeparture:
HStack {
Label("Your cabin", systemImage: "person.fill")
Spacer()
Text(attribute.userCabinNumber)
.font(.title3.bold())
}
case .inflight:
Label("Currently in trip", systemImage: "clock")
case .landed:
Label("Landed", systemImage: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.title3)
Text("Thanks for traveling with us!")
.font(.headline)
default:
Text("Unknown trip status")
}
}
.padding()
}
}
Dynamic Islandダイナミックアイランドのビューを作成する
次に、Dynamic Island用のビューを dynamicIsland
ブロック内に作成する必要があります。
上で紹介したの各コンポーネントに対して、すべてのビューを定義する必要があります。
まず、パラメータを追加します。
@main
struct LiveActivitiesTestWidget: Widget {
let kind: String = "LiveActivitiesTestWidget"
var body: some WidgetConfiguration {
ActivityConfiguration(for: TripAppAttributes.self) { context in
LiveActivitiesTestWidgetEntryView(attribute: context.attributes,
state: context.state)
} dynamicIsland: { context in
+ DynamicIsland {
+ DynamicIslandExpandedRegion(.leading) {
+ // TODO
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ // TODO
+ }
+ DynamicIslandExpandedRegion(.center) {
+ // TODO
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ // TODO
+ }
+ } compactLeading: {
+ // TODO
+ } compactTrailing: {
+ // TODO
+ } minimal: {
+ // TODO
+ }
}
}
}
DynamicIslandExpandedRegion(.leading)
は、Dynamic Islandが拡張されたとき(ユーザーがウィジェットを長押ししたとき)の領域を表します。
DynamicIslandExpandedRegion(.leading) {
+ Text("🚀")
}
DynamicIslandExpandedRegion(.trailing) {
+ Text(context.state.arrivalTime, style: .timer)
+ .font(.caption2)
}
DynamicIslandExpandedRegion(.center) {
+ Text("次の目的地は\(context.state.userStopPlanetName)です。")
}
DynamicIslandExpandedRegion(.bottom) {
+ Button("宇宙機アクセスバッジ") {
+ return
+ }.buttonStyle(.borderedProminent)
}
通常、左 (leading) 右 (trailing) のエリアにはごく短いテキスト、
中央のエリアには1文程度のテキスト説明(またはスコアボード)、
下部にはユーザーがクリックするためのボタンを配置することができます。
そして、コンパクトサイズに対応したビューを提供します。
compactLeading: {
+ Text("🚀 - \(context.attributes.shipNumber)")
} compactTrailing: {
+ Text(context.state.arrivalTime, style: .relative)
+ .frame(width: 50)
+ .monospacedDigit()
+ .font(.caption2)
}
通常、左側 (leading) にはグラフと非常に短いテキスト(タイマーやイベント名など)、
右側 (trailing) にはアニメーション(タイマーやシェイプのアニメーション)が含まれる
最後に、ミニフォーマットで。
minimal: {
Text("🚀")
}
このエリアは非常に小さいので、ユーザーがタップしてアプリを開いたり、長押ししてウィジェットを展開できるように、アイコン(または円形の進捗表示)を表示することができます。
なお、Dynamic Islandダイナミックアイランドに2つ以上のウィジェットが表示されている場合、すべてのウィジェットが最小サイズで表示されることに注意してください。
ライブアクティビティを開始する
メインの iOS アプリでライブアクティビティを作成することができます。
まず、属性を含むオブジェクトを初期化します。
これらは、ライブアクティビティのセッション中、変わらない値(static)です。
let attributes = TripAppAttributes(shipNumber: "火星へ", ... )
次に、アクティビティの初期状態 (state) を設定します。
状態(動的な値 dynamic)は、ライブ・アクティビティ・セッション中に変更することができます。
let contentState = TripAppAttributes.ContentState(tripStatus: TripAppAttributes.TripStatus.inflight.rawValue,
arrivalTime: Calendar.current.date(byAdding: .minute, value: 8, to: Date()) ?? Date())
ライブ活動の開始をリクエストすることができます。
do {
self.currentActivity = try Activity<TripAppAttributes>.request(
attributes: attributes,
contentState: contentState,
pushType: nil)
} catch (let error) {
print(error.localizedDescription)
}
Activity
オブジェクトの .activites
を呼び出すことで、いつでもアクティビティを取得することができます。
let activities = Activity<TripAppAttributes>.activities
ユーザーは、アプリからライブアクティビティの機能をオフにすることもできることに注意してください。
デフォルトでは、すべてのアプリはライブアクティビティを表示することができます。
権限設定を確認することができます。
let isEnabled = ActivityAuthorizationInfo().areActivitiesEnabled
時間制限
Appleのドキュメントによると
- ライブアクティビティは、アプリまたはユーザーが終了させない限り、最大8時間までアクティブにすることができます。
- この制限を過ぎると、自動的に終了します。
- 終了すると、システムは直ちにDynamic Islandからそのライブアクティビティを削除します。
- 終了後、ユーザーがライブアクティビティを削除するまで、またはシステムが削除するまでの最大4時間、ロック画面に残ります(いずれか早い方)。
- その結果、ライブアクティビティは最大12時間ロック画面に表示されます。
既存のライブ活動を更新する
ローカルで
iOSのメインアプリの中から、既存のライブアクティビティを更新することができます。
更新するには、新しいステート (state) オブジェクトを作成し、それを既存のライブアクティビティに更新します。
Button("Trip arrival time +10 minutes") {
Task {
guard let currentActivity else { return }
let updatedState = TripAppAttributes.ContentState(tripStatus: TripAppAttributes.TripStatus.inflight.rawValue,
arrivalTime: Calendar.current.date(byAdding: .minute, value: 10, to: currentActivity.contentState.arrivalTime) ?? Date())
await currentActivity.update(using: updatedState)
}
}
プッシュ通知による更新
ライブ活動では、フライト情報やスポーツ情報など、実時間のデータが表示されることがほとんどです。
この場合、サーバーからiOSデバイスにプッシュ通知することで、ライブアクティビティーを更新することができます。
まず、self.currentActivity?.pushToken
を使用してライブアクティビティを識別するために使用されるトークンを取得します。そのプッシュトークンをサーバーに送信します。
サーバーで、
まず、最新の状態を content-state
内で JSON を使ってエンコードします。
content-state
のフォーマットは Swift の構造体のフォーマットと一致しなければならないことに注意してください。
apns-push-type
は liveactivity
に設定します。
apns-topic
には、バンドル ID を指定して、 [Bundle ID].push-type.liveactivity
と設定します。
ライブアクティビティを更新したい場合は、 event
を update
に設定します。
ライブアクティビティを終了したい場合は、 event
を end
に設定します。
プッシュトークン (pushToken) が変更される可能性があることに注意してください。
プッシュトークンの変更を知るために、 self.currentActivity?.pushTokenUpdates
を使用する必要があります。トークンが変更された場合は、サーバーに通知する必要があります。
ライブ活動を終了する
ライブ・アクティビティを終了するには、終了状態を指定し、ウィジェットがいつ閉じるべきかをシステムに知らせます。
ウィジェットはすぐに閉じることができます。しかし、ほとんどの場合、ウィジェットは最適な時間または特定の時間にシステムによって閉じることができます(たとえば、ユーザーはスポーツイベントが終了した後もスコアを表示したい場合があります)。
Button("End activity") {
Task {
guard let currentActivity else { return }
let updatedState = TripAppAttributes.ContentState(tripStatus: TripAppAttributes.TripStatus.landed.rawValue,
arrivalTime: Date())
await currentActivity.end(using: updatedState, dismissalPolicy: .default)
}
}
Github repo
お読みいただきありがとうございました。
☺️ Twitter @MszPro
🐘 Mastodon @me@mszpro.com
Written by MszPro~
関連記事
・UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用(iOS 16, UIHostingConfiguration)
・iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する(iOS16.1以降)
・iOS 16:秘密値の保存、FaceID認証に基づく個人情報の表示/非表示(LARight)
・iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完
・SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)
・WWDC22、iOS16:iOSアプリに画像からテキストを選択する機能を追加(VisionKit)
・WWDC22、iOS16:数行のコードで作成できるSwiftUIの新機能(26本)
・WWDC22、iOS 16:SwiftUIでChartsフレームワークを使ってチャートを作成する