はじめまして。サービス開発パートナー事業部(旧:AIR Design for Apps事業部)iOSエンジニアの住山です。好きなキーボードはHHKBです。 さて今回はWidgetKitに入門してみたので、記事にしていきたいと思います。
実行環境
- Xcode 14.3
- Swift 5.8
ディレクトリ構成
説明に不要なファイルは省略してます。
. ├── WidgetPractice.xcodeproj ├── WidgetPractice │ ├── Assets.xcassets │ ├── ContentView.swift │ └── WidgetPracticeApp.swift └── WidgetGlpgs ├── Assets.xcassets ├── Info.plist ├── WidgetGlpgs.swift ├── WidgetGlpgsBundle.swift └── WidgetGlpgsLiveActivity.swift
Xcodeの「File」→「New」→「Target」でWidget Extensionを追加すると、必要なファイルが追加されます。(上記ディレクトリ内のWidgetGlpgsフォルダ配下)
ゴール
みんな大好きGithubAPIからjsonを取得して、内容をWidgetに表示。レポジトリはこちら
ウィジェット作成の概要
1. Widgetの各種設定
コード
struct WidgetGlpgs: Widget { let kind: String = "WidgetGlpgs" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in WidgetGlpgsEntryView(entry: entry) } .configurationDisplayName("Widget Name") /// Widgetの説明。 .description("Widgetの名前") /// Widgetのサイズ一覧。 .supportedFamilies([ /// accessoryXXXはロック画面用。iOS16〜 .accessoryCircular, .accessoryInline, .accessoryRectangular, /// サイズ一覧 /// ExtraLargeはiPadOS専用。(https://developer.apple.com/documentation/widgetkit/widgetfamily/systemextralarge) .systemExtraLarge, .systemLarge, .systemMedium, .systemSmall ]) } }
.configurationDisplayName("Widget Name")
ウィジェットの名前。
.description("Widgetの説明")
ウィジェットリストでの説明用。
.supportedFamilies([...])
サポートするサイズ一覧です。後述しますが、サイズ毎にViewを作成できます。
- accessoryXXXはロック画面のWidget用。iOS16〜で使えます。
- ExtraLargeはiPadOS専用のようです。
- Widgetサイズ一覧
2. Viewの作成
コード
struct WidgetGlpgsEntryView : View { var entry: Provider.Entry @Environment(\.widgetFamily) private var widgetFamily var body: some View { /// サイズ毎にViewのカスタマイズ if entry.githubUserInfoModel == nil { Text("ロード中") } else { switch widgetFamily { case .systemMedium: HStack() { Image(uiImage: ApiClient.getImage()) .resizable() .scaledToFit() .frame(height: 100) VStack(alignment: .leading) { Text(entry.githubUserInfoModel!.location ?? "") Text(entry.githubUserInfoModel!.name) Text("PubRepos: \(entry.githubUserInfoModel!.publicRepos)") } } default: Image(uiImage: ApiClient.getImage()) .resizable() } } } }
@Environment(\.widgetFamily)
でユーザが追加したWidgetのサイズを取得し、サイズ毎にViewを作成できます。
3. タイムラインの作成
IntentTimelineProvider を継承したProviderでライフサイクルを実装します。下記コードです。
struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent(), githubUserInfoModel: nil) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration, githubUserInfoModel: GithubUserInfoModel.fake()) completion(entry)} func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { Task { var entries: [SimpleEntry] = [] let currentDate = Date() let githubUserInfoModel: GithubUserInfoModel? /// 10分毎にViewの更新を依頼。 let entryDate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)! let githubUserInfoModel: GithubUserInfoModel? = { do { return try await ApiClient.fetch() } catch { return nil } }() let entry = SimpleEntry(date: entryDate, configuration: configuration, githubUserInfoModel: githubUserInfoModel) entries.append(entry) let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } } struct SimpleEntry: TimelineEntry { var date: Date let configuration: ConfigurationIntent let githubUserInfoModel: GithubUserInfoModel? }
placeholder()
Widgetを追加後、最初に仮で表示されるViewを設定します。
getSnapshot()
Widgetギャラリーの表示用Viewを設定します。
getTimeline()
ここでWidgetのライフサイクルを設定します。
- Calendar.current.date(...)でViewの更新時間を設定します。上記の例では10分毎に更新するよう設定してます。
(試しに1分毎を設定してみましたが、3分前後で更新されました。Viewの更新タイミンについて、完全にはコントロールできないようです。)
そのため頻繁にTimelineの更新を要求すると、逆にシステム側から制限をくらい、全然更新されない場合もあります。逆によく使われるアプリは更新頻度が高くても、許容されます。この部分に関しては公式の説明がないので、経験でしかなく、まだ謎は多いです。(下のブログから引用。)
4. Previewsの作成
struct WidgetGlpgs_Previews: PreviewProvider { static var previews: some View { WidgetGlpgsEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), githubUserInfoModel: GithubUserInfoModel.fake())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } }
ここの記事がとても参考になりました。 qiita.com
おわりに
以前から気になっていたWidgetをブログ執筆を気に学べました。実務でも提案できるように、良いユーザ体験を与えられるようなWidgetを模索していきたいです。