【iOS14】iOSアプリにウィジェットを導入する【WidgetKit】

##ウィジェットとは
iOS14からWidgetExtensionを利用してアプリ毎に用意されたWidgetをホーム画面のどの部分にもユーザーが自由に配置することができるようになりました。
詳しくは公式
[iPhone や iPod touch でウィジェットを使う](https://support.apple.com/ja-jp/HT207122)

##導入方法
メニュー → File → New → Target
![スクリーンショット 2020-10-01 18.30.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/7638fc5c-4077-c202-9a9f-f7ee08092995.png)

Widget Extensionを選択 ターゲット名を入力
![スクリーンショット 2020-10-01 18.31.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/b2211c4a-52a4-d7f5-a5cf-0a7a9c210384.png)

ここではSampleWidgetという名前にしました
このようなものができます
![スクリーンショット 2020-10-01 18.38.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/fa00dc4e-8071-9fc1-ff1e-7b031da26092.png)
以下のファイルが自動生成されます
![スクリーンショット 2020-10-01 18.38.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/b3297bed-5820-6663-5fe1-cd5ff5ffe5a3.png)

##実行してみる
何も変更を加えずシミュレータで実行すると以下のようにウィジェットが出現します。
![スクリーンショット 2020-10-01 18.48.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/73358d69-5e59-d6d8-68c8-72bf4f842b17.png)

SampleWidget.swiftファイル内で定義されている
SampleWidgetEntryViewが表示されています。

```SampleWidget.swift
struct SampleWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}
```
Provider.Entryという構造体に定義されたdateという日時を入れた変数を表示するText型のViewです。

```SampleWidget.swift
@main
struct SampleWidget: Widget {
    let kind: String = "SampleWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            SampleWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
```
supportedFamiliesを追加してみました。
`.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])`
すると

デフォルトのSmallに加えてMidium, Largeの3種類のサイズを選択できるようになります。
![スクリーンショット 2020-10-02 12.02.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/028a7ecd-583a-8ee8-0bf6-89aa0cfa885a.png)![スクリーンショット 2020-10-02 12.02.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/0d834928-f924-1d26-b7b5-581b6ab7ea28.png)


##アプリとの連携
####ウィジェットをタップした時の処理
ウィジェットをタップするとアプリが起動します。
ViewにwidgetURLを実装することによってアプリ側でハンドリングすることができます。
`.widgetURL(URL(string: "widget-link://"))`

```SampleWidget.swift
struct SampleWidgetEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        Text(entry.date, style: .time)
            .widgetURL(URL(string: "widget-link://"))
    }
}
```

アプリ側ではSceneDelegateもしくはSwiftUIのApp内onOpenURLを実装してハンドリングします。

``` SceneDelegate.swift
   // App launched
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _: UIWindowScene = scene as? UIWindowScene else { return }
        maybeOpenedFromWidget(urlContexts: connectionOptions.urlContexts)
    }
    
    // App opened from background
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        maybeOpenedFromWidget(urlContexts: URLContexts)
    }
    
    private func maybeOpenedFromWidget(urlContexts: Set<UIOpenURLContext>) {
        if let _: UIOpenURLContext = urlContexts.first(where: { $0.url.scheme == "widget-link" })  {
            print("🚀 Launched from widget")
        }
    }
```

```SampleApp.swift
struct SampleApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { (url) in
                    print(url)
                }
        }
    }
}
```

####複数のリンク
systemSmallでは単一のURLしか利用できませんが、 systemMedium,systemLargeの場合はタップ領域によって複数のURLを定義できます。
その場合はwidgetURLを複数使うということはできないのでLinkを使います。
例として以下のようにします。

```SampleWidget.swift
struct SampleWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        HStack{
            Link("link 1", destination: URL(string: "widget-link://link1")!)
            Link("link 2", destination: URL(string: "widget-link://link2")!)
        }
    }
}
```

![スクリーンショット 2020-10-02 12.21.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/3d15a82c-7b17-ef1e-b6bd-00cf1a8e3527.png)

これで上記のようにタップ領域ごとにハンドリングすることができます。


##Widgetを更新する

Timelineという概念により実装側で更新頻度を設定する方法
アプリ側から強制的に更新をさせるという方法
があります。

しかしどちらにせよ実際の更新はOSによって制御されており、非常に短い間隔であったりリアルタイム性を持たせるであったり厳密な時間を定義した更新はできないようです。

こちらは自動生成されたデフォルトのSampleWidget.swiftのTimelineに関する実装の内容です。

```SampleWidget.swift
struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}
```

こちらの更新の要求が通ると、OS側から上記のgetTimelineが呼び出されるというイメージです。
上記の実装だと、getTimelineが呼び出された日時から1時間毎に5つのTimelineEntryを作成してTimelineとしています。
この時点でTimelineがシリアライズされてOS内に保持され、dateの日時になるとViewがレンダリングされるといったイメージになります。

`let timeline = Timeline(entries: entries, policy: .atEnd)`
このpolicyというのが肝になっています。
これはTimelineReloadPolicyとして定義されています。

```swift

public struct TimelineReloadPolicy : Equatable {

    /// A policy that specifies that WidgetKit requests a new timeline after
    /// the last date in a timeline passes.
    public static let atEnd: TimelineReloadPolicy

    /// A policy that specifies that the app prompts WidgetKit when a new
    /// timeline is available.
    public static let never: TimelineReloadPolicy

    /// A policy that specifies a future date for WidgetKit to request a new
    /// timeline.
    public static func after(_ date: Date) -> TimelineReloadPolicy
```
atEnd → Timelineを全て経過した後に更新を要求する。
never → Timelineが経過しても更新しない
after(_ date:Date) → dateの日時が来たときに更新を要求する。

####アプリ側から更新を要求する方法
 WidgetCenterというクラスからアクセスします。

```swift
import WidgetKit

//~~~~~~~~~~~~~~

        WidgetCenter.shared.reloadAllTimelines()
        or
        WidgetCenter.shared.reloadTimelines(ofKind: "SampleWidget")

//~~~~~~~~~~~~~~

```


##終わりです

本当に触りだけの簡単な内容になってしまいましたが、個人的にはSwiftUIを全然触ったことがなくTimelineという概念の理解にも時間がかかってしまい記事の作成に苦労しました。

実際の使い方に関しては、例えばライブ配信系のアプリであれば直近の配信スケジュールを表示だったり、自分のフォロワー数とランキングの遷移を表示といったものが考えられると思います。
野球速報のアプリならばその日の試合日程や途中経過が表示されていてタップすればアプリ内の詳細な試合のデータのページにそのまま遷移することができる、といった感じです。
ユーザーの知りたい情報をアプリを起動せずともホーム画面に表示しておけるというのは非常に便利な機能だと思うので、アプリ開発の幅も広がったと感じました。

ありがとうございました。