← すべての記事

5つのAppleプラットフォーム、共有ファイルは3つ — ReturnはクロスプラットフォームSwiftUIをどう出荷しているのか

瞑想タイマーアプリ Return は、iPhone、iPad、Mac、Apple Watch、Apple TV という5つのAppleプラットフォームで動作します。1 コードベースにはSwiftファイルが40個あります(テストを除く)。そのうち5プラットフォームすべてで共有されているのは3ファイルだけです。 残りは別々のXcodeターゲットに分かれており、TimerManagerAudioManagerContentView といった概念を #if os(...) の条件付きコンパイルで共有せず、あえて重複させています。

共有率はおよそ7.5%。そしてこれは意図した設計です。

この記事で扱うのは、2026年にクロスプラットフォームSwiftUIアプリを出荷するとは実際どういうことなのか、なぜ積極的なコード共有が過大評価されているのか、そして実際に共有できた3ファイルに何が共通していたのか、という3点です。

Apple DeveloperのiOS 26プラットフォームタイル Apple DeveloperのiPadOS 26プラットフォームタイル Apple DeveloperのmacOS 26プラットフォームタイル Apple DeveloperのwatchOS 26プラットフォームタイル Apple DeveloperのtvOS 26プラットフォームタイル

Returnが対象とする5つのプラットフォーム。developer.apple.com での表示に準じています。いずれもXcode上では独立したプラットフォームターゲットであり、実行時の分岐ではありません。

要点

  • Return の内訳:メインターゲット(iOS + iPadOS + macOS)が18ファイル、tvOSターゲットが10ファイル、watchOSターゲットが7ファイル、ウィジェット(Live Activities)が2ファイル、そして Return/Shared/ に置かれた真にクロスプラットフォームなファイルが3つ。合計40。
  • 共有されている3ファイルはいずれも永続化に隣接するもの、すなわち MeditationSessionSessionStoreSessionHistoryView です。iCloud経由で移動する状態であって、プラットフォームに適応するUIではありません。
  • tvOSとwatchOSは、メインターゲット内の #if os(tvOS) 分岐ではなく、独立したXcodeターゲットです。操作モデルの差が大きすぎて、ひとつのContentViewには収まりません。
  • メインのiOS/iPadOS/macOSターゲットの内部ですら #if os ブロックは増殖します。ContentView.swift に10個、LiveActivityManager.swift に8個、VideoBackgroundView.swift に8個、AudioManager.swift に6個。
  • 率直に言えば、5つのAppleプラットフォームをまたぐ積極的な共有は保守上の負債になります。小さな共有コア(永続化層)+プラットフォーム個別のUIという構成のほうが、#if だらけの巨大な1ファイルより速く出荷でき、壊れにくいのです。

プラットフォーム個別の話は、Appleプラットフォーム対応表watchOSの実行時制約Liquid Glass の SwiftUI パターン をあわせてご覧ください。

数字で見る

テストとUIテストを除いた、Swiftファイル数で見たコードベースの構成です。

Return/                            18 files   (iPhone + iPad + Mac, single target)
├── Shared/                         3 files     cross-platform truth   ├── MeditationSession.swift   ├── SessionStore.swift   └── SessionHistoryView.swift
├── ContentView.swift              (10 #if os branches)
├── TimerManager.swift             (2 #if os branches)
├── AudioManager.swift             (6 #if os branches)
├── HealthKitManager.swift
├── LiveActivityManager.swift      (8 #if os branches, iOS-only)
├── ThemeManager.swift
├── VideoBackgroundView.swift      (8 #if os branches)
├── GlassTextShape.swift           (Liquid Glass, see prior post)
├── GlassTimerText.swift
└──  (settings, theme, audio assets, etc.)

ReturnTV/                          10 files   (tvOS, separate target)
├── TVContentView.swift
├── TVTimerManager.swift            duplicates main TimerManager
├── TVAudioManager.swift            duplicates main AudioManager
├── TVDurationPicker.swift
├── TVFocusModifier.swift           tvOS button styles for focus
├── TVSettingsView.swift
└── ReturnWatch Watch App/              7 files   (watchOS, separate target)
├── WatchContentView.swift
├── WatchTimerManager.swift         duplicates main TimerManager
├── WatchAudioManager.swift         duplicates main AudioManager
├── WatchHealthKitManager.swift     duplicates main HealthKitManager (mostly)
├── WatchSettingsView.swift
└── ReturnWidgets/                      2 files   (Live Activity + bundle)
├── ReturnLiveActivity.swift
└── ReturnWidgetsBundle.swift

5つのプラットフォーム、3つの共有ファイル、プラットフォーム別の独立ターゲットが2つ、ウィジェットターゲットが1つ、さらにメインターゲット内部の重い条件付きコンパイル。共有率は約7.5%です。「マルチプラットフォームSwiftUI」を扱うチュートリアルの多くは、これと正反対を勧めます。すなわち、@Environment(\.horizontalSizeClass)#if os(...) であらゆるプラットフォームに適応するひとつの ContentView を書け、と。2 それは2プラットフォーム(iPhone + iPad)でなら機能します。5つになると破綻します。

共有できた3ファイルの共通点

Return/Shared/MeditationSession.swift は、SwiftDataに隣接する値型を定義しています。3

struct MeditationSession: Codable, Identifiable, Equatable {
    let id: UUID
    let startDate: Date
    let endDate: Date
    let durationSeconds: Int
    let sourceDevice: DeviceType
    var syncedToHealthKit: Bool

    enum DeviceType: String, Codable, CaseIterable {
        case iPhone, iPad, mac, appleTV, appleWatch
    }
}

このファイルの冒頭コメントは飾りではなく機能です。// Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. 同一のソースファイルを3つのXcodeターゲットが参照しているだけで、シンボリックリンクでもSwift packageへの埋め込みでもありません。Appleのビルドシステムは、1つのファイルを3つのバイナリへ何食わぬ顔でコンパイルしてくれます。

SessionStore.swift は永続化層です。NSUbiquitousKeyValueStore(AppleのiCloud Key-Value Store)を薄く包み、MeditationSession の配列を読み書きします。この選択には意味があります。KVストア同期なら、CloudKitコンテナを用意しなくてもデバイス間でセッション履歴を共有できます。代償は、ストア全体で合計1 MBという上限です。12 1件あたり数百バイト程度の瞑想セッションのリストであれば、この上限は十分すぎます。SessionHistoryView.swift はそのセッション群を描画するSwiftUIのリストです。どちらも iPhone、iPad、Mac、Watch、TV の各ターゲットからまったく同じように使われています。

この3ファイルに共通するのは、相互作用ではなく状態を記述しているという点です。MeditationSession はどのデバイスでも同じ概念であり、過去のセッション一覧はどのデバイスでも同じように読めます。どちらも操作面にも、ウィンドウマネージャにも、オーディオのルーティング判断にも、フォーカスエンジンにも、Digital Crownにも関わりません。ファイルが「自分はどのプラットフォームで動いているのか」を知る必要が生じた瞬間、そのファイルは共有できなくなるのです。

残りを共有しなかった理由

TimerManager を例に取りましょう。iOS/iPadOS/macOS版は Timer.publish(every: 1, ...) を使い、通知を UserNotifications 経由で流します。tvOS版(TVTimerManager)は、ユーザーがSiri Remoteで一時停止したあとスクリーンセーバが起動するケースを扱います。watchOS版(WatchTimerManager)は WKExtendedRuntimeSession に処理を委譲し(WatchSessionManager 経由)、画面が暗くなってもOSがアプリを応答可能な状態に保つようにしたうえで、入力をタッチではなくDigital Crownから受け取ります。3つのプラットフォーム、3つの根本的に異なるタイマー挙動です。

これらを class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... } として統一することはできます。結果として得られるのは、3つのモードを抱え、それぞれ40行の #if 付きコードを持ち、iOS側に触れるとwatchOS側が壊れかねないクラスです。保守の悪夢というほかありません。

ファイル名を3つに分けた3つのクラスは、ディスク上のコード量は増えますが、頭の中のコード量は減ります。読める重複は、読めない抽象に勝ります。

同じ理屈が次にも当てはまります。

  • ContentViewTVContentViewWatchContentView:ナビゲーションモデルが違います(iPhoneはプッシュ型、TVはフォーカス型、Watchはリスト型)。
  • AudioManagerTVAudioManagerWatchAudioManager:オーディオセッションのカテゴリが異なり、watchOSはバックグラウンド再生の規則が厳しく、tvOSはAirPlayへのルーティングが別物です。
  • VideoBackgroundView はメインターゲット内に #if os(iOS) 分岐を8個持ち(うち1つは #elseif os(macOS) を伴います)、動画アセットの違い(fire_phone.mp4fire_mac.mp4)、レイヤー型の違い、アスペクト比の違いをカバーしています。4

ひとつ補足しておくと、メインの Return/ ターゲットは iOS、iPadOS、macOS をまとめて扱っています。この3つは、共有できない部分より共有できる部分のほうが多いのです。SwiftUIの NavigationStack は3つとも動きますし、.glassEffect() も3つとも動きます。ウィンドウ管理の差異は実在しますが、ひとつのターゲット内で御せる範囲です。私がターゲットを分ける線を引いたのは、tvOSとwatchOSでした。

tvOSの事例:フォーカスエンジンがターゲット分割を強いた

Apple TVのナビゲーションはフォーカスエンジンを軸に組み立てられています。5 ユーザーが操作しうるUI要素はすべて自らをフォーカス可能だと宣言し、Siri Remoteの方向操作でフォーカスが要素間を移動し、選択ボタンでフォーカス中の要素が起動します。tvOSのSwiftUIはこれを .focusable().focusEffect、そしてAppleの純正アプリが使う視差チルト効果のために @Environment(\.isFocused) に反応するカスタム ButtonStyle として公開しています。TVFocusModifier.swift の実際のコードがこちらです。6

struct TVCapsuleButtonStyle: ButtonStyle {
    var accentColor: Color = .white
    @Environment(\.isFocused) private var isFocused

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .colorMultiply(isFocused ? focusedTextColor : accentColor)
            .background(
                Capsule().fill(isFocused
                    ? AnyShapeStyle(accentColor)
                    : AnyShapeStyle(.ultraThinMaterial))
            )
            .clipShape(Capsule())
            .scaleEffect(isFocused ? 1.1 : 1.0)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .shadow(color: .black.opacity(isFocused ? 0.3 : 0.1),
                    radius: isFocused ? 20 : 5, y: isFocused ? 10 : 2)
            .animation(.easeInOut(duration: 0.2), value: isFocused)
    }
}

同じファイルには、正方形・円形のコントロール向けに TVCircleButtonStyle も定義しています。どちらのスタイルも、フォーカス時に色と透過を反転させます。非フォーカス時は .ultraThinMaterial の上に乗り、フォーカス時はアクセントカラーで塗りつぶして、拡大と影を加えます。このアプリにおいて、このパターンは構造的にtvOS固有です。@Environment(\.isFocused) 自体は iOS、iPadOS、macOS、watchOS、tvOS のいずれでも利用できますが、13 フォーカス駆動のナビゲーションが主たる操作モデルになるのはtvOSだけです。Siri Remoteはポインタイベントもタッチイベントも生成しないからです。iPhoneやiPadでは同等のコントロールはタップでヒットテストされ、Macではホバーかクリックになります。TVFocusModifier.swift のボタンスタイルは、フォーカスこそがユーザーの主たるアフォーダンスだという前提に立ち、視覚的な応答全体をその周りに設計しています。iOSのタッチ、Macのホバー、tvOSのフォーカス駆動ナビゲーションを1か所で捌く ContentView を書く上手いやり方は存在しません。ビュー構造そのものが違うのです。tvOSの ContentView はフォーカス可能な行のグラフであり、iOSの ContentView はタップして動かすスタックです。

時間選択ピッカーも同様です。iPhoneでは下からせり上がってきてタップを受け付けます。Apple TVでは、リモコンで辿っていく横一列のフォーカス可能なセルになります。TVDurationPicker.swift が独立したファイルなのは、このセルベースのフォーカス設計がiPhoneには対応物を持たないからです。これらを1ファイルに押し込めば、無関係な2つのUIを #if os(tvOS) で貼り合わせただけのものになります。

watchOSの事例:拡張実行セッション、HealthKit、そして狭い画面

watchOSには、他のプラットフォームにはない構造的な制約が2つ加わります。

  1. WKExtendedRuntimeSession ── 画面が暗転している間もアプリを応答可能に保つための仕組みです。8 これがないと、watchOSは1秒刻みのティックごとにアプリを容赦なくサスペンドし、タイマーがずれていきます。Returnは watchOS ターゲットの Info.plistWKBackgroundModes: mindfulness を宣言し、OSに用途を認識させて実行時間の予算を確保しています。実行セッション自体は、既定の WKExtendedRuntimeSession() イニシャライザで生成しています。
  2. WatchConnectivity ではなく NSUbiquitousKeyValueStore によるiCloud同期。7 Returnのセッション履歴の同期は、iPhone、iPad、Mac の各ターゲットが使うのと同じキーバリューストアに乗っています。おかげで、Watchで記録した瞑想が、Watch↔iPhone間の直接メッセージングなしにiPhoneの履歴画面へ現れます。ライブ状態の同期にはWatchConnectivityという選択肢が将来ありえますが、Returnはより単純なモデルを採りました。各デバイスが同じiCloud KVストアに書き、次にどのデバイスで読んでもその和集合が見える、という形です。

WatchTimerManager.swift はWatch側のタイマーで、拡張実行まわりの処理は WatchSessionManager に委譲しています。これは ReturnWatchApp.swiftfinal class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate として定義されています。iOSの TimerManager に対応物がないのは、iOSアプリが明示的な実行セッションなしにフォアグラウンドで応答し続けられるからです。Watch側のロジックを #if os(watchOS) でiOSの TimerManager に押し込めば、iOS側のコードパスは一生使わない WatchKit のシンボルをimportすることになり、加えてwatchOS側のコードパスはiOS側には不要な初期化経路を必要とします。

WatchHealthKitManager.swift は、メインの HealthKitManager を小さくした変種です。マインドフルネスの記録の仕方は同じですが、認可プロンプトのUXが違います(Watchでは HealthKitPermissionSheet を表示できません)。Watch側のクラスは、メイン側のおよそ半分の規模です。

メインのiOS/iPadOS/macOSターゲットの内部で起きること

メインターゲットの内部ですら、共有は自動的には成立しません。ContentView.swift には #if os(macOS) または #if !os(macOS) のブロックが10個、LiveActivityManager.swift には8個、VideoBackgroundView.swift には8個、AudioManager.swift には6個あります。Live ActivitiesはiPhone専用の機能なので、LiveActivityManager は丸ごと #if os(iOS) で包まれています。時間選択ピッカーもiPhoneとiPad/Macでレイアウトが異なるため、ContentView には並行するレイアウト分岐が置かれています。

うまくいっているパターンはこうです。プラットフォーム間の小さな差分(キーボード挙動の違い、パディングの違い、存在しないAPI)には #if os(...) を、大きな構造的差分(フォーカスとタッチ、ワークアウトセッションとタイマー)には別ターゲットを。 私が結局採用した閾値は「分岐が約10行を超えるかどうか」です。それ未満なら条件付きコンパイルで問題ありません。それを超えるなら、そのファイルは同時に2つの仕事をしており、2つ目の仕事は別のターゲットに属しています。

5プラットフォームすべてに出すべきでないとき

正直な見立てを書きます。

情報密度の高いアプリなら、Apple Watchは見送りましょう。 46mmの画面に、30項目のリストと時間選択ピッカーと設定ページを置く余地はありません。ReturnがwatchOSで成立しているのは、中核の操作がボタン1つ(タイマーの開始・停止)だからです。生産性アプリ、金融アプリ、メディア中心のアプリでは、そうはいきません。

インタラクティブなアプリなら、Apple TVは見送りましょう。 TVはアンビエントな体験(部屋の向こうの画面で走っているタイマー、音楽再生)のための場所です。ユーザーからの頻繁な入力を要するものは、プラットフォームと喧嘩することになります。ReturnがtvOSにいるのは、「20分のタイマーをセットして画面の炎を眺める」がまさに正しいアンビエント用途だからです。メモアプリなら悲惨でしょう。

スマートフォン前提のインターフェースなら、Macは見送りましょう。 SwiftUIはMacでも動きますが、NavigationStack のプッシュ型モデルは、本物のMacサイドバーと比べるとおもちゃのように見えます。Mac上で作り込み不足に感じられそうなら、Catalyst(iPadアプリを変換します)で出すか、Macネイティブなインターフェースを作れるようになるまでMacは見送るべきです。

サイズクラス対応をしていないなら、iPadは見送りましょう。 iPhoneアプリをiPadいっぱいに引き伸ばしたものは、安っぽく見えます。iPadには最低限サイドバー付きの NavigationSplitView が要りますし、理想を言えば本物の2ペインレイアウトが要ります。ReturnはiPadでスプリットビュー、iPhoneでスタックを使っています。コードは同じターゲット内にありますが、UIは本質的に別物です。

私が引いた基準はこうです。アプリの中核の操作が、そのプラットフォームの入力モデルに対応づくなら、そのプラットフォームに出します。瞑想タイマーはApple Watchに出します(1タップで開始)。瞑想タイマーはApple TVにも出します(セットしたら放っておける)。カンバンボードは、どちらにも出しません。

労せず移動できるもの

Returnで5プラットフォームすべてを実際にまたげたのは、次の3つです。

  1. データモデルMeditationSession)。この構造体はどのプラットフォームでも同一で、NSUbiquitousKeyValueStore で同期され、どのプラットフォームも他のどのプラットフォームが書いたものを読めます。
  2. セッション履歴ビューSessionHistoryView)。過去のセッションを並べた List は、iPhone、iPad、Mac、Apple Watch、Apple TV で同じように描画されます。SwiftUIの List は、5つのフォームファクタすべてをきれいに横断できる数少ないプリミティブのひとつです。
  3. 永続化ラッパーSessionStore)。読み書きはプラットフォームに依存せず、下層のストレージ(NSUbiquitousKeyValueStore)はどこでも同じAPIです。

概念にして3つ。状態、リスト描画、永続化です。ハードウェア固有の入力モデルに関わらない、状態を持ち提示するだけのものは共有できます。 入力、フォーカス、オーディオのルーティング、画面サイズ、バックグラウンド実行に触れるものは共有できません。

このパターンは、iOSエージェント開発ガイドでも別の言葉で同じことを論じたところに現れています。iOSアプリのうちエージェントが書ける部分は、人間が書く部分とコードの大半を共有します。人間の判断を要する部分(署名、視覚的な仕上げ、パフォーマンス)は、まさにプラットフォーム間でも共有しづらい部分です。9 2つの境界線は重なります。どちらも、ドメイン知識が効き始める場所はどこか、という話だからです。

マルチプラットフォームの代償

投資対効果(ROI)は非対称です。iPhoneアプリにiPadを足すのは、コード量にしておそらく2割増(サイズクラス分岐、一部でのスプリットビュー)。同じターゲットにMacを足すと、さらに15〜20%増(#if os(macOS) 分岐、メニューバー、ウィンドウ管理)。小規模なアプリなら、主要なターゲットが1つ増えるごとにおよそ10ファイル増えます。

高くつくのはApple WatchとApple TVです。ReturnにwatchOSを足すには、専用のオーディオ・タイマー・HealthKitの各マネージャを含め、独立したターゲットに11の新規ファイルが必要でした。tvOSを足すには、フォーカス管理とカスタムの時間選択ピッカーを含め、また別の独立ターゲットに10の新規ファイルが必要でした。この2つで、ユーザーから見た機能は同じアプリのまま、Swiftの表面積はほぼ倍になりました。

5つすべてに出すという判断は、「マルチプラットフォームであること自体が目的」ではありませんでした。個別の判断の積み重ねです。瞑想タイマーは本当に手首にあるべきだからApple Watch。長いセッションには部屋のアンビエントな画面が合うからApple TV。会議の合間にデスクで瞑想するユーザーがいるからMac。どのプラットフォームも、現実の用途を持つことで自分のターゲットを勝ち取ったのです。

ある機能がターゲットを勝ち取れないなら、安く済む手はそのプラットフォームを見送り、アプリが卓越しているプラットフォームに賭け金を積むことです。

これがあなたのアプリにとって意味すること

持ち帰りは3つです。

  1. 主要なプラットフォーム群ごとに1ターゲット、を既定にする。 iOS + iPadOS + macOS を1ターゲットにまとめられるのは、中核の操作(タッチ+カーソル)が似ているからです。tvOSは別ターゲット。watchOSも別ターゲット。ターゲットを分けるたびにおよそ10ファイル増えますが、#if 分岐が際限なく膨らむ神クラスを1つ抱えずに済みます。
  2. 相互作用ではなく、状態を積極的に共有する。 Codableなモデル構造体、永続化ラッパー、List の描画は、ほぼタダで移動します。タイマーマネージャ、オーディオマネージャ、コンテンツビューは移動しません。
  3. プラットフォームを勝ち取らせる。 出せるからwatchOSに出す、はやめましょう。アプリの中核の操作がそのプラットフォームの入力モデルに対応づくときに出し、それ以外は見送るのです。

このパターンは、同じ系統のアプリについて私が書いてきた他の3つの面と並び立ちます。Apple Intelligence向けの型付き App Intents、複数のLLMをまたぐエージェントのための MCP サーバー、そしてデバイスの前にいる人間のための Liquid Glass。同じスタックの最も外側の層がプラットフォームです。つまり、そのアプリがそもそもどの画面で動くのか。AIの面を選ぶときと同じだけの意図をもって、そこを選んでください。

FAQ

共有コードをSwift packageにしないのはなぜですか

検討はしました。3ファイルのために、Swift packageは省ける手間以上の煩雑さを持ち込みます。Xcode 26のビルドシステムは、Target Membershipのチェックを入れるだけで1つのソースファイルを複数ターゲットへ何食わぬ顔でコンパイルしてくれます。packageにすると、別途の Package.swift、別途のテストターゲット、そしてリファクタリングのたびに辿らねばならない間接層が増えます。小さな共有コアであれば、単純なほうが勝ちます。10

SwiftDataはwatchOSやtvOSで動きますか

SwiftDataは iOS 17以降、macOS 14以降、watchOS 10以降、tvOS 17以降で利用でき、Returnが対象とするすべてのプラットフォームをカバーしています。11 ただし MeditationSession 構造体は @Model ではなく素の Codable です。Returnがセッション履歴の同期にSwiftDataコンテナではなく NSUbiquitousKeyValueStore を使っているからです。@Model 型でも考え方は同じで、モデルのファイルは共有し、必要ならば永続化コンテナだけをプラットフォームごとに変えます。

Mac Catalystとネイティブな Mac ターゲット、どちらを使うべきですか

Catalystが正しい道具になるのは、iPadアプリの出来がよく、Catalystで作り直したMac版がネイティブに見える場合です。Returnのメインターゲットは(Catalystではなく)真のマルチプラットフォームターゲットで、iOS、iPadOS、macOS 向けにSwiftUIで1つのバイナリとしてビルドしています。Mac側のインターフェースは #if os(macOS) を使ってiPadとは別の描き方をします。シートの代わりにサイドバー、ボタンへのキーボードショートカット割り当て、といった具合です。Catalystのほうが単純にはなったでしょうが、Mac側のインターフェースは「Mac上のiPadアプリ」に見えたはずです。それこそCatalystの失敗例として最も知られているものです。

小さなアプリでApple TVに出す価値はありますか

おそらくありません。Apple TVのアプリは用途が非常に限られます(アンビエント、メディア、カジュアルゲーム)。そのいずれにも当てはまらないなら、アプリあたり10ファイルという投資を正当化できるほどプラットフォームの利用者層は多くありません。ReturnがあえてtvOSを対象にしているのは、部屋の向こうの画面での長い瞑想セッションが、このプラットフォームに合う数少ない生産性寄りの用途のひとつだからです。

5つすべてに出すには、どれくらいの労力がかかりますか

正確な数字は出しづらく、アプリによります。Returnは後からプラットフォームを追加したのではなく、初日からマルチプラットフォームで出荷しました。後から足していくより、そのほうが速く進みます。おおまかな目安としては、iPhoneのみの構築にiPad対応とMac対応を足すと、iPhoneのみのおよそ1.5倍。Apple Watchの追加でさらに0.5倍。Apple TVの追加でさらに0.5倍。したがって5プラットフォーム同時の初回リリースは、iPhoneのみと比べておよそ2.5倍の労力になります。ただしこれはエージェント支援での構築であり、重複コードの大半は手打ちではなく Claude Code による一括編集だった、という但し書きが付きます。

参考文献


  1. 筆者の Return。2026年4月21日にApp Storeで公開した瞑想タイマーアプリ。ネイティブ対象は iOS 26+、iPadOS 26+、macOS 26+、watchOS 26+、tvOS 26+。全面的にSwiftUI。デバイス間のセッション履歴には NSUbiquitousKeyValueStore を使用。 

  2. Apple Developer, “Configuring a Multi-Platform App”、およびWWDC 2024のセッション “SwiftUI essentials”。Appleの既定の指針は、環境値による適応を用いた単一ターゲットに寄っています。本記事が採る複数ターゲットの道は、そこからの意図的な逸脱です。 

  3. Return/Return/Shared/MeditationSession.swiftSessionStore.swiftSessionHistoryView.swift の実コード。MeditationSession.swift の冒頭コメントには次のようにあります。“Add this file to: Return, ReturnTV, ReturnWatch Watch App targets.” 

  4. Return/Return/VideoBackgroundView.swift#if os(iOS) 分岐8個に加え #elseif os(macOS) 分岐1個)、Return/Return/ContentView.swift#if os 分岐10個)、Return/Return/AudioManager.swift#if os 分岐6個)、Return/Return/LiveActivityManager.swift#if os 分岐8個、ファイル自体がiOS専用)の実コード。分岐数は grep -Ec '^\s*#if os\\(' <file> の実行結果によります。 

  5. Apple Developer, “Focus interactions” Human Interface Guidelines。tvOSのフォーカスエンジンは、iOSのタッチやMacのポインタとは根本的に異なるナビゲーションモデルです。 

  6. Return/ReturnTV/TVFocusModifier.swift の実コード。@Environment(\.isFocused) を包んでフォーカス時に色と透過を反転させ、拡大と影を適用する2つの ButtonStyle 型(TVCapsuleButtonStyleTVCircleButtonStyle)を定義しています。 

  7. Apple Developer, “WatchConnectivity”。ペアリングされたiPhoneとWatch間の通信のためのフレームワーク。Returnはセッション同期にこれを使わず、iCloudキーバリューストアに依拠しています。 

  8. Apple Developer, “WKExtendedRuntimeSession”、および Info.plist キー “WKBackgroundModes”mindfulness の値は「静かな瞑想のための拡張実行セッションを有効にする」と文書化されており、瞑想タイマーにはうってつけです。Returnは既定の WKExtendedRuntimeSession() を生成し、watchOSターゲットの Info.plistWKBackgroundModes: mindfulness を宣言しています。実コード:Return/ReturnWatch Watch App/ReturnWatchApp.swiftWatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate を定義し、WatchTimerManager.swift が拡張実行まわりの処理をそこへ委譲しています。 

  9. 筆者の分析 AIエージェントでiOSアプリを作る。8本の実運用アプリを通じた、エージェント支援によるiOS開発の実践者向けガイドです。 

  10. Apple Developer, “Configuring a Multi-Platform App”。Target Membershipを使えば、Swift packageなしに1つのソースファイルを複数ターゲットへコンパイルできます。小さな共有コアには最適な道具です。 

  11. Apple Developer, “SwiftData” のプラットフォーム対応状況。iOS 17+、iPadOS 17+、macOS 14+、watchOS 10+、tvOS 17+、visionOS 1+ で利用可能であり、Appleの5つのプラットフォームファミリーすべてをカバーします。 

  12. Apple Developer, “NSUbiquitousKeyValueStore”。ユーザーのデバイス間で少量の状態を同期するためのAppleのiCloud Key-Value Store。Appleが公表している制限では、ストア全体のサイズは全キー合計で1 MBが上限です。実コード:Return/Return/Shared/SessionStore.swift。 

  13. Apple Developer, EnvironmentValues.isFocused。iOS 14+、iPadOS 14+、macOS 11+、tvOS 14+、watchOS 7+ で利用可能。APIそのものはクロスプラットフォームであり、違いはフォーカスがユーザーの主たるナビゲーション手段であるかどうかにあります。 

関連記事

Apple プラットフォームマトリクス:どのターゲットにどのアプリがふさわしいか

iOS、iPad、Mac、Watch、Vision、TV。6つのプラットフォーム、6つの責務。Apple ターゲットの選定は、エンジニアリング判断である前にプロダクト判断です。

20 分で読める

iOS 26のHealthKit + SwiftUI:認可、サンプルタイプ、そして2つの実アプリから得たクロスプラットフォームパターン

Water(水分摂取トラッキング、HKQuantitySample)とReturn(マインドフルセッション、HKCategorySample)から得た本番運用パターン。許可UX、async ラッパー、watchOSバリアント、そして避けるべ…

15 分で読める

Codex CLIのインストールと更新:Mac、Linux、Windows

OpenAI Codex CLIをインストール、更新、バージョン固定、アンインストールするすべての方法。インストールスクリプト、npm、Homebrew、wingetを、macOS、Linux、WSL、Windowsで解説します。

13 分で読める