能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

ウィジット上にダイアログ設定を組み込む - ステップ解説付き(Custom Intent) - iOS Application

1) 概要

WidgetKit とApp Extensionに書いたBlogで、Custom Intentの仕組みに慣れるためにウィジット設定(WidgetConfiguration)が手頃だと書いたので、実際にシンプルなウィジット設定を実装していく。手順をスクリーンショットをつけて説明する。

Apple Widget考古学 - App Extension の技術Stack | Irimasu Densan Planning - いります電算企画

1-1) 一次ソースは存在、ただし

Appleの公式に使い方が掲載されている。

Making a Configurable Widget | Apple Developer Documentation

一次ソースの内容で困ることはないが、指示された箇所を適切に選択しないと戸惑うことがあった、ブログ中では操作順序も示してウィジット設定を実現するまでの手順を説明する。

1-2)教材

シンプルなウィジット設定の実装として開発しているアプリ充電報告さんはウィジット設定上で表示切り替えを教材に進めていく。

Xcodeプロジェクトの前提としてiOS14以上をターゲットとしたiOSアプリのプロジェクトを用意が前提となる。

1-3) 選択肢用の定義

選択肢をString型のenum として定義する。ウィジットとウィジット設定の双方のターゲットに追加することで選択肢をenum一箇所でまとめることができる。

String型を使うのはウィジット設定側でString型のID値を必要とするためで、enumを文字列型とすることでID値と共用したいためでString型のenumを使用する。

具体的には以下の定義を使用する(実際の定義ではローカライズ対応も実施している)。

enum DisplayTypeDefinition: String {
    case slot234 = "Slot2-3-4"
    case slot1 = "Slot1"
    case slot2 = "Slot2"
    case slot3 = "Slot3"
    case slot4 = "Slot4"

    func identifier() -> String {
        self.rawValue
    }

    func displayName() -> String {
        switch self {
        case .slot234:
            return "Stack(Slot2,3,4)"
        case .slot1:
            return "Slot1"
        case .slot2:
            return "Slot2"
        case .slot3:
            return "Slot3"
        case .slot4:
            return "Slot4"
        }
    }
}

実際の使用タイミングは手順途中で登場するのでこちら内容は確認までで問題ない。

2) ウィジットターゲットを追加する

iOSアプリプロジェクトにウィジットターゲットを追加する。 WidgetConfigrationSS1

ウィジット追加時にInclude Configurtion Intent にチェックを入れるとウィジット設定が有効になる。

WidgetConfigrationSS2

未チェックの場合はStaticConfigurationを使用した設定無しウィジットターゲットが設定されるので注意。

3) Intentdefinitionを設定する

追加したウィジットターゲットにあるIntentdefinitionファイルにウィジット用の選択肢を追加する。Xcodeのウィジットターゲットで作成されたIntentdefinitionファイルはウィジット設定向けのパラメーターが有効となっている。

3-1) 列挙を追加する

選択肢を追加するために開発者が最初に着手するのはIntentdefinitionファイルへのEnum(列挙定義)の新規追加作業になる。

WidgetConfigrationSS3

Enumの新規追加に際しては名称を指定する。IntentsのEnumはデフォルトでidentifierとname変数が用意されておりそれぞれString型となっている。

WidgetConfigrationSS4

3-2) 列挙をパラメーターとして追加する

追加した列挙はそのままではウィジットの設定には反映されないの。パラメーターとして追加する。 WidgetConfigrationSS5

パラメーターを追加した際の注意点は、パラメーター名指定後に型を2で追加した列挙定義に変更する必要がある。

3-3) パラメーターのカスタマイズ

パラメーター追加はSiriからウィジット設定全般で使いためパラメーター設定のカスタマイズが必要となる。Resolvable項目ほチェックをオフ、Dynamic Options - Options are provided dynamicallyのチェックをオンに変更する。

WidgetConfigrationSS6

3-4) ビルドと確認

Intentdefinitionファイルの設定は3-3で済んでいるがここでプロジェクトのビルドを実行する。理由としてはIntentdefinitionファイルはXcode内のコードジェネレート対象となっておりビルドを挟まないとコードジェネレーターが動作しない。Intentdefinitionファイルの設定完了後にビルドを実行して実際にコードが生成されたか確認する。

WidgetConfigrationSS7

ビルド後、IntentdefinitionファイルのAssistant 表示でコードが生成されたか確認する。ConfigurationIntentとConfigurationIntentHandlerを確認することとができる。

WidgetConfigrationSS8

4) ウィジット設定用Intentsを追加する

ウィジット設定に選択肢を供給するIntents Extentionを追加する。

4-1) Intentsターゲットを追加する

プロジェクトを選択、+ボタンを押してターゲット追加する。

WidgetConfigrationSS9

Intents Extensitonを選択し次のステップへ遷移する。 WidgetConfigrationSS10

Intents Extensionの追加オプションで、Include UI Extensionのチェックを外しておく(Include UI Extensionのチェックは以前に追加したチェック状態が残っているのでチェック外し忘れに注意)。

WidgetConfigrationSS11

4-2) Supported Intents を設定

IntentsがサポートするIntents をターゲットの設定に追加する。Supported IntentsにConfigurationIntent を追加する。

WidgetConfigrationSS16

4-3) 選択肢用の定義を追加する

1-3 で示した選択使用の定義を追加する。ターゲット構成対象としてIntentsの他ウィジットターゲットも追加する。

enum DisplayTypeDefinition: String {
    case slot234 = "Slot2-3-4"
    case slot1 = "Slot1"
    case slot2 = "Slot2"
    case slot3 = "Slot3"
    case slot4 = "Slot4"

    func identifier() -> String {
        self.rawValue
    }

    func displayName() -> String {
        switch self {
        case .slot234:
            return "Stack(Slot2,3,4)"
        case .slot1:
            return "Slot1"
        case .slot2:
            return "Slot2"
        case .slot3:
            return "Slot3"
        case .slot4:
            return "Slot4"
        }
    }
}

WidgetConfigrationSS12

4-4) IntentHandler を置き換える

Intent ExtentionのIntent Handlerを置き換える。4-3で定義した列挙をDisplayType型に変換したものを選択候補として渡す。

ウィジット設定の選択肢を提供する実装はここまでとなる。

class IntentHandler: INExtension, ConfigurationIntentHandling {
    func provideDisplayTypeOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<DisplayType>?, Error?) -> Void) {

        let displayTypeDefinitions: [DisplayTypeDefinition] = [ .slot234,
                                                                .slot1,
                                                                .slot2,
                                                                .slot3,
                                                                .slot4
                                                              ]

        let displayType = displayTypeDefinitions.map { (displayTypeDefinition) -> DisplayType in
            DisplayType(identifier: displayTypeDefinition.identifier(), display: displayTypeDefinition.displayName())
                                                     }

        completion(INObjectCollection(items: displayType), nil)
    }

    func defaultDisplayType(for intent: ConfigurationIntent) -> DisplayType? {
        return DisplayType(identifier: DisplayTypeDefinition.slot234.identifier(), display: DisplayTypeDefinition.slot234.displayName())
    }
}

WidgetConfigrationSS13

5) ウィジットで選択肢を使用する

ウィジット設定を用意できた時点でウィジットを実行するとウィジット長押しで表示されるメニューにウィジットを編集するを選べるようになる。選んだ選択肢はウィジット生成時にパラメータとして渡される。

WidgetConfigrationSS14

WidgetConfigrationSS15

struct GraphEntryView : View {
    var entry: Provider.Entry

    func displayType() -> DisplayTypeDefinition {
        let defaultValue = DisplayTypeDefinition.slot234
        let rawValue = entry.configuration.displayType?.identifier ?? defaultValue.identifier()

        return DisplayTypeDefinition(rawValue: rawValue) ?? defaultValue
    }

    var body: some View {
        switch self.displayType() {
        case .slot234:
            return Text("スタック表示")
        default:
            return Text("その他")
        }
    }
}

6)まとめ

ウィジット設定について選択肢の実装手順を示した。ウィジット設定はa.検索欄を用意してユーザーに選択肢を提示する、b.位置情報から選択肢を提示するなどバリエーションはあるがまずはウィジット設定を動かすまでにXcodeプロジェクト内のターゲットを行き来する動きに慣れる必要がある。

問題はウィジット自体がアプリに紐づいているためiOS, iPadOS, macOSアプリ開発が前提となるため慣れるほどウィジットを作るアプリ開発者は稀だろう。

私自身ウィジットを実装する際に困ったので本内容が参考になれば幸いである。

参考