雲端 Mac 驗收 iOS App Intents:參數契約與捷徑入口

AI 自動化 ·約 7 分鐘閱讀

雲端 Mac 驗收 iOS App Intents:參數契約與捷徑入口

一個「記錄閱讀」捷徑在開發用 Mac 上找得到,換到遠端建置所用的模擬器卻沒有出現。這時先別修改業務程式碼:參數是否有效、Intent 能否執行、系統是否發現該動作,是三項不同的檢查。雲端 Mac 適合用來固定前兩項的測試環境;最後一項仍須在已安裝 App 的模擬器中驗收。

先釐清自動測試的邊界

本文以不寫入磁碟的範例動作驗證流程:輸入書籍編號、去除頭尾空白,再回傳正規化後的編號。如此便能先排除儲存權限與網路波動的影響。日後接上資料庫時,應另外測試寫入及重複執行策略,不要把「回傳成功」誤認為「資料已儲存」。

驗收分成三層:以純 Swift 規則檢查空值與不合法字元;透過 XCTest 呼叫 perform(),檢查回傳值與錯誤;最後由人工在模擬器中確認動作是否可見、參數能否填寫,以及結果能否讀取。前兩層失敗應阻止合併。第三層會受系統索引影響,適合作為版本發布前的檢查項目,而不是靠固定等待秒數編寫測試。

不要為了等捷徑出現,就在自動測試中反覆解除安裝、重新安裝 App。先證明程式碼契約正確,再排查 App 安裝、首次啟動與系統發現動作的過程。

將輸入規則抽成可直接測試的型別

以下規則接受 1–32 個 ASCII 字母、數字、底線與連字號。空白只允許出現在輸入的頭尾;其他字元會回傳明確的錯誤。範例中的 perform() 只回傳編號,不會產生持久化的副作用。

import AppIntents
import Foundation

enum ReadingIDError: LocalizedError {
    case invalid

    var errorDescription: String? {
        "编号须为 1–32 位字母、数字、下划线或连字符"
    }
}

enum ReadingID {
    static func normalize(_ raw: String) throws -> String {
        let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        let pattern = #"^[A-Za-z0-9_-]{1,32}$"#
        guard value.range(of: pattern, options: .regularExpression) != nil else {
            throw ReadingIDError.invalid
        }
        return value
    }
}

struct CheckReadingIDIntent: AppIntent {
    static var title: LocalizedStringResource = "检查书籍编号"
    static var openAppWhenRun = false

    @Parameter(title: "书籍编号")
    var bookID: String

    func perform() async throws -> some IntentResult & ReturnsValue<String> {
        let normalized = try ReadingID.normalize(bookID)
        return .result(value: normalized)
    }
}

請將參數標題、動作標題及錯誤提示納入專案的本地化資源。程式碼中的簡體中文只是範例文案,不應直接當成多語言 App 所有語言的交付文字。規則本身只接受 ASCII 編號;若業務允許漢字或其他文字,應先定義允許的字元集,再修改運算式與測試案例。

覆蓋邊界條件,而不只測正常輸入

至少要檢查 " book_7 " 會回傳 "book_7",而空字串、含有中間空白的字串,以及超過 32 個字元的編號都會被拒絕。錯誤分支應斷言錯誤型別,不要只斷言「有某種錯誤被拋出」;否則底層相依項意外拋錯,也可能讓測試通過。測試 perform() 時,還要確認回傳值符合正規化規則。將純規則測試與 Intent 呼叫測試分開,失敗時才能判斷是輸入規則改變,還是系統介面的封裝層改變。

註冊入口並在模擬器中檢查

若要讓系統顯示建議動作,可以在 App target 中加入 AppShortcutsProvider。短語中保留 App 名稱的佔位符,讓系統將動作關聯到目前的 App;這不是 shell 字串。

import AppIntents

struct ReadingShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: CheckReadingIDIntent(),
            phrases: ["在 \(.applicationName) 检查书籍编号"],
            shortTitle: "检查书籍编号",
            systemImageName: "book"
        )
    }
}

先確認這份檔案屬於 App target,而非只屬於測試 target。安裝建置完成的 App 並啟動一次,再開啟模擬器中的「捷徑」,搜尋動作標題,填入有效編號並執行。接著提交無效編號,檢查錯誤訊息是否易懂。若動作未出現,記錄系統版本、App 建置版本,以及安裝後的操作順序;不要把「重新開啟頁面後碰巧出現」寫成確定有效的修復方法。

固定測試裝置與結果目錄

在 ZSHMac 的雲端 Mac 上,先以 xcrun simctl list devices available 查看已安裝且可用的模擬器,選定裝置 UDID,並設定 SIM_UDID。以下命令假設專案中有名為 Reader 的共用 scheme;實際執行時,請改用自己專案的 scheme。每次執行都為結果套件指定新路徑,避免既有檔案導致測試命令直接失敗。

mkdir -p artifacts
test -n "${SIM_UDID:-}" || { printf '%s\n' 'SIM_UDID is required' >&2; exit 1; }
result="$PWD/artifacts/app-intents-$(date +%Y%m%d-%H%M%S)-$$.xcresult"
xcodebuild test \
  -scheme Reader \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath "$result"

在 CI 中保留命令的結束代碼與結果套件:結束代碼決定作業是否通過,結果套件則供失敗後定位測試案例。若 scheme 未共用,或目標模擬器不可用,應先修正執行入口,不要將其歸類為 Intent 邏輯失敗。手動驗收時,則記錄動作是否出現、輸入提示是否正確,以及有效與無效參數的實際表現。

失敗時依層次排查

現象 優先檢查 不要先做
純規則測試失敗 字元範圍、長度與頭尾空白處理 重新安裝模擬器
perform() 結果不符 傳入的參數與回傳值型別 等待系統索引
測試命令找不到裝置 可用裝置清單與 UDID 修改 Swift 規則
捷徑中沒有動作 App target 成員關係、安裝與首次啟動 在測試中加入固定延遲
動作存在但執行時報錯 參數提示、錯誤型別與目標系統版本 吞掉所有錯誤

分層檢查的價值在於縮小故障範圍,而不是保證系統入口在安裝後立即可見。程式碼測試通過,且模擬器中的動作也完成有效與無效輸入檢查,這次驗收才算完整。接入真正的寫入功能後,還須另外測試重複執行與資料寫入磁碟,不能沿用本例「只回傳編號」的通過標準。

常見問題

XCTest 通過後,為何捷徑仍找不到動作?

先確認包含 AppShortcutsProvider 的 App 已安裝並啟動,再檢查系統版本與目標設定。系統索引可能延遲,不能將立即顯示當成單元測試斷言。

參數驗證應放在哪一層?

把可重用的格式規則放進獨立 Swift 型別,並在 perform() 呼叫它;測試便不必依賴系統索引。

無介面的 CI 能取代捷徑驗收嗎?

不能。CI 可檢查輸入與回傳契約;動作是否顯示、參數提示是否正確,仍須在目標系統的模擬器或裝置上驗證。

需要一台獨享雲端 Mac 嗎?

選擇機型、節點和計費週期。交付的是獨享實體機,實際可用狀態以控制台即時回報為準。

查看規格並下單