【SwiftUI】LiveActivityを実装する part 2(iOS16.1)

SwiftUI

LiveActivityを実装する part 2です。
part 2ではLiveActivityのWidget Extensionの部分について詳しく解説して行きます。

part 1は以下のリンクから見ることができます。

LiveActivityとWidget Extension

LiveActivityは個別でExtensionが用意されている訳ではありません。
Widget Extensionに含まれる形になっています。

WidgetとLiveActivityはまとめてここに実装する事になります。

WidgetBundleについて

WidgetBundleはLiveActivityやWidgetをまとめたものです。
LiveActivityやWidgetを複数実装する場合はここに追加して行きます。

@main
struct LiveActivityForBlogWidgetBundle: WidgetBundle {
    var body: some Widget {
        LiveActivityForBlogWidget()
        LiveActivityForBlogWidgetLiveActivity()
    }
}

なぜこの様な形となっているかと言うと、
LiveActivityやWidgetにもエントリポイントが必要だからです。

見ての通りWidgetBundleは@mainでマークされエントリポイントとなっています。
複数あるLiveActivityやWidgetを全て@mainでマークする事は出来ない為、
WidgetBundleでまとめてそこをエントリポイントとします。

その為、1つしか実装しない場合はWidgetBundleは不要です。
以下の様にLiveActivityを@mainでマークして直にエントリポイントとする事が出来ます。

@main
struct LiveActivityForBlogWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivityForBlogWidgetAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
            
        } dynamicIsland: { context in
            //略
        }
    }
}

なお、先程言った通り複数を@mainでマークすることは出来ません。
Targetにつき1つまでです。

その為、デフォルトで生成されるLiveActivityのコードの様に、
LiveActivityとActivityAttributesを同じファイルに書くのは良くありません。

import ActivityKit
import WidgetKit
import SwiftUI

//アプリ側でも使うのでLiveActivityとは別ファイルが望ましい
struct LiveActivityForBlogWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var value: Int
    }
    var name: String
}

@main //ActivityAttributesとLiveActivityを共に書くならWidgetBundleにすべき
struct LiveActivityForBlogWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        //略
    }
}

ActivityAttributesはアプリ側で使用する必要があります。
ActivityAttributesの書かれたファイルのTarget Membershipでアプリにチェックを入れたいです。

アプリ側はAppが@mainでマークされています。
その為、Widget Extension側で@mainでマークされたエントリポイントを
アプリ側で使いファイルに含ませてはいけません。

Target Membershipについて

エントリポイントに関わって先に述べてしまいましたが、
ActivityAttributesはアプリ側でも使用します。

struct LiveActivityForBlogWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var value: Int
    }
    var name: String
}

その為、Target Membershipでアプリにチェックを入れて双方で使用できる様にします。

同様にアプリとLiveActivityで共通して使用するViewやメソッドのあるファイルは、
Target Membershipにチェックを入れて双方で使用できる様にして下さい。

ActivityAttributesについて

アプリ側でも使用すると言ったActivityAttributesですが、
これはLiveActivityに表示するデータをアプリから渡す為に使います。

struct LiveActivityForBlogWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var value: Int
    }
    var name: String
}

ActivityAttributesのメンバ変数は変更しない値です。
LiveActivityを表示する毎に異なる値使う場合があるが、
1度表示したLiveActivityには最初から最後まで表示される値に使用します。

対して内側で宣言されているContentStateは変更する事ができる値です。
LiveActivityは表示した後も更新する事が可能です。
その時に変更できるのがこのContentStateの中の値です。

画像を入れる

LiveActivityの表示について

LiveActivityで表示する内容はWidgetを継承した構造体に書いて行きます。
Widgetの場合はStaticConfigurationでしたがLiveActivityではActivityConfigurationとなっています。

struct LiveActivityForBlogWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivityForBlogWidgetAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
            
        } dynamicIsland: { context in
            //略
        }
    }
}

このActivityConfigurationに書かれたViewがロックスクリーンに表示される内容になります。
ロックスクリーンに表示される内容に関しては比較的素直に表示されるので問題ないと思います。

DynamicIslandは以下の様になっています。

struct LiveActivityForBlogWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivityForBlogWidgetAttributes.self) { context in
            // 略
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T")
            } minimal: {
                Text("Min")
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

DynamicIslandは細かく区分けされていて、それぞれに表示するViewを実装していく必要があります。
それぞれの表示領域をしっかり確認して実装していく必要があります。

DynamicIslandExpandedRegionはDynamicIslandをタップして拡大された時に表示されます。

ホーム画面や他のアプリを操作している際には
compactLeadingとcompactTrailingが表示されます。

minimalはLiveActivityが複数ある場合に表示されます。
どちらのLiveActivityが先に出たかで若干見え方は異なりますが、
どちらでもminimalのみ表示となります。

LiveActivityから操作を行う

LiveActivityから操作を行うにはDeepLinkを使用する必要がありそうです。
Apple純正アプリはLiveActivityから直接操作が行える様ですが、
ドキュメントではDeepLinkを使うよう促されており、
ざっと確認しただけではサードパーティ製アプリで直接操作する方法は見つかりませんでした。

まずはURLスキームを設定しましょう。
ProjectのアプリのInfoタブの下部にURL Typesがあります。
+ボタンから追加してIdentifierとURL Schemesだけ入力すれば今回は十分です。

次はLiveActivityにDeepLinkを追加しましょう。

Linkを使ってDeepLinkを実装して行きます。
LockScreenと拡大したDynamicIslandには複数のリンクを配置できます。
DynamicIslandでリンク以外をタップした場合とcompactやminimalの場合は
widgetURL Modifierで1つだけリンクを設定する事ができます。

Buttonも一応は配置出来るのですが、
UIApplication.sharedが使えない事と、
現状はアプリが開いてしまう為Linkを使用します。

struct LiveActivityForBlogWidgetLiveActivity: Widget {
    
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivityForBlogWidgetAttributes.self) { context in
            VStack {
                Link("LockScreen", destination: URL(string: "liveActivityForBlog://deeplink?param=LockScreen")!)
                Link("LockScreen2", destination: URL(string: "liveActivityForBlog://deeplink?param=LockScreen2")!)

                //UIApplication.sharedが使えない
//                Button("LockScreen"){
//                    UIApplication.shared.open(URL(string: "liveActivityForBlog://deeplink?param=LockScreen")!)
//                }
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
            
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Link("Leading", destination: URL(string: "liveActivityForBlog://deeplink?param=Leading")!)
                    Link("Leading2", destination: URL(string: "liveActivityForBlog://deeplink?param=Leading2")!)
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Link("Trailing", destination: URL(string: "liveActivityForBlog://deeplink?param=Trailing")!)
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T")
            } minimal: {
                Text("Min")
            }
            .widgetURL(URL(string: "liveActivityForBlog://deeplink?param=DynamicIsland"))
            .keylineTint(Color.red)
        }
    }
}

最後にアプリ側にDeepLinkでアプリを開いた際の処理を入れます。

onOpenURL ModifierでDeepLinkからアプリを開いた時の処理を実装する事ができます。
今回は単純に1つ目のパラメータの値をテキストとして表示しています。

struct ContentView: View {
    
    @State var linkParam = "None"
    
    var body: some View {
        VStack {
            Text("DeepLink Param:\(linkParam)")
            
            Button("Request LiveActivity"){
                do {
                    let attributes = LiveActivityForBlogWidgetAttributes(name: "TestName")
                    let contentState = LiveActivityForBlogWidgetAttributes.ContentState(value: 1)
                    let liveActivity = try Activity.request(attributes: attributes, contentState: contentState)
                } catch (let error) {
                    print("Error requesting Live Activity \(error.localizedDescription).")
                }
            }
        }
        .padding()
        .onOpenURL(perform: { url in
            let comp = URLComponents(url: url, resolvingAgainstBaseURL: true)
            linkParam = comp?.queryItems?.first?.value ?? ""
        })
    }
}

DeepLinkを使ってアプリを開くことでLiveActivityから対応した処理をさせる事ができます。
現状ではアプリが開いてしまうのが難点ですが、複数のリンクを配置できるので便利です。

最後に

LiveActivityを実装する part 2は以上となります。

DynamicIslandが細かく分かれていてレイアウトはよく考えなければなりませんが、
複数のDeepLinkが配置出来るので便利に使えると思います。

アプリを開かないと操作出来ない問題については、
何か情報を見つけ次第記事を書こうと思っています。
もし何かあればTwitterの方に連絡を頂けると幸いです。

次回 part 3はアプリ側からのLiveActivityの更新や表示中のLiveActivityの取得などを紹介する予定です。

また、LiveActivityを追加して実行するまでを書いた「LiveActivityを実装する part 1」は以下のリンクから読むことができます。

コメント

  1. […] 【SwiftUI】LiveActivityを実装する part 2(iOS16.1)LiveActivityを実装する part 2です…未分類ActivityKitiOS16LiveActivitySwiftUIWidgetシェアする Twitter Facebook はてブ Pocket LINE コピーthwork.devをフォローする thwork.dev thwork […]

  2. […] 【SwiftUI】LiveActivityを実装する part 2(iOS16.1)LiveActivityを実装する part 2です…SwiftUIActivityKitiOS16LiveActivitySwiftUIWidgetKitシェアする Twitter Facebook はてブ Pocket […]

タイトルとURLをコピーしました