カテゴリー: iOS

【iOS14】新機能WidgetKitへの挑戦【実践編】

はじめに

suzukiさんが以前の記事でWidget機能ついて紹介してくれていますが、今回はxcode12も正式リリースされましたし、より実践的なWidgetKitの使い方を紹介したいと思います。

Today Extensionとの違い

今までiOSのWidgetといえばToday Extensionでしたが、WidgetKitとはどのような違いがあるか以下にまとめました。

  • ホーム画面に設置可能
  • SwiftUIのみ対応
  • ユーザーアクションはシングルタップのみ、スクロールやスイッチなどのUIコンポーネントは設置できない
  • サイズは固定の3種類のみ(.systemSmall(2×2サイズ), .systemMedium(4×2サイズ), .systemLarge(4×4サイズ))

また、WWDC20にてWidgetKitの目的は

  • Glanceable(ひと目でわかる)
  • Relevant(関連性)
  • Personalaized (パーソナライズ)

とのことなので、ホーム画面にてユーザーに関連する情報が一目でわかるようにするのがWidgetKitなようなので、Today Extensionではミニアプリ的な側面もありましたが、WidgetKitでは完全に別物としてデザインしなければ行けないと思います。

ターゲットの追加

ターゲットの追加方法に関しましては、以前の記事でご確認ください。

StaticConfiguration

StaticConfigurationとはInclude Configuration Intentチェックなしで追加される、ユーザー設定可能なプロパティを持たないWidgetです。

TimelineProvider

WidgetではTimelineという、いつどのViewを表示するか管理する仕組みを実装します。
まずはEntryを定義します。

struct SimpleEntry: TimelineEntry {
    let date: Date
}

続いて、TimelineProviderの実装です。

struct Provider: TimelineProvider {

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(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)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

getSnapshot

SnapshotはWidgetのプレビュー機能やWidget Galleryで即座にWidgetを表示するために使われます。 SnapshotはひとつのTimelineEntryを受け取ります。 Widgetのコンテンツが提供されるまではモック的な値を入れておきます。

getTimeline

getTimelineは、実際のウィジェット表示に使用されます。TimelineはTimelineEntryオブジェクトの配列です。Timelineの更新は最後のTimelineEntryを表示したとき(.atEnd), 指定した時間が経過した後(.after), アプリからWidgetCenterを利用して更新通知されたら更新する(.never)の3種類があります。 TimelineはTimelineProviderに準拠したProviderによってwidgetsに渡されます。

View

Viewには実際に表示するWidgetを実装します。

struct CharacterWidgetEntryView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily
    var entry: Provider.Entry

    @ViewBuilder // Viewにバリエーションがある時に宣言する
    var body: some View {
        switch family { 
        case .systemSmall: Text(entry.date, style: .time); Text("small")
        case .systemMedium: Text(entry.date, style: .time); Text("medium")
        case .systemLarge: Text(entry.date, style: .time); Text("large")
        @unknown default: fatalError()
        }
    }
}

Widgetのサイズは3種類あるのですが、上記のように現在のサイズが取得できるので、表示をサイズによって変更するようにしています。

IntentConfiguration

IntentConfigurationとはInclude Configuration Intentチェックありで追加される、ユーザー設定可能なプロパティを持つWidgetです。例をあげると、天気のWidgetなど、Widgetを長押しでユーザー設定の変更が可能な機能を持つWidgetになります。

IntentTimelineProvider

今回は、ユーザーが設定できる情報nameを追加したEntryを作成します。

struct SimpleEntry: TimelineEntry {
    let date: Date
    
    var name: String
}

TimelineProviderとの違いは各関数で、インテントの選択を表す追加のパラメータConfigurationIntentを取得します。また、「typealias Intent = ConfigurationIntent」を追記しておきます。「ConfigurationIntent」については後述します。

struct Provider: IntentTimelineProvider {
    
    typealias Intent = ConfigurationIntent
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), name: "")
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), name: "")
        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, name: configuration.Name ?? "")
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Widget設定の変更

ユーザーがWidget設定の変更ができるようにしていきたいと思います。まずはIntentsフレームワークをTargetから追加します。

今回はUI要素は不要なため「Include UI Extension」のチェックは外しておきます。

作成したSampleIntentsのターゲットページに、Supported Intentsという名前のセクションがあります。今回はConfigurationIntentという名前のアイテムを新規で作追加します。

Include Configuration Intentチェックありで作成していれば、.intentdefinitionファイルが作成されているはずなので、先ほど作成したSampleIntentsをターゲットに追加します。
今回はEntryに追加したnameの情報をユーザーに変更させたいのでParametersにNameを追加します。

ここまでやったら、SampleIntentsのIntentHandler.swiftファイルにConfigurationIntentHandlingを追記すると、Xcodeが関数を自動生成を促してくれると思います。

class IntentHandler: INExtension, ConfigurationIntentHandling {

自動生成されたprovideNameOptionsCollection関数を以下のように編集します。

func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
    let nameIdentifiers: [NSString] = [
        "佐藤",
        "田中",
        "鈴木"
    ]
    let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
    completion(allNameIdentifiers, nil)
}

そうしたらViewのbodyにnameを表示するように編集します。

var body: some View {
    Text(entry.name)
        .font(.headline)
        .padding(1)
    Text(entry.date, style: .time)
}

これで準備は完了です。Widgetを長押しすると「ウィジェットを編集」が表示されると思います。

あとは以下のように変更ができます。

アプリからWidgetを更新

アプリが通知を受けて更新するなどといった場合など、アプリ側からWidgetを更新することが可能です。
以下を呼び出すことで、アプリのWidgetすべてをリロードさせることが可能です。

WidgetCenter.shared.reloadAllTimelines()

また、アプリ内で複数のWidgetを管理している場合、特定のWidgetを更新したい場合は以下のように行います。

WidgetCenter.shared.reloadTimelines(ofKind: "com.example.WidgetSample.WidgetTest1")

さいごに

今回のWidget機能はできることが絞られている分、デザインの大事さを痛感しますね。ミニアプリのような使い方ではないのでAndroidのWidgetとも違い、リッチなアイコンのような印象です。

おすすめ書籍

nukky

シェア
執筆者:
nukky
タグ: SwiftSwiftUI

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前