数年前、運用中のアプリにホーム画面ウィジェットを追加したところ、機能はほぼ同じままなのに起床時間帯の起動数が目に見えて増えた経験があります。以来、私は新しい「置き場所」が OS に増えるたびに、機能追加より先に導線追加を検討するようになりました。
iOS 18 で増えた置き場所が、コントロールセンターです。WidgetKit の ControlWidget を使うと、自作のボタンやトグルをコントロールセンター・ロック画面・アクションボタンに配置できます。Rork Max はネイティブ Swift と拡張ターゲットを扱えるため、この領域も指示の出し方さえ押さえれば届きます。
機能としては地味です。ただ、個人開発のアプリが毎日思い出してもらうための装置として、コストに対する効きは良い部類だと考えています。
導線としての位置づけ — 既存の置き場所との違い
すでにホーム画面ウィジェットや App Shortcuts を実装している方向けに、性格の違いを整理します。
観点 ホーム画面ウィジェット App Shortcuts(Siri/Spotlight) ControlWidget ユーザーの操作コスト ホーム画面へ移動して視認 検索または発話 画面右上から一振り(どの画面からでも) 表現力 高い(情報表示が主役) 低い(テキスト中心) 低い(アイコンと1アクション) 向いている機能 状態の常時表示 名前で呼べる操作 反射的に使う単発操作
ControlWidget の本質は「どの画面からでも1スワイプで届く 」ことです。表現力を捨てる代わりに到達コストを最小化しています。メモの新規作成、記録の開始・停止、今日の1件を開く——考えずに指が動く類の操作をひとつだけ選んで置く場所です。
リテンションの文脈で言えば、起動のきっかけを OS の一等地に常駐させる意味があります。広告収益で運営しているアプリなら、DAU の底上げはそのまま収益の底上げです。私自身、AdMob 中心で運営してきた経験から、この種の「思い出してもらう装置」への投資は機能追加より回収が読みやすいと感じています。
最小実装 — アプリを開くボタン
ControlWidget はウィジェット拡張ターゲットの中に定義します。もっとも単純な、アプリの特定画面を開くボタンから始めます。
import WidgetKit
import SwiftUI
import AppIntents
// コントロールから起動されるインテント。openAppWhenRun でアプリを前面に出す
struct OpenQuickMemoIntent : AppIntent {
static let title: LocalizedStringResource = "クイックメモを開く"
static let openAppWhenRun: Bool = true
func perform () async throws -> some IntentResult & OpensIntent {
// アプリ側は onOpenURL やシーン復元でこの遷移を受ける
return . result ( opensIntent : OpenURLIntent ( URL ( string : "myapp://quick-memo" ) ! ))
}
}
struct QuickMemoControl : ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration ( kind : "com.example.myapp.quickmemo" ) {
ControlWidgetButton ( action : OpenQuickMemoIntent ()) {
Label ( "クイックメモ" , systemImage : "square.and.pencil" )
}
}
. displayName ( "クイックメモ" )
. description ( "どの画面からでもメモ入力を開きます。" )
}
}
ポイントは2つです。openAppWhenRun = true を忘れると、インテントはバックグラウンドで実行されてアプリが前面に出ません。そして深いリンク先(この例では myapp://quick-memo)をアプリ側で受ける実装が別途必要です。コントロールは玄関であって、案内はアプリ本体の仕事です。
状態を持つ機能はトグルで — ControlWidgetToggle
記録の開始・停止のような二値の機能には ControlWidgetToggle を使います。現在状態の提供には ControlValueProvider を実装します。
struct RecordingControl : ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration (
kind : "com.example.myapp.recording" ,
provider : RecordingStateProvider ()
) { isRecording in
ControlWidgetToggle (
"記録" ,
isOn : isRecording,
action : ToggleRecordingIntent ()
) { on in
Label (on ? "記録中" : "停止中" ,
systemImage : on ? "record.circle.fill" : "record.circle" )
}
}
. displayName ( "記録の開始/停止" )
}
}
struct RecordingStateProvider : ControlValueProvider {
// ギャラリーのプレビューに使われる値
var previewValue: Bool { false }
func currentValue () async throws -> Bool {
// App Group 経由でアプリ本体と状態を共有する
UserDefaults ( suiteName : "group.com.example.myapp" ) ?
. bool ( forKey : "isRecording" ) ?? false
}
}
トグルの状態はアプリ本体と共有する必要があるため、App Group の UserDefaults か共有ファイルを使います。そしてアプリ側で状態が変わったら ControlCenter.shared.reloadControls(ofKind:) を呼んでコントロールの表示を更新します。ここを忘れると「アプリでは停止したのにコントロールは記録中のまま」という不整合が起きます。実装後に必ず一度は踏む詰まりどころなので、状態変更とセットで reload を書く習慣にしてしまうのが安全です。
Rork Max への指示 — 拡張ターゲットを前提に分割する
コントロールはウィジェット拡張の中に置くため、Rork Max への指示も本体とは分けます。私が使っている分割は次の3段です。
「ウィジェット拡張に iOS 18 の ControlWidget を追加して。kind は逆ドメイン形式、アクションボタン・ロック画面・コントロールセンターで使える前提で」
「AppIntent は openAppWhenRun = true で、myapp://quick-memo を開く構成に」
「アプリ本体に myapp:// スキームの受け口を追加して、quick-memo でメモ入力画面へ遷移」
拡張と本体をまたぐ機能は、1回のプロンプトに混ぜるとどちらかが中途半端になりがちです。拡張側→インテント→本体の受け口 の順で1つずつ確かめながら進めると、生成コードの検証もしやすくなります。
if #available(iOS 18.0, *) の分岐が必要になる最低ターゲット構成なら、その旨も明示してください。指示にないと、ターゲットによってはビルドエラーで止まります。
置いてもらうまでが仕事 — アプリ内の案内
実装して終わりにならないのが ControlWidget の難しいところです。ユーザーはコントロールセンターの編集画面(ギャラリー)から自分で追加する必要があり、この存在に気づく人は多くありません。
機能を3回以上使ったユーザーにだけ、アプリ内で一度だけ案内を出すことを推奨します。「コントロールセンターからも開けます」という1枚のシートに、追加手順を3ステップで載せる。全員に初回表示すると邪魔になり、誰にも出さないと存在しないのと同じになります。使い込んでいる人にだけ届ける、という絞り方が体感として一番収まりが良いです。
計測も仕込んでおきましょう。インテントの perform() 内でアナリティクスイベントを送れば、コントロール経由の起動が何割を占めるかを追えます。導線としての投資判断は、この数字があって初めてできます。
詰まりどころの短いリスト
最後に、実装時に手が止まりやすい点と対処をまとめます。
ギャラリーに出ない : 拡張ターゲットのビルドが古い可能性。実機でアプリを一度起動し直すと反映されることが多いです
タップしても無反応 : openAppWhenRun の欠落か、URL スキームの受け口不在。まずインテント単体を App Shortcuts から呼んで切り分けます
状態が更新されない : reloadControls(ofKind:) の呼び忘れ。状態を書き換える全箇所を grep して確認してください
派手さのない API ですが、毎日使われるアプリになるための足場としては堅実です。まずはアプリを開くだけのボタン1個から、置いてみてください。