• トップ
  • ブログ一覧
  • iOS14に追加されたウィジェット実装してみた
  • iOS14に追加されたウィジェット実装してみた

    いまむー(エンジニア)いまむー(エンジニア)
    2022.07.04

    IT技術

    はじめに

    今回はウィジェットの表示、ウィジェットの編集、ディープリンクで特定の画面を開くところまでのデモアプリを作成しました。

    本記事がウィジェットを実装される方のお役に少しでも立てば幸いです。

    Widgetとは

    WidgetとはUIのことで、ホーム画面を左にスワイプすると出てくるアイコン4つ分くらいの大きさのUIをWidgetと言います。

    iOS14からWidgetがホーム画面にも追加できるようになりました。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Widgetの特徴

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Widgetの特徴には以下の3つがあります。

    1. Glanceable(ひと目で分かる)
    2. Relevant(関連性がある)
    3. Personalized(個々にカスタマイズできる)

    Glanceable(ひと目で分かる)

    まず、ひと目で分かるという特徴についてです。

    Widgetでは以下のように特定のコンテンツを表示することができ、ひと目で情報を確認することができます。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Relevant(関連性がある)

    次に関連性があるという特徴についてです。

    Widgetはユーザーが求めている情報を適切な時間で表示することができます。

    例えば、朝起きた時に天気を確認し、音楽を聞くと言った場合、Widgetは起きる時間では天気の情報を表示し、10分後に音楽アプリの情報を表示するということができます。

    また、Widgetには以下のようなスマートスタックというWidgetがあり、限られた画面スペースを最大限に活用し、システムが適切なタイミングでWidgetを回転させて表示します。(スワイプして表示を変えることもできます。)

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Personalized(個々にカスタマイズできる)

    最後に個々にカスタマイズできるという特徴についてです。

    Widgetはユーザーに合わせて表示内容やサイズをカスタマイズすることができます。

    例えば天気アプリでは、Widgetを長押ししてウィジェットの変更を選択することで、地域を選択することができます。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Widgetのサイズ

    WidgetのサイズはWidgetFamilyというenumで定義されており、systemSmall、systemMedium、systemLargeという3種類のサイズがあります。

    Widgetを導入する場合、全てのサイズに適応しなければならないわけではなく、どのWidgetサイズにアプリを適応させるかは開発者が決めることができます。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Widgetの更新

    Widgetの更新にはTimelineを使用します。

    開発者が更新したい時間にTimelineを設定することでWidgetを更新することができます。

    また、Timelineを更新するには2つの方法があり、TimelineにReloadPolicyを設定する方法とWidgetCenterのメソッドを使って更新する方法の2つがあります。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    ReloadPolicy

    Timeline作成時にReloadPolicyを設定することで、Timelineの更新タイミングを設定することができます。

    ReloadPolicyには「atEnd」「after(date: Date)」「never」の3種類があります。

    1. atEnd:設定したTimelineの最後でリロードを要求
    2. after(date: Date):特定の日付の後にリロードを要求
    3. never:Timelineをリロードしないよう指定

    ※注意点:ReloadPolicyを設定したからと言って、必ず指定した時間に更新されるわけではなく、最終的にSystem reloadsが適切なタイミングでリロードします。

    WidgetCenter

    WidgetCenterを使うことで、リロードをリクエストすることができます

    1. 特定のWidgetのリロードをリクエストする
      WidgetCenter.shared.reloadTimelines(ofKind:)
    2. 全てのWidgetのリロードをリクエストする
      WidgetCenter.shared.reloadAllTimelines()

    WidgetCenterを使用することで、アプリ起動時やプッシュ通知受信時などのイベントからリロードをリクエストすることができます。

    Widgetの編集

    Widgetの編集は、IntentConfigurationを設定することで追加することができます。

    IntentConfigurationを設定することで、Widgetに表示する内容をユーザーが選択できるようになります。

    この機能を使って、株価アプリでは以下の画像のように表示する銘柄を選択できます。

    WWDC20 - Videos - Apple Developer Meet WidgetKit から引用

    Deep linking

    Widgetにはディープリンクを追加することができ、ディープリンクを使って画面遷移した状態でアプリを表示することができます。

    Widgetに追加できるディープリンクの数はWidgetのサイズによって違いがあり、systemSmallでは1つ、systemMedium、systemLargeでは複数のディープリンクが設定できます。

    WWDC20 - Videos - Apple Developer Widgets Code-along,part 2: Alternate timelines から引用

    Widgetの実装

    今回デモアプリとして、ポケモンをテーブルに表示し、セルをタップした時にポケモンの画面に遷移するアプリを作成しました。

    Widgetの編集で好きなポケモンを選択でき、またディープリンクでWidgetがタップされた時にポケモンの画面が表示されるようにしました。

    Widgetの実装①

    まずは作成したプロジェクトにWidgetを追加します。

    メニューのFile → New → Target...からWidget Extensionを選択し、NextでWidgetExtensionを追加します。

    追加されたファイルを開くと、日付が表示される簡単なWidgetが実装してあるので、あとはコードの追加と修正をすればWidgetを作成することができます。

    まずは、追加されたファイル内のTimelineEntryを修正します。修正後のコードが以下になります。

    1struct SmallEntry: TimelineEntry {
    2    let date: Date
    3    let configuration: MyPokemonIntent
    4    let pokemon: Pokemon
    5}

    dateとconfigurationはファイルを作成した時に実装されているもので、configurationには後で作成するMyPokemonIntentを指定しています。

    また、WidgetのViewを作成する時に使用するクラスを追加します。今回はPokemonを追加しました。

    Widgetの実装②

    TimelineEntryが修正できたら、次にWidgetのViewを作成します。

    entryには先ほど定義したTimelineEntryが入ってくるので、entryを使ってViewを作成します。

    そして、一番下の「.widgetURL」でWidgetにディープリンクを追加します。

    実際にViewを作成したコードが以下になります。

    1struct SmallWidgetEntryView : View {
    2    var entry: SmallProvider.Entry
    3
    4    var body: some View {
    5        ZStack {
    6            Image("pokemonBackground")
    7                .resizable()
    8            VStack(spacing: 10) {
    9                Image(entry.pokemon.imageName)
    10                    .resizable()
    11                    .aspectRatio(contentMode: .fit)
    12                    .frame(width: 90)
    13                Text(entry.pokemon.name)
    14                    .font(.body)
    15                    .foregroundColor(.white)
    16                    .bold()
    17            }
    18        }
    19        .widgetURL(entry.pokemon.url)
    20    }
    21}

    Widgetの実装③

    Viewが作成できたので、次にIntentTimelineProviderを設定します。

    pleceholder(in:)にはWidgetを追加する前のプレビュー表示に使うTimelineEntryを設定します。

    getSnapshot(for:, in:, completion:)ではWidgetの情報を取得するまでの間、表示しておくTimelineEntryを設定します。

    最後にgetTimeline(for:, in:, completion:)でTimelineの設定をします。
    今回はWidgetの更新はを行わないため、一つだけEntryを作成し、policyを.neverに設定しました。

    実際に設定したコードは以下になります。

    1   // ここで指定したものが、Widgetを追加する時にプレビュー表示される
    2    func placeholder(in context: Context) -> SmallEntry {
    3        SmallEntry(date: Date(), configuration: MyPokemonIntent(), pokemon: Pokemon.fusigidane)
    4    }
    5
    6    // Widgetが画面に追加された時と、遷移があるたびに呼ばれる
    7    // Widgetを素早く返すためのメソッド
    8    func getSnapshot(for configuration: MyPokemonIntent, in context: Context, completion: @escaping (SmallEntry) -> ()) {
    9        let entry = SmallEntry(date: Date(), configuration: configuration, pokemon: Pokemon.fusigidane)
    10        completion(entry)
    11    }
    12
    13    // WidgetがHome画面に表示されるたびに呼ばれる
    14    // ここでTimelineを作成する
    15    func getTimeline(for configuration: Intent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    16        let entries: [SmallEntry] = [SmallEntry(date: Date(), configuration: configuration, pokemon: pokemon(for: configuration))]
    17        let timeline = Timeline(entries: entries, policy: .never)
    18        completion(timeline)
    19    }

    Widgetの実装④

    IntentTimelineProviderを実装することができたので、次にWidgetを設定します。

    kindではWidgetを判別するための名前、configurationDisplayNameとdescriptionでは、Widgetを設定する時に表示する名前とWidgetについての説明文を設定します。

    そして、最後のsupportedFamiliesでサポートするWidgetサイズを指定します。

    実際に設定したコードは以下になります。

    1struct SmallWidget: Widget {
    2    let kind: String = "SmallWidget"
    3
    4    var body: some WidgetConfiguration {
    5        IntentConfiguration(kind: kind, intent: MyPokemonIntent.self, provider: SmallProvider()) { entry in
    6            SmallWidgetEntryView(entry: entry)
    7        }
    8        .configurationDisplayName("Small Widget")
    9        .description("This is a small widget.")
    10        .supportedFamilies([.systemSmall]) // サポートするウィジェットサイズ
    11    }
    12}

    Widgetの実装⑤

    最後にWidgetを編集するための機能を実装します。

    XcodeのNewfileからIntentファイルを作成し、ParametersにEnumでMyPokemonを追加します。

    そして、MyPokemonからTimelineEntryのPokemonを設定するように実装すれば、Widgetの実装完了です。

    まとめ

    今回実装したものは、Widgetの更新を行わず、またWidgetの編集機能も簡単なものでした。

    実際のアプリに導入するとなると、ユーザーがアプリを操作した時や、プッシュ通知を受け取った時にWidgetの更新するような実装が必要になると思います。

    またWidgetの編集も選択ができるだけでなくTextFieldを表示したり、Switchを表示したりすることもあるかもしれません。

    実際に個人開発アプリに導入する機会などあれば、もうちょっと深掘りしながら導入していこうと思います。

    本記事が何かお役に立てれば幸いです。

    ご覧いただき、ありがとうございました。

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事