Rork Max × WidgetKit・ライブアクティビティ実装ガイド 2026 — Dynamic Island 対応まで
iOS 16 以降、Widget はホーム画面だけでなくロック画面にも配置できるようになりました。さらに Dynamic Island と連携したライブアクティビティが加わり、アプリのエンゲージメントを大きく高める機能として定着しています。ユーザーがアプリを開かなくても最新情報を届けられる Widget は、リテンション向上と収益化の両面で重要な役割を果たします。
Rork Max はネイティブ Swift コードを生成できる AI アシスト開発環境です。WidgetKit・ActivityKit の実装も Rork Max のプロンプトドリブン開発で行えますが、SwiftUI の仕組みや Widget 固有の制約を理解していないと、Rork Max が出力したコードに誤りが含まれていても気づけません。ここでは実装の原理から丁寧に解説し、プロが使える水準のウィジェットを構築するための知識を体系的に提供します。
私自身、個人開発で運用している壁紙アプリに「今日の一枚」を映すウィジェットを載せたとき、最初に面食らったのが更新のされなさでした。getTimeline で数分おきに新しい画像へ差し替えるつもりが、iOS は省電力のために再読み込みをかなり間引きます。そこで方針を変え、一日分のエントリをあらかじめ Timeline にまとめて積んでおき、深夜0時に一度だけ全体を組み直す設計にしました。リクエスト回数を最小限に抑えつつ、ユーザーから見れば毎朝きちんと絵柄が変わる。この「先に焼き込む」発想にたどり着くまで、何度も実機の挙動を観察した記憶があります。以降の各セクションでも、こうした実運用での手触りを添えながら進めていきます。
1. WidgetKit の基本アーキテクチャを理解する
WidgetKit はアプリ本体とは別プロセスで動作する Widget Extension として実装されます。アプリのコードを直接呼び出せないため、AppGroup や UserDefaults(suite 付き)を使ってデータを共有する設計が必要です。
Widget の核となるコンポーネントは3つです。
TimelineEntry。 特定の時刻における Widget の状態を表す構造体です。表示したいデータをすべてここに持たせます。
TimelineProvider。 iOS が「次のタイムラインをいつ更新するか」を判断するために呼び出すプロトコルです。placeholder・getSnapshot・getTimeline の3メソッドを実装します。
EntryView。 TimelineEntry を受け取って SwiftUI ビューを返す型です。ウィジェットの見た目はすべてここに書きます。
// 最小構成のWidgetKit実装
import WidgetKit
import SwiftUI
// 1. TimelineEntry — ウィジェットに表示するデータ
struct AppEntry : TimelineEntry {
let date: Date
let title: String
let progress: Double // 0.0 〜 1.0
}
// 2. TimelineProvider — タイムライン供給ロジック
struct AppProvider : TimelineProvider {
func placeholder ( in context: Context) -> AppEntry {
AppEntry ( date : .now, title : "読み込み中..." , progress : 0.5 )
}
func getSnapshot ( in context: Context,
completion : @escaping (AppEntry) -> Void ) {
let entry = AppEntry ( date : .now, title : "今日のタスク: 5件" , progress : 0.6 )
completion (entry)
}
func getTimeline ( in context: Context,
completion : @escaping (Timeline<AppEntry>) -> Void ) {
// AppGroupからデータを取得
let defaults = UserDefaults ( suiteName : "group.com.yourapp.shared" )
let title = defaults ? . string ( forKey : "widget_title" ) ?? "タスクなし"
let progress = defaults ? . double ( forKey : "widget_progress" ) ?? 0.0
let entry = AppEntry ( date : .now, title : title, progress : progress)
// 15分後に再更新をリクエスト
let nextUpdate = Calendar.current. date ( byAdding : .minute, value : 15 , to : .now) !
let timeline = Timeline ( entries : [entry], policy : . after (nextUpdate))
completion (timeline)
}
}
// 3. EntryView — SwiftUIビューの定義
struct AppWidgetEntryView : View {
var entry: AppProvider.Entry
var body: some View {
VStack ( alignment : .leading, spacing : 8 ) {
Text (entry.title)
. font (.headline)
. lineLimit ( 2 )
ProgressView ( value : entry.progress)
. tint (.blue)
Text ( " \( Int (entry. progress * 100 ) ) % 完了" )
. font (.caption)
. foregroundStyle (.secondary)
}
. padding ()
. containerBackground (.background, for : .widget)
}
}
// 4. Widget定義
@main
struct AppWidget : Widget {
let kind = "AppWidget"
var body: some WidgetConfiguration {
StaticConfiguration ( kind : kind, provider : AppProvider ()) { entry in
AppWidgetEntryView ( entry : entry)
}
. configurationDisplayName ( "進捗ウィジェット" )
. description ( "タスクの進捗を確認できます。" )
. supportedFamilies ([.systemSmall, .systemMedium, .accessoryCircular, .accessoryRectangular])
}
}
期待する出力 : ホーム画面に「今日のタスク: 5件 / 60% 完了」のウィジェットが表示されます。
Rork Max にこの構造を理解させてプロンプトを書くと、実用的なコードが出力されます。「WidgetKit Extension を作成して、AppGroup 経由でデータを受け取り、ホーム画面とロック画面に対応したウィジェットを実装してください。supportedFamilies は small・medium・accessoryCircular・accessoryRectangular を含めてください」のように、具体的な要件を指定するのがコツです。
2. Rork Max でのプロジェクトセットアップ
Rork Max でウィジェットを実装する前に、Xcode 側での準備が必要です。Rork Max は基本的なファイル生成は行いますが、Xcode のターゲット設定や Capability の追加は手動で行う必要があります。
2-1. Widget Extension ターゲットを追加する
Xcode のメニューから「File → New → Target → Widget Extension」を選択します。このとき「Include Live Activity」にもチェックを入れておくと、ActivityKit 用のボイラープレートも生成されます。
ターゲット名は AppNameWidget のように命名するのが慣例です。バンドル ID は com.yourapp.widget という形式になります。
2-2. AppGroup を設定する
アプリ本体と Widget Extension の間でデータを共有するには AppGroup が必須です。Xcode の「Signing & Capabilities」タブから両ターゲットに同じ AppGroup ID(例: group.com.yourapp.shared)を追加します。
2-3. Rork Max へのプロンプト設計
セットアップが完了したら、Rork Max に次のようにプロンプトを渡します。
Widget Extension ターゲット「TaskWidget」が追加済みです。
AppGroup ID は「group.com.yourapp.shared」です。
以下の仕様で TaskWidgetBundle.swift・TaskWidget.swift を実装してください:
- アプリ側では UserDefaults(suiteName:) でタスク件数と完了率を保存する
- ウィジェット側では同じ UserDefaults から値を読み取り表示する
- 対応ファミリー: systemSmall, systemMedium, accessoryCircular, accessoryRectangular
- iOS 17 の .containerBackground モディファイアを使用する
具体的なファイル名・AppGroup ID・対応ファミリーを明示することで、Rork Max の出力精度が大幅に向上します。
3. ロック画面ウィジェット(Accessory Family)の実装
iOS 16 から追加されたロック画面ウィジェットは、accessoryCircular・accessoryRectangular・accessoryInline の3サイズがあります。アプリの認知度向上に非常に効果的で、特に習慣化系・健康系・生産性系アプリとの相性が抜群です。
// ロック画面ウィジェット向けビュー分岐の実装
struct TaskWidgetEntryView : View {
var entry: TaskProvider.Entry
@Environment (\.widgetFamily) var widgetFamily
var body: some View {
switch widgetFamily {
case .systemSmall :
SmallWidgetView ( entry : entry)
case .systemMedium :
MediumWidgetView ( entry : entry)
case .accessoryCircular :
// ロック画面: 円形
Gauge ( value : entry.progress) {
Image ( systemName : "checkmark.circle" )
} currentValueLabel : {
Text ( " \( Int (entry. progress * 100 ) ) " )
. font (.caption2)
}
. gaugeStyle (.accessoryCircular)
case .accessoryRectangular :
// ロック画面: 矩形
VStack ( alignment : .leading) {
Label ( " \( entry. completedCount ) / \( entry. totalCount ) 件" , systemImage : "checklist" )
. font (.headline)
Text (entry.nextTaskTitle)
. font (.caption)
. foregroundStyle (.secondary)
. lineLimit ( 1 )
}
case .accessoryInline :
// ロック画面: 1行テキスト
Label ( " \( entry. completedCount ) / \( entry. totalCount ) タスク完了" , systemImage : "checklist" )
default:
SmallWidgetView ( entry : entry)
}
}
}
ロック画面ウィジェットは モノクロ表示 (システムが色を管理)であることに注意してください。@Environment(\.widgetRenderingMode) で現在の描画モードを取得し、フルカラー・ビブラント・アクセントカラーを適切に使い分けることができます。
4. ライブアクティビティと Dynamic Island の実装
iOS 16.1 で導入されたライブアクティビティは、ActivityKit フレームワークを通じてロック画面と Dynamic Island にリアルタイムな情報を表示します。配達追跡・スポーツスコア・フードデリバリーなど「進行中の状態」を持つアプリに最適です。
4-1. ActivityAttributes の定義
ライブアクティビティのデータ構造は2種類に分かれます。変化しない情報 (ContentState に含めない)とリアルタイムに更新される情報 (ContentState)です。
import ActivityKit
// ライブアクティビティのデータ定義
struct DeliveryAttributes : ActivityAttributes {
// 変化しない情報(アクティビティ開始時のみ設定)
let orderId: String
let storeName: String
let customerName: String
// リアルタイムに更新される情報
struct ContentState : Codable , Hashable {
var status: DeliveryStatus
var estimatedMinutes: Int
var currentLocation: String
enum DeliveryStatus : String , Codable {
case preparing = "準備中"
case picked = "ピックアップ済み"
case delivering = "配達中"
case arrived = "到着"
}
}
}
4-2. Dynamic Island の UI 実装
Dynamic Island は Compact Leading・Compact Trailing・Minimal・Expanded の4つの表示形式があります。
struct DeliveryLiveActivityView : Widget {
var body: some WidgetConfiguration {
ActivityConfiguration ( for : DeliveryAttributes. self ) { context in
// ロック画面 / バナー表示
LockScreenLiveActivityView ( context : context)
} dynamicIsland : { context in
DynamicIsland {
// Expanded(長押し展開時)
DynamicIslandExpandedRegion (.leading) {
VStack ( alignment : .leading) {
Label (context.attributes.storeName,
systemImage : "bag.fill" )
. font (.caption)
Text (context.state.currentLocation)
. font (.caption2)
. foregroundStyle (.secondary)
}
}
DynamicIslandExpandedRegion (.trailing) {
VStack ( alignment : .trailing) {
Text ( "あと" )
. font (.caption2)
Text ( " \( context. state . estimatedMinutes ) 分" )
. font (.title2. bold ())
. foregroundStyle (.orange)
}
}
DynamicIslandExpandedRegion (.bottom) {
HStack {
ForEach (DeliveryAttributes.ContentState.DeliveryStatus.allCases,
id : \. self ) { step in
StatusStepView (
step : step,
isActive : context.state.status == step,
isCompleted : step.order < context.state.status.order
)
}
}
. padding (.horizontal)
}
} compactLeading : {
// Compact Leading(通常表示・左側)
Image ( systemName : "bicycle" )
. foregroundStyle (.orange)
} compactTrailing : {
// Compact Trailing(通常表示・右側)
Text ( " \( context. state . estimatedMinutes ) 分" )
. font (.caption. bold ())
. foregroundStyle (.orange)
} minimal : {
// Minimal(複数アクティビティ競合時)
Image ( systemName : "bicycle" )
. foregroundStyle (.orange)
}
. widgetURL ( URL ( string : "yourapp://delivery/ \( context. attributes . orderId ) " ))
. keylineTint (.orange)
}
}
}
4-3. アクティビティの開始・更新・終了
アプリ側からライブアクティビティを制御するコードは次のようになります。
import ActivityKit
class DeliveryActivityManager {
private var currentActivity: Activity<DeliveryAttributes> ?
// アクティビティを開始する
func startActivity ( orderId : String , storeName : String ) throws {
guard ActivityAuthorizationInfo ().areActivitiesEnabled else {
throw ActivityError.notEnabled
}
let attributes = DeliveryAttributes (
orderId : orderId,
storeName : storeName,
customerName : "政樹"
)
let initialState = DeliveryAttributes. ContentState (
status : .preparing,
estimatedMinutes : 30 ,
currentLocation : "店舗で準備中"
)
currentActivity = try Activity. request (
attributes : attributes,
content : . init ( state : initialState, staleDate : nil ),
pushType : .token // APNs経由のプッシュ更新を使う場合
)
print ( "✅ アクティビティ開始: \( currentActivity ? . id ?? "" ) " )
}
// 状態を更新する(例: 配達員がピックアップした)
func updateActivity ( status : DeliveryAttributes.ContentState.DeliveryStatus,
minutes : Int , location : String ) async {
let newState = DeliveryAttributes. ContentState (
status : status,
estimatedMinutes : minutes,
currentLocation : location
)
// アラート付きで更新(バナー通知が表示される)
let alertConfig = AlertConfiguration (
title : "配達状況が更新されました" ,
body : " \( location ) — あと \( minutes ) 分" ,
sound : .default
)
await currentActivity ? . update (
. init ( state : newState, staleDate : nil ),
alertConfiguration : alertConfig
)
}
// アクティビティを終了する
func endActivity () async {
let finalState = DeliveryAttributes. ContentState (
status : .arrived,
estimatedMinutes : 0 ,
currentLocation : "配達完了"
)
await currentActivity ? . end (
. init ( state : finalState, staleDate : nil ),
dismissalPolicy : . after (.now. addingTimeInterval ( 30 )) // 30秒後に消える
)
}
}
期待する出力 : アプリがバックグラウンドにあっても Dynamic Island に「🚴 あと12分」が表示され続け、状態更新時にバナー通知が表示されます。
5. Timeline 更新戦略と更新頻度の最適化
WidgetKit の Timeline 更新は OS によって最適化されており、短すぎる更新間隔を指定しても OS が制限します。一般的に、バックグラウンドでの更新は1日あたり約40〜70回が目安です。
5-1. 更新ポリシーの選択
// 3種類の更新ポリシー
Timeline ( entries : entries, policy : .atEnd) // 最後のエントリが表示されたら更新
Timeline ( entries : entries, policy : . after (date)) // 指定時刻以降に更新
Timeline ( entries : entries, policy : .never) // アプリからの明示的なリロードのみ
静的コンテンツ (天気・カレンダー・タスク)には .after(date) が向いています。15〜60分の間隔を設定するのが現実的です。
動的コンテンツ (スポーツスコア・株価)は .atEnd と短い entries 配列を組み合わせ、アプリ側から WidgetCenter.shared.reloadTimelines(ofKind:) を使って能動的にリロードします。
5-2. アプリ側からのリロードトリガー
import WidgetKit
// アプリのデータが更新されたときにウィジェットを再描画する
func updateWidget ( title : String , progress : Double ) {
// AppGroupにデータを保存
let defaults = UserDefaults ( suiteName : "group.com.yourapp.shared" )
defaults ? . set (title, forKey : "widget_title" )
defaults ? . set (progress, forKey : "widget_progress" )
// Widgetのタイムラインを即座にリロード
WidgetCenter.shared. reloadTimelines ( ofKind : "AppWidget" )
// または全てのウィジェットをリロード
// WidgetCenter.shared.reloadAllTimelines()
}
6. App Intents を使ったインタラクティブ Widget
iOS 17 からウィジェット上で直接タップ操作が可能になりました。AppIntent を使えば、アプリを開かずにタスクを完了したりトグルをオンにしたりできます。これはユーザー体験の大幅な改善につながります。
import AppIntents
import WidgetKit
// ボタンタップ時に実行されるIntent
struct CompleteTaskIntent : AppIntent {
static var title: LocalizedStringResource = "タスクを完了"
@Parameter (title : "タスクID" )
var taskId: String
init () {}
init ( taskId : String ) {
self .taskId = taskId
}
func perform () async throws -> some IntentResult {
// AppGroupを通じてタスクを完了状態に更新
let defaults = UserDefaults ( suiteName : "group.com.yourapp.shared" )
var completedIds = defaults ? . stringArray ( forKey : "completed_task_ids" ) ?? []
if ! completedIds. contains (taskId) {
completedIds. append (taskId)
defaults ? . set (completedIds, forKey : "completed_task_ids" )
}
// Widgetを即座に更新
WidgetCenter.shared. reloadTimelines ( ofKind : "TaskWidget" )
return . result ()
}
}
// インタラクティブなウィジェットビュー
struct InteractiveTaskRow : View {
let task: TaskItem
var body: some View {
Button ( intent : CompleteTaskIntent ( taskId : task.id)) {
HStack {
Image ( systemName : task.isCompleted
? "checkmark.circle.fill" : "circle" )
. foregroundStyle (task.isCompleted ? .green : .secondary)
Text (task.title)
. strikethrough (task.isCompleted)
. foregroundStyle (task.isCompleted ? .secondary : .primary)
}
}
. buttonStyle (.plain)
}
}
この実装により「ウィジェット上のチェックボックスをタップするだけでタスクが完了する」体験が実現します。Rork Max にこのパターンを実装させる場合は、「AppIntent を使ったインタラクティブ Widget を実装してください。Intent は AppGroup の UserDefaults を更新してから WidgetCenter.reloadTimelines を呼び出す設計にしてください」と明示するのが重要です。
ウィジェットの設計思想はネイティブ開発の原則と深く関わっています。
7. よくあるエラーと対処法
Widget 開発では特有のトラブルが発生しやすいです。代表的なものをまとめます。
「Widget が更新されない」問題。
最も多いのが AppGroup の設定漏れです。アプリ本体と Widget Extension の両方に同じ AppGroup ID が追加されているか、Entitlements ファイルに正しく記述されているかを確認してください。シミュレーターでは WidgetCenter.shared.reloadAllTimelines() を呼び出してもすぐに反映されないことがあります。実機でテストするか、Widget の Preview を使いましょう。
「ライブアクティビティが開始できない」問題。
Info.plist に NSSupportsLiveActivities キーが YES で追加されているか確認します。また、iOS 16.1 以降の実機が必要で、シミュレーターではテストできません。
「Compact/Minimal が正しく表示されない」問題。
Dynamic Island の Compact 表示は iPhone 14 Pro 以降の機種でしか確認できません。Xcode のシミュレーターで「iPhone 15 Pro」を選択してテストしてください。
「containerBackground の警告が出る」問題。
iOS 17 以降では .containerBackground(.background, for: .widget) が必須です。古いコードでは ZStack + .background(Color(...)) パターンが使われていましたが、iOS 17 以降はシステムが背景を管理するためこの方法は非推奨になっています。
「Timeline の更新頻度が期待通りでない」問題。
OS のバッテリー最適化により、更新間隔は保証されません。低電力モード時や特定の条件下では更新が大きく遅れることがあります。重要な更新が必要な場合はプッシュ通知(APNs)経由で Widget に送信する「Push-to-Widget」パターンを検討してください。
8. ライブアクティビティと APNs プッシュ更新
ライブアクティビティはアプリがバックグラウンドにある間も APNs 経由で状態を更新できます。サーバー側から最新情報をプッシュする設計により、アプリのプロセスを起動することなく Dynamic Island の内容を更新できます。
// ActivityKit の pushToken を取得してサーバーへ送信
func startActivityWithPushToken ( orderId : String ) async throws {
let activity = try Activity < DeliveryAttributes > . request (
attributes : DeliveryAttributes ( orderId : orderId, storeName : "カフェ" , customerName : "政樹" ),
content : . init ( state : . init ( status : .preparing, estimatedMinutes : 30 , currentLocation : "準備中" ), staleDate : nil ),
pushType : .token // Push更新を有効化
)
// pushToken の変化を監視してサーバーへ送信
for await data in activity.pushTokenUpdates {
let token = data. map { String ( format : "%02x" , $0 ) }. joined ()
print ( "Push Token: \( token ) " )
// サーバーのAPIエンドポイントへトークンを送信
await sendTokenToServer ( activityId : activity.id, pushToken : token)
}
}
APNs 経由のプッシュ通知の基本的な実装については、Rork Max でプッシュ通知を実装する完全ガイドも参照してください。ライブアクティビティ向けの APNs は通常の通知とペイロード構造が異なりますが、APNs 証明書の設定手順は共通です。
サーバー側では次のようなペイロードで APNs に送信します。
{
"aps" : {
"timestamp" : 1712300000 ,
"event" : "update" ,
"content-state" : {
"status" : "delivering" ,
"estimatedMinutes" : 12 ,
"currentLocation" : "あなたの近くにいます"
},
"alert" : {
"title" : "配達状況が更新されました" ,
"body" : "あと12分で到着します"
}
}
}
この設計により、サーバーから直接 Dynamic Island の表示を更新でき、バッテリー効率が大幅に向上します。
9. 収益化・リテンションへの活用
Widget は「無料でも使える機能」として提供するか、「プレミアム限定機能」として収益化のフックにするかで戦略が異なります。
プレミアム限定ウィジェット戦略。 基本的なウィジェット(Small サイズのみ)は無料で提供し、Medium/Large サイズや Dynamic Island 対応のライブアクティビティはプレミアム限定にするモデルが有効です。サブスクリプション課金の実装についてはRork Max で StoreKit 2 の In-App Purchase を実装するで詳しく解説しています。また、Rork Max で Apple ネイティブ API を活用する完全ガイド では HealthKit や ARKit など他のネイティブ API との連携パターンも紹介しています。ウィジェット追加画面でプレミアム限定のサイズを「ロック」アイコン付きで表示し、タップしたときにペイウォールを見せる設計が自然なアップセルになります。
エンゲージメント向上の測定。 ウィジェットからアプリが開かれた場合は .widgetURL に ディープリンク URL を設定し、アプリ側で計測します。
// ウィジェット → アプリの遷移を計測
. widgetURL ( URL ( string : "yourapp://widget?kind=small&source=homescreen" ))
アプリ側の onOpenURL ハンドラでこの URL を受け取り、Firebase Analytics や Mixpanel にイベントとして記録することで、Widget がどれだけコンバージョンに寄与しているかを把握できます。
また、アプリのアイコンとウィジェットのデザインを統一することでブランド認知度が高まります。ストアのスクリーンショットにウィジェットが映り込んだ画像を使うと ASO(App Store Optimization)でも効果が出ます。
10. FAQ
Q1. Rork Max で Widget Extension を自動生成してもらえますか?
Rork Max は Swift コードファイルの生成と編集に対応していますが、Xcode でのターゲット追加や Capability の設定は手動で行う必要があります。「Widget Extension の雛形コードを生成して」というプロンプトで TaskWidget.swift・TaskWidgetBundle.swift・AppIntent.swift のコードを生成してもらい、Xcode に手動で追加するワークフローが現実的です。
Q2. ライブアクティビティはシミュレーターでテストできますか?
ActivityKit(ライブアクティビティ)は iOS 16.1 以降の実機のみで動作し、シミュレーターは非対応です。ただし Xcode 14.2 以降では「ライブアクティビティのプレビュー」機能を使って UI デザインだけはシミュレーターで確認できます。Dynamic Island の表示確認には iPhone 14 Pro 以降の実機、または「iPhone 15 Pro」シミュレーターが必要です。
Q3. ウィジェットの更新頻度はどれくらいが適切ですか?
用途によって異なりますが、一般的なガイドラインとして、リアルタイム性が重要でないアプリ(ニュース・天気)は30〜60分間隔、習慣管理・カレンダー系は1時間以上が推奨されます。更新頻度を上げすぎるとバッテリー消費の観点から OS に制限されることがあります。高頻度の更新が必要な場合は APNs を使ったプッシュ更新が最も効率的です。
Q4. 複数の Widget を1つの Extension で管理できますか?
はい、WidgetBundle を使えば1つの Extension で複数の Widget 種類を定義できます。@main を付けた WidgetBundle の中に複数の Widget を並べるだけです。ただし、ライブアクティビティの ActivityConfiguration も WidgetBundle に含めることができます。App Store の審査では Extension ごとに審査されるため、Extension を分割せず一つにまとめる設計が管理しやすいです。
Q5. Rork Max で生成されたウィジェットコードに containerBackground エラーが出ます。
これは Rork Max が iOS 16 以前のコードを出力している場合に起きます。プロンプトに「iOS 17 以降向けに実装してください。containerBackground モディファイアを使用してください」と明記するか、生成されたコードの .background(...) を .containerBackground(.background, for: .widget) に置き換えてください。
全体を振り返って
WidgetKit・ライブアクティビティ・Dynamic Island は、アプリのエンゲージメントとブランド認知度を高める重要な機能です。Rork Max を使えばコードの大部分を自動生成できますが、AppGroup の設定・Timeline 更新戦略・ActivityKit の APNs 連携など、ネイティブ開発の知識が不可欠なポイントがあります。本記事で解説した仕組みを理解したうえで Rork Max に指示を与えることで、プロクオリティのウィジェットを効率よく実装できるようになります。
ウィジェット開発