プログラミング

iOSウィジェット開発の「できること・できないこと」──WidgetKitとApp Intentsで理解するカスタマイズの限界

記事内に商品プロモーションを含む場合があります

iOSのウィジェット機能は、アプリの情報をホーム画面に常時表示できる便利な仕組みとして定着している。しかし、いざ開発に着手すると「思ったより自由度が低い」と感じる場面に出くわすことも多い。ウィジェットを長押しして表示される「ウィジェットを編集」画面で設定できる項目は、実はかなり限定されている。

本記事では、WidgetKitとApp Intentsの仕組みを整理しながら、ウィジェット編集画面でできること・できないことを明確に切り分けて解説する。「なぜここまでしか作り込めないのか」を理解することで、設計段階での判断ミスを減らせるはずだ。

WidgetKitのアーキテクチャ

WidgetKitの基本構造

WidgetKitでウィジェットを作るには、3つの主要コンポーネントを理解する必要がある。

Widget

アプリのウィジェット定義本体。ウィジェットの種類(systemSmall、systemMedium、systemLargeなど)や表示名、説明文を指定する。WidgetBundleを使えば、1つのアプリで複数種類のウィジェットを提供することも可能だ。

struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("マイウィジェット")
        .description("アプリの情報を表示します。")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

TimelineProvider

ウィジェットの表示内容を時間軸に沿って提供する役割を持つ。システムは定期的にプロバイダーに問い合わせを行い、表示すべきエントリーのリスト(タイムライン)を取得する。

struct Provider: AppIntentTimelineProvider {
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        return Timeline(entries: [entry], policy: .atEnd)
    }
}

Entry

特定の時点で表示するデータを保持する構造体。日付情報は必須で、それ以外はアプリが必要とするデータをプロパティとして追加する。

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}

この3つが連携して、ウィジェットの表示が成り立っている。重要なのは、ウィジェットのViewは通常のSwiftUIと同様に記述できるが、表示更新はシステムが管理するタイムラインに従うという点だ。

App Intentsによるウィジェット設定

iOS 17以降、ウィジェットの設定にはAppIntentConfigurationConfigurationAppIntentを使うのが標準的なアプローチになった(従来のIntentConfigurationは非推奨)。

ConfigurationAppIntentの定義

ウィジェットに設定可能なパラメータを定義するには、ConfigurationAppIntentに準拠した構造体を作成する。

struct ConfigurationAppIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "設定"
    static var description = IntentDescription("ウィジェットの表示内容を設定します。")

    @Parameter(title: "表示モード")
    var displayMode: DisplayMode

    @Parameter(title: "通知を有効化")
    var notificationEnabled: Bool

    @Parameter(title: "ラベル")
    var label: String?
}

ここで定義した@Parameterが、そのままウィジェット編集画面のUIとして反映される。これはシステムが自動生成するもので、カスタムUIを差し込む余地はない。

ウィジェット編集画面で「できること」

ホーム画面でウィジェットを長押しし、「ウィジェットを編集」をタップすると表示される設定画面。開発者がコントロールできる範囲は以下の通りだ。

ウィジェット名・説明文の指定

configurationDisplayNamedescriptionモディファイアで指定する。ウィジェット追加画面やギャラリーに表示されるテキストだ。

パラメータの追加

@Parameterプロパティラッパーを使って、以下のタイプの設定項目を追加できる。

UIの表示
AppEnum ピッカー(選択リスト)
Bool トグルスイッチ
String テキスト入力フィールド
Date 日付ピッカー
AppEntity 動的リスト(データソースから取得)
Int/Double 数値入力フィールド
enum DisplayMode: String, AppEnum {
    case compact = "compact"
    case detailed = "detailed"

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "表示モード"
    static var caseDisplayRepresentations: [DisplayMode: DisplayRepresentation] = [
        .compact: "コンパクト",
        .detailed: "詳細"
    ]
}
設定UIの例

ウィジェット編集画面で「できないこと」

ここからが本題。多くの開発者が期待して、結局できないと知って戸惑うポイントを整理する。

レイアウトの変更

編集画面自体のレイアウトをカスタマイズする方法は存在しない。パラメータは上から順に並ぶだけで、セクション分けや条件付き表示なども不可能だ。

色・フォント・アイコンの指定

編集画面のUI要素に対して、色やフォント、アイコンを指定するAPIは提供されていない。システム標準のスタイルで固定される。

カスタムSwiftUIビューの埋め込み

「ここに独自のプレビューを表示したい」「スライダーで値を調整させたい」といった要望は実現できない。表示できるのは@Parameterで定義したプリミティブな入力コントロールのみだ。

画像や複雑なコンテンツの表示

AppEntity経由で動的リストを表示する場合、各項目にはタイトルとサブタイトル程度しか設定できない。サムネイル画像を付けることはできるが、自由なレイアウトは組めない。

複数パラメータ間の連動

「Aを選んだらBの選択肢が変わる」といった動的な連動もサポート外。すべてのパラメータは独立した設定項目として扱われる。

複数ウィジェット配置時の挙動

ユーザーは同じウィジェットをホーム画面に複数配置できる。このとき、各インスタンスは独立した設定を保持する。

struct Provider: AppIntentTimelineProvider {
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
        // configurationはウィジェットインスタンスごとに異なる値を持つ
        let selectedMode = configuration.displayMode
        // ...
    }
}

設計上の注意点として、設定値はシステムが管理するため、アプリ側から「すべてのウィジェットインスタンスの設定を一括変更する」といった操作はできない。ユーザーが各ウィジェットを個別に編集する前提で設計する必要がある。

widgetURLを使ったディープリンク

ウィジェットはタップ時にアプリを起動できる。widgetURLモディファイアを使えば、URLスキームを指定して特定の画面に遷移させることが可能だ。

struct MyWidgetEntryView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(entry.configuration.label ?? "未設定")
        }
        .widgetURL(URL(string: "myapp://widget/\(entry.configuration.displayMode.rawValue)"))
    }
}

Linkを使えば、ウィジェット内の複数箇所にそれぞれ異なる遷移先を設定することもできる(Medium以上のサイズで有効)。

Link(destination: URL(string: "myapp://action1")!) {
    Text("アクション1")
}

設計判断のポイント

WidgetKitの制約を理解した上で、どう設計すべきか。

設定項目は最小限に

パラメータが多すぎるとUIが縦に長くなり、ユーザー体験が悪化する。本当に必要な設定のみに絞ること。複雑な設定が必要なら、アプリ本体で設定させてウィジェットは結果を表示するだけにする方が現実的だ。

アプリ側との連携を設計する

ウィジェットはあくまで「ひと目で情報を確認する」ためのもの。詳細な操作はアプリに委ねる前提で、widgetURLを使った遷移を設計しておく。

プリセットで複雑さを吸収する

Enumでプリセット的な選択肢を用意し、個別のパラメータを隠蔽するアプローチが有効。「シンプルモード」「詳細モード」のように、ユーザーには大まかな選択だけさせて、内部で複数の設定を切り替える方法だ。

まとめ

WidgetKitのウィジェット編集画面は、設計上の制約が多い。開発者は@Parameterで定義したパラメータを編集UIとして提供できるが、そのUIのレイアウト・スタイル・挙動をカスタマイズする手段は基本的にない。

この制約はAppleの設計思想によるもので、ユーザーに一貫した操作体験を提供し、バッテリー消費を抑え、プライバシーを保護するという目的がある。不満に感じる開発者も多いだろうが、この制約を前提に設計することで、かえってシンプルで使いやすいウィジェットが生まれるともいえる。

ウィジェットに機能を詰め込みすぎず、アプリ本体との役割分担を意識した設計を心がけよう。

出典