個案研究

每一下,都記錄在案。

一款快速、私密的阻力訓練日誌,已在 App Store 上架,支援 iPhone、iPad 與 Apple Watch。

2026年7月12日上架。我做 Reps,是因為自己訓練時就想要這樣的東西:一本安靜到不干擾訓練的日誌、練完就能鎖上的日子,以及一個讓 Claude 不必碰資料庫就能讀取我的歷史、規劃下一次課表的連接器。36,000 行 Swift 與 185 個測試之後,它以十七種語言上架。以下是它成形的過程。

941 Reps 訓練進行中:兩組已完成,以綠色手繪刪除線劃掉
產品

點一下圓圈,鎖上這一天。

訓練日誌只有一個任務:在你忘記之前,把那一組記下來。Reps 是我為自己想要的日誌。打開就是今天,顯示我排好的課表,點一下圓圈就記錄一組。沒有動態消息,也沒有別人的數字,因為在組間休息時,這兩樣我根本不會用。

一組就是一個重量加一個次數。RPE、組間計時器和筆記留給需要的人,對其他人則完全不礙事。個人紀錄會從完成的組數自動計算:最大重量,加上以 Brzycki 公式估算的單次最大重量。

如果其他功能都得砍掉,我唯一會留下的是鎖定日。練完一天、鎖上,App 就會拒絕任何後續修改,直到你親自解鎖。解鎖是在 App 裡的明確動作;代替你行事的自動化流程完全無法編輯已鎖定的日子。回顧一個月的訓練時,鎖定的一天讀起來就和你當初記下的一模一樣。

商業模式也遵循同一條規則。日誌永久免費,你的紀錄永遠不會被鎖在付費牆後面。Reps Pro 訂閱只賣一樣東西:託管的 AI 規劃,給想用規劃器、又不想自備 API 金鑰的人。

941 Reps 鎖定後的訓練:完成的組數已劃上刪除線,當日的鎖已扣上
鎖定的一天,讀起來就和你當初記下的一模一樣。
設計

黑板上的手寫字。

紙本訓練日誌有一件事做對了:一個日期、一個動作,下面潦草寫著數字。Reps 保留了這整個物件。日期與重量以 Bradley Hand 和 Noteworthy 呈現,配色維持在近黑底上的墨色,只添一種綠;分隔線也不是材質貼圖。它們之所以歪歪扭扭,是因為一個十六段的 Shape 把每一段都從中線微微推開:

Swift · Theme.swift:143-158
struct RepsHandDrawnLine: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let segments = max(Int(rect.width / 12), 8)
        let segmentWidth = rect.width / CGFloat(segments)
        path.move(to: CGPoint(x: 0, y: rect.midY))

        for index in 1...segments {
            let x = CGFloat(index) * segmentWidth
            let y = rect.midY + (index.isMultiple(of: 2) ? -0.8 : 0.9)
            path.addLine(to: CGPoint(x: x, y: y))
        }

        return path
    }
}

試試下面這一列。它為這個頁面重建了 App 的核心互動:點一下圓圈完成一組,然後鎖上這一天,看它怎麼拒絕你。

6月12日 星期五

胸 / 背

反向划船

點點圓圈。鎖上這一天。再試一次。

手寫字到哪裡為止。

十七種上架語言中,有十種使用手寫字體。日文、韓文、中文、阿拉伯文、印地文與泰文則改用 SF Rounded,因為 Bradley Hand 沒有這些文字的字符,而假手寫風的 CJK 字型看起來就像戲服。色彩遵循同樣的紀律:品牌綠偏淺,所以淺色底上的強調文字會自動加深,維持 5.4:1 的對比度;tvOS 的焦點狀態也有自己的墨色。每個點擊目標都維持 Apple 規定的 44-point 下限,因為你是在組間、雙手帶著疲勞按這些控制項,不是坐在書桌前。

你看不到的部分來自 Kit941,941 系列 App 共用的 SwiftUI 設計系統:間距、動態與觸覺回饋的 token、經快照測試的元件,以及包在單一樣式後面的 Apple Liquid Glass。Reps 從不直接呼叫 .glassEffect()。筆記本的外貌屬於 Reps;骨架是共用的。

平台

iPhone、iPad 與 Apple Watch,十七種語言。

Reps 以同一套 SwiftUI 程式碼在 iPhone、iPad 與 Apple Watch 上執行。Watch App 刻意保持精簡:顯示當天的組數、在手腕上打勾完成,WatchConnectivity 幾秒內就把每筆變更帶回手機。在健身房裡,勾選清單勝過鍵盤。

Siri 與捷徑透過 App Intents 接入。說一句「記錄一組」,不用打開 App,那一組就記進去了,因為意圖走的是與 UI 相同、經過驗證的寫入路徑。

941 Reps 在 iPad 上:今天的胸背課表,完成的組數已劃上刪除線
iPad
941 Reps 在 Apple Watch 上:今天的組數以勾選清單呈現,槓鈴臥推 260 lb 做 6 下
Apple Watch
941 Reps 在 Mac 上:桌面視窗裡的手寫筆記本
Mac,開發中。
941 Reps 在 Apple TV 上:當天的訓練,其中一列組數處於焦點狀態
Apple TV,開發中。
  • English
  • Deutsch
  • Español
  • Français
  • Italiano
  • Nederlands
  • Português
  • Svenska
  • Bahasa Indonesia
  • Tiếng Việt
  • 日本語
  • 한국어
  • 简体中文
  • 繁體中文
  • العربية
  • हिन्दी
  • ไทย

十七種語言在上架第一天就到位,App Store 中繼資料涵蓋 22 個地區。每次建置都由驗證指令碼把關,735 個字串必須零缺漏才放行。

Mac 與 Apple TV 版本已經存在、可以執行。等它們準備好了,就會上架。

工程

只有一扇正門。

Reps 內建一個連接器:一組 CLI 和一個 MCP 伺服器,讓 AI 代理真正存取你的日誌。Claude 讀你的歷史、注意到哪裡停滯了,然後把星期五排好。從終端機執行:

reps — zsh
$ reps last
Friday, June 26 — Chest / Back  [locked]
  Barbell Bench Press   255 lb × 6, 6, 6, 5
  Weighted Pull-Up      25 lb × 5, 5, 4
  Weighted Dip          60 lb × 10, 10, 10

$ claude "I started BJJ twice a week. What should change?"
Reading your last 8 weeks via the reps connector… pulling
volume is high for a grappling schedule. Suggesting: drop one
row day, add hip hinge + neck work. Plan Friday around this?

讀與寫走不同的路。讀取開啟的是你資料的同步本機副本,唯讀,所以查詢不可能弄壞任何東西,離線也能用。寫入永遠不碰那個儲存區。每個變更都以指令送進 App;App 用與 UI 相同的規則驗證、套用、同步,然後回傳一張回條。鎖定的日子拒絕代理的編輯,就像拒絕指尖一樣。

941 Reps 規劃器面板:快速指令與白話輸入區
規劃器:用快速指令,或直接用白話提問。

連接器接受的每個指令,都會經過與點擊完全相同的驗證;遇到鎖定的日子,會在任何東西碰到儲存區之前就擲出錯誤:

Swift · ConnectorCommandHandler.swift:438-448
guard let session = try sessionForDay(date, modelContext: modelContext) else {
    throw CommandError.validationFailed("no workout exists on \(dayLabel)")
}
guard !session.isLocked else {
    throw CommandError.dayIsLocked(dayLabel)
}
guard let target = (session.sets ?? []).first(where: {
    $0.exerciseName.caseInsensitiveCompare(exerciseName) == .orderedSame
        && $0.setNumber == setNumber
}) else {
    throw CommandError.validationFailed("no set \(setNumber) of \(exerciseName)")
}

AI 規劃器也受到同樣的待遇。模型(託管或你自己的金鑰)只負責解析意圖:今天練哪一種、要加強什麼、要避開什麼。實際排課表的是手機上的一個確定性引擎:從收錄 42 個動作的動作庫裡挑選、套用符合你目標的次數配置、依你記錄的歷史遞增負荷,並在結果進入日誌之前先行檢查。AI 從不直接寫入任何一次訓練。

一路走來,有兩個能運作的架構被砍掉:先是以檔案為基礎的 Claude Desktop 擴充功能,接著是配對式的區域網路伺服器,帶 TLS 憑證綁定和自己的 RPC 匯流排。它們都替資料多開了一扇門,所以都被拿掉了。上架的 App 只有一扇。

連接器對外提供 23 個 MCP 工具,從 get_exercise_historyset_custom_program。這一年來,我一直在寫以助理為第一位讀者的軟體這個主題;Reps 就是把那個論點做出來上架。而 Get Bananas 把 MCP 伺服器放進了購物清單;Reps 則把規劃器交給代理,同時守住護欄。

隱私

資料住在哪裡。

隱私口號很廉價,所以直接給你看儲存區的設定本身。訓練資料鏡射到你的 iCloud 私人資料庫;健康數據住在第二個儲存區,從不同步,也從不進入備份:

Swift · RepsModelContainer.swift:67-78 (condensed)
// No explicit name: the synced configuration keeps the historical
// default.store path, which the Mac connector reads directly.
let modelConfiguration = ModelConfiguration(
    schema: syncedSchema,
    isStoredInMemoryOnly: false,
    cloudKitDatabase: .private("iCloud.com.941apps.Reps")
)
let healthCacheConfiguration = ModelConfiguration(
    "Reps-healthcache",
    schema: localSchema,
    isStoredInMemoryOnly: false,
    cloudKitDatabase: .none
)

你的訓練資料

住在你的 iCloud 私人資料庫,透過 Apple 在裝置之間同步。Reps 沒有帳號系統,也沒有儲存訓練資料的伺服器。我讀不到你的深蹲重量。

健康數據

從「健康」讀取的心率與卡路里存放在第二個儲存區:只在裝置上、排除於備份之外、從不同步。把「健康」的體能訓練連結到鎖定的日子時,同步的只有那個連結。測量值留在手機上。

AI 規劃

選用。啟用時,規劃請求(你的個人檔案與近期歷史)會送往你選擇的供應商:託管服務,或你自己的 API 金鑰。展開並驗證每份課表的引擎在手機上執行。不用 AI,就不會有任何資料送往任何 AI 供應商。

記錄在案。

17 上架時的語言
23 MCP 工具
42 動作庫裡的動作

Reps 在 App Store 免費上架。我為自己的訓練做了它,也是我每天打開的日誌;如果你想要同樣的東西,它就在那裡。如果你在為代理打造軟體,寫入路徑是最值得偷走的部分。(數字為上架當日統計。)