Return...
一款橫跨五種螢幕的禪意冥想與專注計時器:iPhone、iPad、Apple Watch、Apple TV 與 Mac。
2026 年 4 月 21 日上線。一套程式碼。二十七種語言,包含阿拉伯文與希伯來文。四種主題、三種鐘聲、零分析追蹤。以下是它的誕生過程:技術選擇、設計權衡,以及從數百滴 AI 生成水珠中細細挑出一滴的漫長靜默歷程。
一套程式碼,五種螢幕。
Return 是我第一款從單一 Xcode 專案跨越所有 Apple 螢幕類別的 App:iPhone、iPad、Apple Watch、Apple TV 與 Mac。五十七個 Swift 檔案,約 12,700 行程式碼,零外部相依套件。純 SwiftUI、AVFoundation、HealthKit、ActivityKit 與 WidgetKit。
天真的做法是寫一個通用的 TimerManager,以 #if 分支處理每個平台差異。我沒有這麼做。Return 提供三個計時器類別(iOS 與 macOS 上的 TimerManager、tvOS 上的 TVTimerManager、watchOS 上的 WatchTimerManager),它們共享狀態語意,但尊重各平台真正擅長的事情。Live Activities 僅限 iOS。HealthKit 僅限有此 API 的平台。延伸執行時段(extended runtime sessions)僅限 Watch。每個管理器都比單一的多型類別更精簡、更誠實。
在該共享的地方共享。
單一的 Shared/ 資料夾存放所有 target 都必須共識的部分:MeditationSession 資料模型、SessionStore iCloud 封裝器,以及 SessionHistoryView。設定透過 App Group(group.com.941apps.Return)在 Watch 與手機之間同步。其餘部分則刻意保持平台專屬。
最清楚的例子是那行判斷某次冥想時段是否已寫入 HealthKit 的程式碼。iPhone 直接寫入,因此時段結束的當下「已同步」即為 true。Mac 與 TV 根本無法寫入 HealthKit,所以「已同步」為 false,直到稍後 iPhone 接手處理待同步時段。意圖相同,布林值相反,以一段 #if 表達:
/// Save session to SessionStore for cross-device sync and HealthKit syncing private func saveSessionToStore(startTime: Date, endTime: Date) { // On iOS: if healthKitEnabled, we save directly to HealthKit, so mark as synced // On Mac: if healthKitEnabled, we want to sync to iPhone, so mark as NOT synced #if os(iOS) let alreadySynced = settings.healthKitEnabled #else let alreadySynced = !settings.healthKitEnabled #endif let session = MeditationSession( startDate: startTime, endDate: endTime, sourceDevice: .current, syncedToHealthKit: alreadySynced ) SessionStore.shared.addSession(session) }
我不斷回到這個模式:在仍讓意圖清晰可讀的前提下,寫最少的程式碼。當同一個布林值在不同平台代表不同意義時,就把它寫成不同的布林值。#if 本身就成為文件的一部分。
二十七種語言,並支援由右至左。
Return 是我第一款以所有我在乎的語言上架的 Apple App。二十七個語系都經過完整審閱,包含阿拉伯文與希伯來文。一切都存放在同一個 Localizable.xcstrings 檔案裡,聽起來壯舉,其實沒那麼了不起。只要你願意停止手動拼字串,Xcode 會代勞大部分工作。





只要別跟 RTL 對抗,它就是免費的勝利。
SwiftUI 把 .leading 與 .trailing 視為語意方向,而非像 .left 與 .right 那樣的固定方向。以語意方向排版一次,同一個畫面在阿拉伯文、希伯來文、波斯文或烏爾都文中會自動鏡像,不需專屬程式碼路徑。設定標籤翻轉、返回箭號反向、開關位置對調。主題圖示(水滴、火焰、葉片)保持不變。我沒有為這個行為寫任何一行 RTL 程式碼。
上架時我抓到一個例外:SwiftUI 也會對 Text 檢視套用排版方向,這意味著阿拉伯文與希伯來文首批螢幕截圖中,計時器顯示為「00:02」而非「20:00」——拉丁數字被由右至左排版了。在每一個承載時間或數字內容的 Text 檢視上加一個 .environment(\.layoutDirection, .leftToRight) 修飾器即可修復。上方螢幕截圖來自已內建該修飾器的發行版本。
整組螢幕截圖是由 fastlane 以不同的 -AppleLanguages 參數執行相同 UI 測試所產生。App 本身的 effectiveLocale 模式會讀取該旗標、重建檢視階層,並擷取結果。一個輔助工具、二十七個語系、四類裝置,全部在一夜之間完成。
/// The locale to use for the app - either user-selected or system default /// In snapshot mode, always use system language (set by -AppleLanguages) /// to allow screenshot generation for different locales private var effectiveLocale: Locale { if isSnapshotMode || appLanguage.isEmpty { if let preferredLanguage = Locale.preferredLanguages.first { return Locale(identifier: preferredLanguage) } return .current } return Locale(identifier: appLanguage) } var body: some Scene { WindowGroup { WatchContentView() .preferredColorScheme(.dark) .environment(\.locale, effectiveLocale) .id(appLanguage) // Force rebuild when locale changes } }
.id(appLanguage) 是真正物有所值的細節。沒有它,SwiftUI 會快取舊的檢視階層,執行時切換語言字串不會更新。加上它後,整棵樹會捨棄並重建,所有元件自動重新讀取在地化字串。一行程式碼,消除了一整類 bug。
正念分鐘,終於可以隨心所欲。
Apple Watch 原生的「正念」App 把內建的「靜心」與「呼吸」時段限制在五分鐘以內。HealthKit API 本身並沒有這種上限。只要 HKCategorySample 的結束時間晚於開始時間,它就會樂意接受。限制存在於 UI,不在系統。Return 在每部裝置上都提供 5 到 60 分鐘的選擇器,並如實寫入你真正靜坐的時間。
/// Save a mindful session with the given start and end time func saveMindfulSession(start: Date, end: Date) async -> Bool { guard isAvailable else { return false } // Don't save if end is before or equal to start guard end > start else { return false } let sample = HKCategorySample( type: mindfulType, value: HKCategoryValue.notApplicable.rawValue, start: start, end: end ) ... }
唯一的驗證是 end > start。這也是 HealthKit 自己唯一會驗證的條件。Apple 的 API 一直都願意記錄一次四十五分鐘的冥想。只是請求的那顆按鈕從來沒有出現過。
跨裝置運作,即便三部裝置完全沒有 HealthKit。
Mac 與 Apple TV 根本沒有 HealthKit。直覺的反應是「那就別在那裡記錄時段了」。比較不直覺但正確的反應是:照樣記錄,寫到 iCloud Key-Value Store,下次手機醒來時再由它接手處理。Return 的 SessionStore 是共享儲存,MeditationSession.syncedToHealthKit 是待處理旗標,HealthKitManager.syncPendingSessions() 會在 iOS App 每次回到前景時執行。
iCloud Key-Value Store
這正是我認為 Apple 自己該做的:一個真正跨平台的正念分鐘寫入機制,不需要手機醒著你才能在 Mac 上冥想。在他們動手之前,Return 做了。
那些水是從哪裡來的。
四種主題。四段環境循環。三種鐘聲。全部由 AI 生成,大多數被扔掉。影片是 Midjourney,音訊是 ElevenLabs,真正重要的工作不是下 prompt,而是編輯。在兩百滴水珠的網格中盯著看,挑出一滴能乾淨循環、看不出接縫的。聽著四十個版本的寺廟鐘聲,直到其中一個擁有正確的起音與正確的衰減,且聽起來不像手機通知音。




每一格都是一次生成。愛心是通過第一輪篩選的。播放三角形是我帶去做影片的。四種主題上線。其餘全部留在網格裡,這正是整個流程的重點:比例才是關鍵。
鐘聲在音訊上走的是同一條弧線。下 prompt、聆聽、修正、再下 prompt。我留下三個:Singing Bowl、Temple Bell、Soft Chime。每個都反覆迭代,直到不再聽起來合成感十足為止。
我不會假裝能數清總生成次數。每個主題數百組,這是老實話。紀律不在 prompt 本身,而在於捨棄所有「還不錯」的,只留下能夠在計時器背後安靜地陪你二十分鐘、卻不曾成為你注意到的那個東西的那幾組。
為什麼是計時器,而非老師。
這段是私人的。我做 Return 是因為我已經有冥想修行,卻找不到一款肯退到一旁的計時器。我所靜坐的是日本禪的武道脈流:Takuan、Yagyu、Musashi、Dogen、Hakuin。不是大型 App 所販售的治療式正念。意圖不同,質地也不同。
一週之中輪替的內容:
- Susokukan(數息)。隨呼吸從一數到十,數丟就回到一。基礎。先建立專注,joriki。
- Shikantaza(只管打坐)。無對象。不數息、不參話頭、不觀想。不執取的心。Dogen 禪修的核心形式,也是最貼近我真正想要的狀態的正式對應。
- Koan。主要是 Joshu 的 Mu。一個無法以思考解決的問題,持續參究直到思考放棄為止。
- Maranasati(念死)。以 Hagakure 的框架。審慎使用。面對存亡會收緊心;這個則直接切穿它。
- Isshin(一心)。Takuan 與 Yagyu 的領域:放鬆卻堅定,安住卻能動。坐墊與接下來一切之間的橋樑。
- 整合日。感恩、慈悲、傳承。Jihi。Katsujinken:活人劍,而非殺人劍。通常是週六。
- Sakki(察覺敵意)。每次時段後附加五分鐘的開放式聆聽。把 shikantaza 帶離坐墊,在日常環境中實測。
輪替並不僵硬。需要穩定時數息。需要突破時參話頭。需要在開放中安歇時就 shikantaza。需要釐清什麼才重要時就念死。多樣性本身就屬於這套訓練。
Return 是一個計時器,因為我不需要手機裡的老師。我需要一個幫我守時鐘的東西,以我尊敬的鐘聲標記開始與結束,在中間完全退到一旁。如果你已經有修行,你大概也想要這樣。如果你是全新的初學者,請找一位真人老師,進到一間真正的禪房。然後再回來。
Return 裡沒有什麼。
Return 不是 Calm。不是 Headspace。沒有英國腔旁白把你帶進身體掃描。沒有卡通角色慶祝你的連續天數。沒有訂閱方案來解鎖新的引導課程。Return 是一個計時器。想法是:如果你已經有修行,App 裡就不需要老師。你需要的是一個為你守住時間、然後退到一旁的工具。
- 沒有引導旁白或配音
- 沒有連續天數、分數或遊戲化機制
- 沒有訂閱或 App 內購
- 永遠沒有廣告
- 沒有分析追蹤;App 什麼都不記錄
- 沒有社群登入或分享
- 沒有煩人提示畫面,沒有冷啟動彈窗
- IAP 流程裡沒有暗模式,因為根本沒有 IAP 流程
Return 裡真正有的,都刻意保持精簡:四種重複模式(僅一次、直到停止、直到指定時間、重複 N 次)、每個循環之間兩秒的呼吸停頓、每次轉換時響一到三聲鐘、三種鐘聲可選、四種主題、HealthKit 授權選項,以及語言選擇器。這就是整個產品。
這種嚴格的代價會顯現在設定模型上。每一項面向使用者的偏好都由屬性本身夾取到有效範圍內,而不是靠 UI 驗證。稍不小心,UI 驗證本身就會變成另一種暗模式。bellRepeatCount 的 getter 除了 1、2、3 之外不可能回傳其他值。往底層的 @AppStorage 寫入 0 或 47,會默默被夾回到允許範圍內。
@ObservationIgnored @AppStorage("bellRepeatCount") private var _bellRepeatCount = 1 /// Validated bell repeat count (1-3) var bellRepeatCount: Int { get { max(1, min(3, _bellRepeatCount)) } set { _bellRepeatCount = max(1, min(3, newValue)) } }
Return 售價 2.99 美元。買斷制,一次付清就是你的。沒有伺服器成本要支撐、沒有訂閱要續費、沒有分析管線在監看你的行為。產品就是產品本身。如果你想讀完整版為什麼我堅持用這種方式做 App,請讀 Minimum Worthy Product 與 The Steve Test。短版就在這一段。