← 所有文章

五個 Apple 平台,三個共用檔案:Return 如何真正做出跨平台 SwiftUI

我的冥想計時器 Return 跑在五個 Apple 平台上:iPhone、iPad、Mac、Apple Watch 與 Apple TV。1 這個程式庫共有 40 個 Swift 檔案(不含測試)。其中只有三個由五個平台共用。 其餘的則拆進各自的 Xcode target,寧可重複實作 TimerManagerAudioManagerContentView 這些概念,也不用 #if os(...) 條件編譯把它們湊在一起。

共用比例約 7.5%,而這是刻意的。

這篇文章要談的是:2026 年推出一款跨平台 SwiftUI App 實際上長什麼樣、為什麼激進的程式碼共用被高估了,以及那三個真的共用了的檔案有什麼共同點。

來自 Apple Developer 的 iOS 26 平台圖卡 來自 Apple Developer 的 iPadOS 26 平台圖卡 來自 Apple Developer 的 macOS 26 平台圖卡 來自 Apple Developer 的 watchOS 26 平台圖卡 來自 Apple Developer 的 tvOS 26 平台圖卡

Return 支援的五個平台,取自 Apple 在 developer.apple.com 上的呈現方式。每一個在 Xcode 裡都是獨立的平台 target,而不是執行期的分支。

重點摘要

  • Return:主 target(iOS + iPadOS + macOS)18 個 Swift 檔案、tvOS target 10 個、watchOS target 7 個、Widget 2 個(Live Activities),再加上 Return/Shared/ 裡真正跨平台的 3 個,總共 40 個。
  • 三個共用檔案都靠近持久化層:MeditationSessionSessionStoreSessionHistoryView。共用的是透過 iCloud 流動的狀態,不是隨平台調整的 UI。
  • tvOS 與 watchOS 是獨立的 Xcode target,而不是主 target 裡的 #if os(tvOS) 分支。操作模型差太多,塞不進同一個 ContentView。
  • 即使在 iOS/iPadOS/macOS 這個主 target 內部,#if os 區塊照樣蔓延:ContentView.swift 有 10 處、LiveActivityManager.swift 8 處、VideoBackgroundView.swift 8 處、AudioManager.swift 6 處。
  • 老實說:在五個 Apple 平台之間激進共用,是維護上的負債。一個小小的共用核心(持久化層),加上各平台各自的 UI,比一個塞滿 #if 的巨型檔案更快出貨、也更少出錯。

想看各平台的延伸討論,可以讀 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

五個平台、三個共用檔案、兩個各自獨立的平台 target 再加一個 Widget target,主 target 裡還有大量條件編譯。共用比例約 7.5%。多數「多平台 SwiftUI」教學建議的正好相反:寫一個 ContentView,靠 @Environment(\.horizontalSizeClass)#if os(...) 適應每個平台。2 這招在兩個平台(iPhone + iPad)上行得通,到了五個平台就崩了。

三個共用檔案的共同點

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. 同一份原始檔被三個 Xcode target 同時引用——不是符號連結,也不是包在 Swift package 裡。Apple 的建置系統很樂意把同一個檔案編進三個二進位檔。

SessionStore.swift 是持久化層:NSUbiquitousKeyValueStore(Apple 的 iCloud 鍵值儲存)的一層薄包裝,負責讀寫 MeditationSession 陣列。這個選擇很關鍵:鍵值儲存的同步讓 Return 不必開通 CloudKit 容器就能有跨裝置的紀錄,代價是整個儲存空間上限只有 1 MB。12 對一份平均每筆幾百位元組的冥想紀錄清單來說,這個上限綽綽有餘。SessionHistoryView.swift 則是把這些紀錄畫出來的 SwiftUI 清單。iPhone、iPad、Mac、Watch 與 TV 這幾個 target 使用它們的方式完全一樣。

這三個檔案的共同點是:它們描述的是狀態,不是互動。 MeditationSession 在任何裝置上都是同一個概念。過往紀錄清單在任何裝置上讀起來也一樣。兩者都不牽涉操作介面、視窗管理、音訊路由決策、焦點引擎或數位錶冠。一個檔案只要開始需要知道自己跑在哪個平台上,就不再適合共用了。

其餘檔案為何沒有共用

TimerManager 為例。iOS/iPadOS/macOS 版用 Timer.publish(every: 1, ...),通知走 UserNotifications。tvOS 版(TVTimerManager)要處理使用者用 Siri Remote 暫停、螢幕保護程式跟著跳出來的狀況。watchOS 版(WatchTimerManager)則把工作委派給 WKExtendedRuntimeSession(透過 WatchSessionManager),讓系統在螢幕變暗時仍維持 App 的反應能力,輸入也走數位錶冠而不是觸控。三個平台,三種截然不同的計時行為。

大可把它們統一成 class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... }。結果會是一個有三種模式的類別,每種模式四十行被 #if 圈住的程式碼,動到 iOS 那條路徑就可能弄壞 watchOS 那條。這是維護上的噩夢。

三個獨立的類別、三個檔名,磁碟上的程式碼變多了,腦子裡要裝的卻變少了。看得懂的重複,勝過看不懂的抽象。

同樣的道理也適用於:

  • ContentView 對上 TVContentViewWatchContentView:導覽模型不同(iPhone 靠推入堆疊、TV 靠焦點、Watch 靠清單)。
  • AudioManager 對上 TVAudioManagerWatchAudioManager:音訊工作階段類別不同,watchOS 對背景音訊的規則更嚴,tvOS 導向 AirPlay 的方式也不一樣。
  • VideoBackgroundView 在主 target 裡有 8 個 #if os(iOS) 分支(外加一個 #elseif os(macOS) 搭配),涵蓋不同的影片素材(fire_phone.mp4fire_mac.mp4)、不同的圖層型別,以及不同的長寬比。4

補充一點:主 Return/ target 確實把 iOS、iPadOS 與 macOS 綁在一起。這三個平台共用的程式碼比不共用的多。SwiftUI 的 NavigationStack 三邊都能用,.glassEffect() 也是。視窗管理的差異真實存在,但在同一個 target 裡還處理得來。獨立 target 那條線,我畫在 tvOS 與 watchOS 上。

tvOS 的情況:焦點引擎為何逼出獨立 target

Apple TV 的導覽是圍繞焦點引擎打造的。5 每個可互動的 UI 元素都會宣告自己可取得焦點;Siri Remote 上的方向操作讓焦點在元素之間移動;按下選取鍵就啟動當前取得焦點的元素。tvOS 上的 SwiftUI 透過 .focusable().focusEffect,以及回應 @Environment(\.isFocused) 的自訂 ButtonStyle 來呈現這一切——Apple 自家 App 那種視差傾斜效果就是這樣做出來的。以下是 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 上,取得焦點的按鈕則填入強調色,並放大尺寸、加深陰影。對這個 App 來說,這個模式在結構上就是 tvOS 專屬的。@Environment(\.isFocused) 在 iOS、iPadOS、macOS、watchOS 與 tvOS 上都可用,13 但只有在 tvOS 上,焦點驅動的導覽才是主要互動模型——Siri Remote 不會產生任何指標或觸控事件。在 iPhone 或 iPad 上,對應的控制項是靠點擊做命中測試;在 Mac 上則是滑過或點按。TVFocusModifier.swift 裡的按鈕樣式假設焦點就是使用者的主要操作線索,整個視覺回饋都繞著它設計。想寫出一個 ContentView 同時處理 iOS 的觸控、Mac 的滑鼠停留與 tvOS 的焦點導覽,沒有什麼好辦法。視圖結構本來就不一樣:tvOS 的 ContentView 是由可取得焦點的列所組成的一張圖,iOS 的 ContentView 則是點一下就動作的堆疊。

時長選擇器也一樣。在 iPhone 上,它從底部滑上來、接受點擊。在 Apple TV 上,它是一排可取得焦點的水平儲存格,使用者用遙控器在其間移動。TVDurationPicker.swift 之所以自成一個檔案,是因為這種以儲存格為基礎的焦點設計,在 iPhone 上根本沒有對應物。硬塞進同一個檔案,只會得到兩套毫不相干的 UI 被 #if os(tvOS) 黏在一起。

watchOS 的情況:延伸執行階段、HealthKit 與更小的介面

watchOS 多了兩個其他平台沒有的結構性限制:

  1. WKExtendedRuntimeSession,用來在錶面變暗時維持 App 的反應能力。8 少了它,watchOS 會在每一秒的跳動之間積極暫停 App,計時就開始飄移。Return 在 watchOS target 的 Info.plist 裡宣告 WKBackgroundModes: mindfulness,讓系統認得這個使用情境並撥給執行時間額度;延伸執行階段本身則是用預設的 WKExtendedRuntimeSession() 建構式建立的。
  2. 透過 NSUbiquitousKeyValueStore 做 iCloud 同步,而不是 WatchConnectivity。7 Return 的紀錄同步搭的是 iPhone、iPad 與 Mac target 共用的那個鍵值儲存,所以在錶上記錄的一次冥想,不需要任何錶對手機的直接訊息傳遞,就會出現在 iPhone 的歷史紀錄畫面裡。WatchConnectivity 未來或許可以拿來做即時狀態同步,但 Return 選了更簡單的模型:每台裝置都寫進同一個 iCloud 鍵值儲存,任何一台裝置下次讀取時看到的就是聯集。

WatchTimerManager.swift 是錶端的計時器;延伸執行階段的工作交給 WatchSessionManager,後者定義在 ReturnWatchApp.swift,宣告為 final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate。iOS 的 TimerManager 沒有對應物,因為 iOS App 在前景本來就能維持反應,不需要明確的執行階段。把錶端邏輯用 #if os(watchOS) 塞進 iOS 的 TimerManager,等於讓 iOS 那條路徑匯入它永遠用不到的 WatchKit 符號,而 watchOS 那條路徑又需要 iOS 路徑不需要的初始化流程。

WatchHealthKitManager.swift 是主 HealthKitManager 的縮小版。記錄正念分鐘數的方式一樣,但授權提示的 UX 不同(錶上無法顯示 HealthKitPermissionSheet)。錶端這個類別的篇幅大約是主版本的一半。

主 target(iOS/iPadOS/macOS)內部發生什麼事

即使在主 target 內部,共用也不是理所當然。ContentView.swift 有十個 #if os(macOS)#if !os(macOS) 區塊;LiveActivityManager.swift 八個;VideoBackgroundView.swift 八個;AudioManager.swift 六個。Live Activities 是 iPhone 專屬功能,所以整個 LiveActivityManager 都包在 #if os(iOS) 裡。iPhone 上的時長選擇器與 iPad、Mac 上的版面不同,於是 ContentView 裡就有了並行的版面分支。

行得通的模式是:平台差異小就用 #if os(...)(鍵盤行為不同、間距不同、少了某個 API),結構差異大就開獨立 target(焦點對觸控、體能訓練工作階段對計時器)。 我最後採用的門檻是「分支超過約 10 行」。低於這個數字,條件編譯沒問題;高於它,這個檔案就是同時在做兩件事,而第二件事該搬去另一個 target。

什麼時候不該五個平台全上

說點實話。

如果您的 App 資訊密度高,就跳過 Apple Watch。 46mm 的螢幕塞不下 30 筆清單、一個時長選擇器再加一頁設定。Return 能在 watchOS 上活下來,是因為核心互動只有一顆按鈕(開始/停止計時)。生產力 App、理財 App 或影音密集的 App 做不到。

如果您的 App 互動頻繁,就跳過 Apple TV。 電視適合環境式體驗(房間另一頭螢幕上跑著的計時器、播放中的音樂)。任何需要使用者頻繁輸入的東西,都是在跟平台作對。Return 之所以上 tvOS,是因為「設一個 20 分鐘的計時器,然後看著螢幕上的火」正好是最合適的環境式情境。記事 App 放上去只會很痛苦。

如果您的 App 是手機優先的介面,就跳過 Mac。 SwiftUI 在 Mac 上能跑,但 NavigationStack 的推入模型跟真正的 Mac 側邊欄一比,會顯得像玩具。要是這個 App 在 Mac 上會給人半成品的感覺,那就改出 Catalyst 版(它會轉換 iPad App),或者乾脆先跳過 Mac,等到做得出 Mac 原生 UI 再說。

如果您沒做 size class 適配,就跳過 iPad。 把 iPhone App 拉大填滿 iPad,看起來就是廉價。iPad 至少需要一個帶側邊欄的 NavigationSplitView,理想上要有真正的雙欄版面。Return 在 iPad 上用分割視圖、在 iPhone 上用堆疊。程式碼在同一個 target 裡,但 UI 確實不同。

我畫的規則是:當 App 的核心互動能對應到某個平台的輸入模型時,才在那個平台上推出。冥想計時器適合 Apple Watch(點一下就開始),也適合 Apple TV(設好就不管)。看板工具兩邊都別上。

哪些東西能毫不費力地跨平台

在 Return 裡,真正跨越五個平台共用的有三樣:

  1. 資料模型MeditationSession)。這個結構在每個平台上都一模一樣,透過 NSUbiquitousKeyValueStore 同步,任何平台都能讀到其他平台寫入的內容。
  2. 歷史紀錄畫面SessionHistoryView)。過往紀錄的 List 在 iPhone、iPad、Mac、Apple Watch 與 Apple TV 上的呈現完全相同。SwiftUI 的 List 是少數能乾淨適應這五種外型的基本元件之一。
  3. 持久化包裝層SessionStore)。讀寫與平台無關;底層儲存(NSUbiquitousKeyValueStore)在哪裡都是同一套 API。

三個概念:狀態、清單呈現、持久化。只要是關於狀態與呈現、又不牽涉特定硬體輸入模型的東西,就可以共用。 只要碰到輸入、焦點、音訊路由、螢幕尺寸或背景執行,就不行。

這個模式在《用 AI 代理開發 iOS App》指南裡也出現過,我在那裡用不同的說法講了同一件事:iOS App 中代理能寫的部分,與人類負責的部分共用了大部分程式碼;而需要人類判斷的部分(簽章、視覺打磨、效能),正好也是跨平台最難共用的部分。9 這兩條界線是對齊的,講的都是領域知識從哪裡開始變得重要。

多平台要付出什麼代價

ROI 並不對稱。在 iPhone App 上加 iPad,大概多付出 20% 的程式碼(size class 分支、部分地方的分割視圖)。在同一個 target 上再加 Mac,又多 15% 到 20%(#if os(macOS) 分支、選單列、視窗管理)。對一個小型 App 來說,每多一個主要 target 就多出約 10 個檔案。

Apple Watch 與 Apple TV 才是貴的那兩個。替 Return 加上 watchOS,需要在獨立 target 裡新增 11 個檔案,包含專屬的音訊、計時與 HealthKit 管理器。加上 tvOS,則是在另一個獨立 target 裡新增 10 個檔案,包含焦點管理與自訂的時長選擇器。兩者加起來,讓這個在使用者功能層面上完全相同的 App,Swift 的表面積幾乎翻倍。

決定五個平台全上,並不是「我們想為了多平台而多平台」。那是一連串各自獨立的判斷:上 Apple Watch,是因為冥想計時器本來就該在手腕上;上 Apple TV,是因為環境式的大螢幕適合在房間裡進行的長時間練習;上 Mac,是因為有些使用者會在會議之間於辦公桌前冥想。每個平台都是靠一個真實的使用情境,才掙到自己的 target。

如果某個功能撐不起自己的 target,比較划算的做法是跳過那個平台,把力氣加倍投在這個 App 表現出色的平台上。

這對您的 App 意味著什麼

三個重點。

  1. 預設每個主要平台群組配一個 target。 iOS + iPadOS + macOS 放在同一個 target 行得通,因為核心互動(觸控加游標)相近。tvOS 獨立一個 target,watchOS 再獨立一個。每個獨立 target 大約多花 10 個檔案,但省下一個 #if 分支無限增生的萬能類別。
  2. 要激進地共用狀態,而不是互動。 Codable 的模型結構、持久化包裝層與 List 的呈現幾乎可以免費跨平台。計時管理器、音訊管理器與內容視圖則不行。
  3. 每個平台都要掙來。 別因為做得到就上 watchOS。等到 App 的核心互動能對應平台的輸入模型時再推出,其餘的就跳過。

這個模式與我為同一系列 App 寫過的另外三個介面層彼此呼應:給 Apple Intelligence 的具型別 App Intents、給跨 LLM 代理的 MCP 伺服器,以及給裝置前那個人的 Liquid Glass。同一疊架構的最外層就是平台:這個 App 究竟會在哪些螢幕上執行。挑平台時,要跟挑 AI 介面層一樣審慎。

常見問題

共用程式碼為什麼不用 Swift package?

我考慮過。就三個檔案而言,Swift package 帶來的繁文縟節多過它省下的功夫。只要勾選 Target Membership,Apple 的 Xcode 26 建置系統就很樂意把同一份原始檔編進多個 target。改用 package,就得多一個 Package.swift、多一個測試 target,而且每次重構都得穿過一層間接。對一個小小的共用核心來說,比較簡單的答案勝出。10

SwiftData 在 watchOS 與 tvOS 上能用嗎?

SwiftData 支援 iOS 17+、macOS 14+、watchOS 10+ 與 tvOS 17+,涵蓋 Return 支援的每一個平台。11 MeditationSession 是單純的 Codable 結構,而不是 @Model,因為 Return 用 NSUbiquitousKeyValueStore 而非 SwiftData 容器來同步歷史紀錄。同樣的模式套用在 @Model 型別上也成立:模型檔案共用,持久化容器則在必要時各平台各自處理。

該用 Mac Catalyst 還是原生的 Mac target?

當 iPad App 本身夠好、好到用 Catalyst 重建出來的 Mac 版讀起來就像原生時,Catalyst 就是對的工具。Return 的主 target 是真正的多平台 target(不是 Catalyst),用 SwiftUI 為 iOS、iPadOS 與 macOS 建置成同一個二進位檔。Mac 版 UI 靠 #if os(macOS) 呈現得與 iPad 不同:用側邊欄取代 sheet、按鈕加上鍵盤快捷鍵等等。Catalyst 會比較省事,但 Mac 版 UI 就會看起來像一個放在 Mac 上的 iPad App——那正是 Catalyst 最惡名昭彰的失敗模式。

小型 App 值得上 Apple TV 嗎?

大概不值得。Apple TV App 的使用情境很特定(環境式、影音、休閒遊戲)。如果您的 App 不屬於其中一種,這個平台的受眾規模撐不起那 10 個 Swift 檔案。Return 之所以特地支援 tvOS,是因為在房間另一頭的螢幕上進行長時間冥想,是少數幾個與生產力沾邊、又剛好適合這個平台的情境之一。

五個平台全部推出要花多少工夫?

很難給出精確數字,得看 App 而定。Return 從第一天就是多平台推出,而不是後來一個一個加,這比事後補強來得快。粗略的經驗法則是:只支援 iPhone 的 MVP 再加上 iPad 與 Mac 支援,大約是純 iPhone 版的 1.5 倍。加上 Apple Watch 再多 0.5 倍,加上 Apple TV 又多 0.5 倍。所以一次推出五個平台的首發版本,大約是純 iPhone 版工作量的 2.5 倍——但要注意,這是一次由代理協助的建置,大部分重複的程式碼是由 Claude Code 批次改寫,而不是手動敲出來的。

參考資料


  1. 作者的 Return,一款於 2026年4月21日在 App Store 上架的冥想計時器 App。原生 target: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 的預設建議偏向單一 target 搭配環境驅動的適配;本文採取的多 target 路線是刻意的偏離。 

  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(8 個 #if os(iOS) 分支加上一個 #elseif os(macOS) 分支)、Return/Return/ContentView.swift(10 個 #if os 分支)、Return/Return/AudioManager.swift(6 個 #if os 分支)、Return/Return/LiveActivityManager.swift(8 個 #if os 分支,整個檔案僅限 iOS)。分支數量以 grep -Ec '^\s*#if os\\(' <file> 統計取得。 

  5. Apple Developer,“Focus interactions” Human Interface Guidelines。tvOS 的焦點引擎,與 iOS 的觸控或 Mac 的指標是根本不同的導覽模型。 

  6. Return/ReturnTV/TVFocusModifier.swift 中的上線程式碼。定義了兩個 ButtonStyle 型別(TVCapsuleButtonStyleTVCircleButtonStyle),兩者都包住 @Environment(\.isFocused),在取得焦點時反轉顏色與透明度,並套用縮放與陰影。 

  7. Apple Developer,“WatchConnectivity”。這是用於 iPhone 與已配對 Watch 之間通訊的框架;Return 並未用它來同步紀錄,而是改用 iCloud 鍵值儲存。 

  8. Apple Developer,“WKExtendedRuntimeSession”,以及 Info.plist 的 “WKBackgroundModes” 鍵。文件對 mindfulness 這個值的說明是:「Enables extended runtime sessions for silent meditation」——正好適合冥想計時器。Return 建立預設的 WKExtendedRuntimeSession(),並在 watchOS target 的 Info.plist 裡宣告 WKBackgroundModes: mindfulness。上線程式碼:Return/ReturnWatch Watch App/ReturnWatchApp.swift 定義了 WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegateWatchTimerManager.swift 把延伸執行階段的工作委派給它。 

  9. 作者在 用 AI 代理開發 iOS App 中的分析——這是一份橫跨 8 個上線 App、關於代理協助 iOS 開發的實務指南。 

  10. Apple Developer,“Configuring a Multi-Platform App”。Target membership 讓同一份原始檔不必透過 Swift package 就能編進多個 target。對小型共用核心而言,這才是對的工具。 

  11. Apple Developer,“SwiftData” 平台支援狀況。支援 iOS 17+、iPadOS 17+、macOS 14+、watchOS 10+、tvOS 17+、visionOS 1+,涵蓋全部五個 Apple 平台家族。 

  12. Apple Developer,“NSUbiquitousKeyValueStore”。Apple 的 iCloud 鍵值儲存,用於在使用者的裝置之間同步少量狀態。依 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平台矩陣:哪些目標平台值得擁有哪種App

iOS、iPad、Mac、Watch、Vision、TV。六個平台,六種義務。挑選Apple目標平台是產品決策,而非工程決策。

15 分鐘閱讀

iOS 26 上的 HealthKit + SwiftUI:授權、樣本類型,以及兩款上架 App 的跨平台模式

來自 Water(飲水追蹤、HKQuantitySample)與 Return(正念冥想、HKCategorySample)的真實生產級模式。權限體驗、async 包裝、watchOS 變體,以及務必避開的陷阱。

12 分鐘閱讀

安裝與更新 Codex CLI:Mac、Linux、Windows

安裝、更新、鎖定版本與移除 OpenAI Codex CLI 的每一種方式:安裝指令碼、npm、Homebrew、winget,涵蓋 macOS、Linux、WSL 與 Windows。

9 分鐘閱讀