跳到主要內容

【2026 最新】Claude Code SwiftUI 教學:把 CLI 變成原生 Mac App 的 6 步驗收流程

最後更新: ·
Claude Code SwiftUI 教學:從 CLI 核心到原生 Mac App

Claude Code SwiftUI 真正值得學的,不是叫 AI 把終端機「換皮」,而是把既有 CLI 留在它最擅長的位置,再補上一個能點、能看、也能被驗收的 macOS 原生介面。核心等式很簡單:原生小工具=可驗證的 CLI 核心+穩定資料邊界+薄 SwiftUI 外殼

這個方向近期引發很多討論。Thomas Ptacek 主張 AI 已大幅降低個人 macOS GUI 的原型門檻,但他同時說 CLI 通常仍值得保留;Simon Willison 也分享兩個由 AI 協助完成、自己持續使用的 menu-bar App。不過,這些都是實作者案例,不是生產力 benchmark,更不能推成「GUI 成本歸零」或「所有 TUI 都該消失」。本篇會取兩者最可守住的交集:保留 CLI,讓 Claude Code 幫你建立可選的原生入口,最後用測試而不是截圖判定完成。如果你還在比較 coding agent,可先看 Claude Code vs Codex 的工作方式差異

本篇寫給已會在 Terminal 執行基本指令、有一個可用 CLI,但第一次做 macOS 原生 App 的讀者。你不必先精通 Swift;我們會把每一步都綁到可觀察的完成條件,讓 Claude Code 寫程式,人負責批准邊界與驗收結果。

先說結論:CLI 是核心,SwiftUI 是可選入口

讀完你會完成六件事:挑選適合包裝的 CLI、凍結 JSON contract、用 Claude Code 規劃、建立 MenuBarExtra、以 Process 安全接回 helper,最後補齊資料、錯誤、無障礙與分發驗收。這不是「從此不用終端機」,而是讓非 CLI 使用者多一條低摩擦入口。

先選對 CLI:四個條件少一個都先別包 GUI

第一次練習,請選「單機、低風險、讀取型、輸出可固定」的小工具。例如顯示同步狀態、整理本機專案摘要、查詢固定 API,或把一組診斷結果放進 menu bar。不要從刪檔、改權限、金流、客戶資料或系統管理工具開始;「read-only」也不是零風險,它仍可能把路徑、環境變數或敏感輸出寫進 log。

  • 能力邊界:GUI 只呼叫已批准的固定子命令,不接受任意 shell 字串。
  • 資料邊界:新增版本化的 --json 模式,stdout 放資料、stderr 放診斷,保留原有 exit code 語意。
  • 行為邊界:CLI 原本能被腳本、SSH、排程與測試呼叫的方式不變。
  • 失敗邊界:先寫下 missing binary、non-zero exit、逾時、空資料、壞 JSON 與超大輸出的畫面。

如果 CLI 本來就是 Swift,優先把業務邏輯抽成 shared library,讓 CLI 與 App 共用同一核心;若重構可能改變既有行為,Apple 也有正式的 內嵌 command-line helper 路徑。這和「LLM 提案、程式裁決」的 Deterministic Core 原則相同:不要把已經能測的部分重新交給模型猜。

CLI 核心透過版本化 JSON 邊界連接薄 SwiftUI 外殼的架構圖
保留 CLI 的可組合性,SwiftUI 只負責狀態呈現與固定操作。圖:AlphaLab

Claude Code SwiftUI 步驟 1:先固定環境,再只做規劃

MenuBarExtra 從 macOS 13 起可用,因此本文用 ObservableObject@Published@StateObject 保持 macOS 13 相容;若直接採用 @Observable,部署下限會到 macOS 14。建置 App 需要完整 Xcode,只有 Command Line Tools 並不含 xcodebuild

xcode-select --print-path
xcodebuild -version
swift --version
claude --version
cd /path/to/your-cli
claude --permission-mode plan

先讓 Claude Code 探索 repo、列出現有 build/test 指令與 CLI contract,再批准實作。Anthropic 的 官方最佳實務也建議 explore → plan → implement,並把 build、test、exit code、fixtures 或 screenshot 變成可執行的 pass/fail。注意:Plan mode 是工作模式,不是整台電腦的安全邊界;CLAUDE.md 也是行為指引,不會取代 permissions、sandbox 或 deny rules。需要把安全控制做成硬規則時,可搭配 Agent Runtime Controls 的四道閘門思路。

步驟 2:把「完成條件」寫進提示,而不是只說做一個 App

下面這份提示可以直接改。它刻意限制 Claude Code 不重寫核心,也不把成功編譯當成終點:

先探索專案並提出計畫,不要立刻改檔。

目標:保留現有 CLI 與測試,新增 macOS menu-bar SwiftUI App。
邊界:
1. GUI 只能執行 bundled helper 的固定 snapshot --format json。
2. 禁止 /bin/sh -c、任意使用者命令與 inherited PATH。
3. stdout/stderr 分流;保留 exit code;加入 timeout 與 output cap。
4. UI 必須有 idle/loading/ready/empty/failure 狀態。
5. macOS 13 target,使用 ObservableObject;必要的 AppKit 放在單一 boundary。

驗收:
- CLI 與 GUI 共用同一組 JSON fixtures。
- 測 missing executable、non-zero exit、invalid JSON、timeout、oversized output。
- xcodebuild build/test、鍵盤操作、VoiceOver、dark mode、clean reinstall。
- 未完成任何一項就列為 blocker,不得宣告完成。

把這類可重跑規範留在 repo,而不是只存在聊天紀錄。若你常把規則、Skills 與 Hooks 打包,可延伸閱讀 Cursor Plugin 的可驗收封裝方式;工具不同,版本化工作合約的概念相同。

步驟 3:用 MenuBarExtra 做薄殼,不把終端機塞進視窗

最小 SwiftUI shell 只需要一個狀態模型與一個 scene。.window style 適合放正常按鈕與狀態卡;若要隱藏 Dock 圖示,還要在 Info.plist 明確設定 LSUIElement=true,MenuBarExtra 不會自動替你做。

@main
struct CLIPulseApp: App {
    @StateObject private var model = StatusModel()

    var body: some Scene {
        MenuBarExtra("CLI Pulse", systemImage: model.symbolName) {
            StatusPanel(model: model)
        }
        .menuBarExtraStyle(.window)
    }
}

介面不要直接理解 CLI 的人類可讀文字。讓 StatusModel 把資料轉成 idle / loading / ready / empty / failure 五種狀態,View 只渲染狀態。這樣 CLI 格式變動時,你會在 decoder 或 fixture test 看到明確失敗,而不是得到一張看似正常、數字卻錯的卡片。

步驟 4:用 Process 接回 helper,避開 shell 注入與 PATH 陷阱

Apple 的 Foundation Process 會把 arguments 直接放進 argv,不會做 shell expansion,因此不要先把參數拼成一串字,也不要用 /bin/sh -c。內嵌 helper 時,用 bundle 解析絕對位置:

guard let helper = Bundle.main.url(
    forAuxiliaryExecutable: "ReadOnlyCore"
) else { throw CLIError.missingExecutable }

let process = Process()
process.executableURL = helper
process.arguments = ["snapshot", "--format", "json"]
process.currentDirectoryURL = URL(fileURLWithPath: "/", isDirectory: true)
process.environment = ["LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr

正式 wrapper 還要在背景執行、同時 drain stdout 與 stderr、限制總輸出、設定 timeout/cancel,最後依序驗證 exit code、UTF-8、JSON schema 與欄位語意。不要在 MainActor 上同步 waitUntilExit();單純包進 Task {} 也可能繼承 MainActor。程序環境則應採 allowlist,避免把 token、shell function 或不受控 PATH 一起帶進 App。

步驟 5:補齊錯誤、鍵盤、VoiceOver 與 AppKit 邊界

漂亮的 ready state 只占五分之一。至少把下列狀況畫成可辨識、可恢復的介面:尚未執行、讀取中、無資料、成功、失敗。錯誤訊息不能只靠紅色;重新整理要有可見按鈕與 Command-R,退出可用 Command-Q,icon-only control 要加 accessibility label、value 或 hint。用 VoiceOver 實際完成任務,而不是只看元件有沒有標籤;macOS 可用 Command-F5 開啟 VoiceOver,並用 Xcode 的 Accessibility Inspector 稽核。

「UI 可被 VoiceOver 操作」和「App 需要 macOS 的 Accessibility 權限」是兩件事。當 App 透過 Accessibility API 存取或控制其他 App、系統 UI 時,才進入相關 TCC 授權議題;單純顯示 MenuBarExtra、啟動自己的 helper,不能據此宣稱需要這項權限。AppKit 也應集中在一個 boundary,處理 activation policy、測試視窗、quit 或必要的公告,不要讓整個 View tree 依賴它。

Claude Code SwiftUI 步驟 6:用四層驗收阻止「能開」冒充完成

驗收順序應由內往外。第一層是核心正確性:CLI 與 GUI 吃同一組 JSON fixtures。第二層是程序失敗:測 binary 不見、permission/non-zero、壞 JSON、壞 payload、timeout、超大輸出。第三層才是畫面與操作:固定 window size、golden screenshot、鍵盤腳本、深色模式、VoiceOver。第四層是交付:archive、簽章、乾淨重裝、啟動、讀 crash report。

SwiftUI 原生 App 的核心正確性、錯誤、無障礙與重裝四層驗收卡
把 build、fixtures、accessibility 與 clean install 分開過關。圖:AlphaLab

為確認流程不是紙上談兵,本篇另建了最小 proof:在 macOS 26.1、Xcode 26.3、Swift 6.2.4(Swift 5 language mode、package deployment target macOS 14)下,兩個 Xcode scheme 成功建置,8 個程序與 payload 測試通過,ad-hoc nested signing 與 codesign --verify --deep --strict 通過,App 也被重新複製到獨立 temp install 目錄後啟動,讀到 bundled CLI 的 ready state。這不是乾淨機器或正式分發驗證,只證明最小架構可行;前述 macOS 13 寫法有 Apple API 文件支持,但沒有在這次本機 proof 執行。自動 screenshot 無法產生預期圖片,後續鍵盤 artifacts 也沒有出現;VoiceOver automation 則尚未實作或執行。這三項仍是 blocker,不能被「build 成功」抵銷。這種雙重驗收思路,也可參考 Claude Code OfficeCLI 的資料與視覺驗收

打包分流:自用、Developer ID、Mac App Store 不同路

ad-hoc signing 不構成受 Gatekeeper 信任、可公證的 Developer ID 發佈。走 Developer ID 直接分發時,需要合適的 Developer ID 憑證、Hardened Runtime 與 notarization;notarytool 上傳的是 ZIP、DMG 或 flat PKG,不是裸 .app。ZIP 本身不能 staple,應 staple App/DMG/PKG 後再重新封裝。走 Mac App Store 則必須處理 App Sandbox;內嵌 helper 可以存在,但會繼承宿主 sandbox,不能靠啟動 shell 繞過權限。

下方是 Developer ID 路徑的順序範本,不是所有專案都能原封不動執行。先在 Xcode 設定實際 Team、certificate 與 signing,再建立有效的 ExportOptions.plist;本機 Xcode 26.3 顯示的 Developer ID export method 是 developer-id,其他鍵值要以你的專案與當版 xcodebuild -help 為準。

xcodebuild -project CLIPulse.xcodeproj -scheme CLIPulse \
  -configuration Release -archivePath .build/CLIPulse.xcarchive archive

xcodebuild -exportArchive \
  -archivePath .build/CLIPulse.xcarchive \
  -exportPath .build/export \
  -exportOptionsPlist ExportOptions.plist

ditto -c -k --keepParent .build/export/CLIPulse.app .build/CLIPulse.zip
xcrun notarytool submit .build/CLIPulse.zip \
  --keychain-profile AC_NOTARY --wait
xcrun stapler staple .build/export/CLIPulse.app
xcrun stapler validate .build/export/CLIPulse.app
ditto -c -k --keepParent .build/export/CLIPulse.app \
  .build/CLIPulse-notarized.zip

依賴任意 Homebrew binary、任意檔案路徑或廣泛系統權限的 wrapper,通常需要重新設計 helper 邊界,或改走適合的直接分發方案。先決定 distribution,再決定 sandbox 與 entitlements;不要反過來讓 Claude Code 猜。

CLI、TUI 還是原生 App?用工作情境決定

  • 保留純 CLI:主要在 SSH、CI、腳本、低頻寬或跨平台環境工作。
  • 選 TUI:需要高密度鍵盤操作,但仍要留在 terminal session、tmux 或遠端主機。
  • 加 menu-bar App:任務是 glanceable status、少量固定操作、通知或快速切換,且主要使用者在 macOS 桌面。
  • 做完整視窗 App:需要多步驟導覽、文件編輯、複雜拖放或大量設定;不要把所有流程硬塞進 menu bar。

AI 降低的是探索與樣板成本,不會自動消除 correctness、無障礙、簽章、公證、更新與維護成本。保留 CLI,等於同時保留一條可測、可回退、可遠端操作的 escape hatch。

Claude Code SwiftUI 常見問題

1. 一定要重寫原本 CLI 嗎?

不一定。能安全抽 library 就共用核心;否則先把原 binary 當 bundled helper,保留既有 contract,再逐步重構。

2. MenuBarExtra 最低支援哪個 macOS?

macOS 13。更早系統需改用 AppKit 的 NSStatusBar;若使用 Observation 的 @Observable,則要注意 macOS 14 的部署下限。

3. 可以直接呼叫使用者安裝的 Homebrew CLI 嗎?

可以設計,但 PATH、CPU 架構、版本、簽章與 sandbox 都會變成變數。新手專案優先內嵌已簽章 helper,或讓使用者明確選定並驗證路徑。

4. 用 Process 就沒有 injection 風險嗎?

不是。Process 避開 shell expansion,但你仍要固定 executable、驗證 arguments、限制 environment、輸出大小、timeout 與可存取資料。

5. Screenshot test 能證明 CLI 結果正確嗎?

不能。Screenshot 只驗視覺回歸;數值與狀態要靠 CLI/GUI 共用 fixtures、schema 與 semantic assertions。

6. Menu-bar App 一定要 Accessibility 權限嗎?

不一定。讓 UI 支援 VoiceOver 不等於要求 TCC Accessibility 權限;只有實際用相關 API 存取或控制其他 App、系統 UI 時,才依功能判定。

7. Build 成功後可以直接給別人下載嗎?

不是。Build 成功本身不代表適合放心公開發佈;正常 Developer ID 路徑還要完成 release archive、正式簽章、notarization 與 staple。乾淨機器安裝、更新與回復策略則是交付 QA,不要混成「編譯通過」的一部分。

8. 這套方法也適合 Windows 或 Linux 嗎?

架構原則可以參考,但 SwiftUI、MenuBarExtra、App Sandbox 與 notarization 都是 Apple 平台細節,不能直接外推到 WinUI、GTK 或跨平台產品。

給新手的 6 個帶走重點

  1. 先選低風險、讀取型、輸出可固定的 CLI。
  2. 保留 CLI contract,新增版本化 JSON 邊界。
  3. SwiftUI 只渲染有限狀態,不解析人類文字。
  4. Process 使用固定 helper、靜態參數與最小 environment。
  5. build、資料、錯誤、無障礙、重裝要分層驗收。
  6. 分發方式先決定,簽章、sandbox 與公證才不會做到一半重來。

接著閱讀

左右滑動查看更多推薦

結語:先包一個狀態,不要重寫整個工具

最好的第一步不是叫 Claude Code「把 CLI 變成漂亮 App」,而是挑一個穩定的 status --json,做出只會顯示、重新整理與報錯的 menu-bar shell。等同一組 fixtures 能同時證明 CLI 與 GUI 正確,再加入更多操作。這樣 AI 省下的是介面原型時間,而不是把工程風險藏進更漂亮的視窗。

想繼續拆解 AI 工具與開發方法,可以回到 AlphaLab AI 專區;需要依序練習的完整路線,則可查看 AlphaLab 線上課程

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多三封:一封 Weekly 週報與最多兩封關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。