Community影像github.com

ParthJadhav/ios-marketing-capture

用於自動化 SwiftUI iOS 應用程式在多個語言、裝置或外觀下的行銷截圖捕獲。涵蓋全螢幕截圖、獨立元件渲染(輪播卡片、小工具)以及可重現的輸出命名。觸發條件包括行銷截圖、語言截圖、小工具渲染、App Store 素材、fastlane 替代方案、simctl 截圖。

ios-marketing-capture 是什麼?

此技能旨在自動化 SwiftUI iOS 應用程式的行銷截圖捕獲流程,支援多個語言環境,並提供兩種並行輸出:全螢幕截圖(使用確定性種子數據和真實狀態欄/安全區域邊框)和獨立元件渲染(如卡片、小工具、圖表,具有透明背景和自然圓角)。其核心架構採用「應用程式內捕獲模式」而非傳統的 XCUITest,主要優勢包括:無需新增測試目標、迭代速度更快(僅需重新啟動已安裝的二進位檔,而非執行完整的 `xcodebuild test`)、可直接存取應用程式真實狀態(如 ViewModel、SwiftData、ImageRenderer),以及能進行 XCUITest 無法實現的元件級渲染。該技能支援 Cursor、Claude Code 和 Codex 等平台,透過一個 DEBUG 閘控的單一檔案 (`MarketingCapture.swift`) 和一個基於步驟的協調器來運作,依序執行導航、等待、擷取和清理動作。使用者需先與技能互動以收集需求(螢幕清單、獨立元件、語言、裝置、外觀、種子數據),然後進行程式碼庫探索來了解導航模式、深層連結和數據填充機制,最後由 AI 實作並迭代驗證,確保自動產生的截圖符合行銷規格。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add ParthJadhav/ios-marketing-capture

Installed? Explore more 影像 skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

iOS 行銷截圖捕獲

概述

為一個 SwiftUI iOS 應用程式自動化可重現的行銷截圖捕獲,支援多個語言環境,並提供兩種並行輸出流程:

  1. 全螢幕截圖 — 每個與行銷相關的畫面,使用確定性種子數據、真實的狀態列/安全區域邊框
  2. 元件截圖 — 特定元件(卡片、小工具、圖表)的獨立渲染,在圓角內部具有自然背景,外部則為透明背景

此技能僅負責截圖步驟。如果使用者還想要圍繞截圖合成 Apple 風格的行銷頁面(裝置模型、標題、漸層),請結合 app-store-screenshots 技能作為後處理步驟。

核心方法

應用程式內捕獲模式,而非 XCUITest。這是一個艱難的決定,它與 Fastlane snapshot / XCUITest 的慣例有所不同,但在幾乎所有實際專案中都勝出。

為什麼應用程式內模式優於 XCUITest:

  • 無需新的測試目標。 為現有 Xcode 專案新增 UI 測試目標是一項脆弱的 pbxproj 手術。許多專案根本沒有測試目標,也沒有使用 xcodegen — 手動新增一個很容易出錯。
  • 更快的迭代。 每次執行 UI 測試都需要 30 秒以上的啟動時間。應用程式內截圖只需重新啟動已安裝的二進位檔。
  • 無需 xcodebuild test。 整個流程是執行一次 xcodebuild build,然後為每個語言環境執行 simctl launch。沒有測試套件的額外負擔。
  • 可直接存取真實的應用程式狀態。 你可以直接呼叫 ViewModel、SwiftData、ImageRenderer 和 UIWindow.drawHierarchy。XCUITest 只能點擊和讀取輔助功能元素。
  • 元件渲染無論如何都需要在進程內完成。 在小工具視圖或獨立元件上使用 ImageRenderer 必須在應用程式進程內執行 — XCUITest 沒有對應的等效方法。

運作方式:

  1. 一個僅限 DEBUG 的 MarketingCapture.swift 檔案存在於主應用程式目標中
  2. 當使用 -MarketingCapture 1 參數啟動時,應用程式會播種數據,然後一個協調器會遍歷 CaptureStep 列表 — 每個步驟會進行導航、等待畫面穩定、進行截圖,並進行清理
  3. PNG 檔案會寫入應用程式沙盒中的 Documents/marketing/<locale>/ 目錄
  4. 一個 shell 腳本會執行一次建置、安裝,然後透過使用 -AppleLanguages (xx) -AppleLocale xx 參數重新啟動應用程式來循環處理每個語言環境,並透過 simctl get_app_container 提取檔案

處理流程

按順序完成這些步驟。不要跳步。

步驟 1:收集需求

一次一個問題 地向使用者提問(不要一次問完 — 每個答案都可能使後續問題無效):

  1. 要截取的畫面 — 「你想要哪些畫面?請提供每個畫面的導航路徑或頁籤名稱。」取得一個具體的清單,而不是「主要流程」。
  2. 獨立元件 — 「是否有任何元件需要以透明背景獨立渲染?(輪播卡片、小工具、英雄圖塊、圖表等)」
  3. 語言環境 — 「哪些語言環境?(a)Localizable.xcstrings 中的所有語言環境,(b)一個我會指定的 App Store 子集,或(c)讓我給你一個明確的清單。」如果是(a),使用以下指令從 .xcstrings 檔案中擷取語言代碼:
    python3 -c "import json; d=json.load(open('<path>/Localizable.xcstrings')); langs=set(); [langs.update(v.get('localizations',{}).keys()) for v in d['strings'].values()]; print(sorted(langs))"
    
  4. 裝置 — 「哪個模擬器?(建議使用 6.1 吋 iPhone 17 以支援 iOS 26 的設計功能)」— 透過 xcrun simctl list devices available 驗證裝置是否可用。
  5. 外觀 — 「僅淺色模式、僅深色模式,還是兩者都要?」
  6. 種子數據 — 「目前如何填充示範數據?(a)全新安裝時自動播種,(b)有一個偵錯的「載入示範數據」按鈕,(c)你手動添加,(d)尚無示範數據。」然後:「現有數據是否足夠詳盡,讓你列出的每個畫面在行銷上看起來都已填充完整?與使用者一起稽核它。」

步驟 2:探索

在編寫任何程式碼之前,充分探索程式碼庫以回答以下問題:

  • 專案是否使用 Xcode 同步資料夾群組(Xcode 16+,PBXFileSystemSynchronizedRootGroup)?如果是,新檔案會自動包含在其目標中 — 無需編輯 pbxproj。使用 grep -c PBXFileSystemSynchronized <proj>.xcodeproj/project.pbxproj 進行檢查。
  • 根導航模式是什麼?
    • TabView(selection:) — 最常見。你需要:@State selectedTab 綁定、頁籤索引,以及哪些頁籤有巢狀的 NavigationStack。
    • NavigationStack(使用路由器的單一堆疊)— 你需要:路徑綁定或路由器物件,以及 NavigationLink(value:) / .navigationDestination 類型的集合。
    • NavigationSplitView — 你需要:側邊欄選取綁定、詳細資訊欄的導航狀態。
    • 自訂協調器 / UIKit 主機 — 你需要:協調器的 navigate(to:) 方法或其等效方法。
  • 深層連結 是如何路由的?找到 onOpenURL 處理常式以及將 URL 對應到導航狀態的列舉/切換。
  • 示範數據播種器 定義在哪裡?從偵錯按鈕(如果有的話)追蹤程式碼路徑到實際寫入 ModelContext 的函式。如果沒有播種器存在,請參閱下面的「建立示範數據播種器」。
  • 小工具 是否位於獨立的目標中?小工具視圖檔案和條目類型是否也存在於主應用程式目標中?(幾乎可以肯定不是 — 如果你想透過 ImageRenderer 渲染它們,需要將它們加入。)
  • 應用程式是否使用 Live Activities / ActivityKit?如果是,請標記為已知陷阱(見下文)。
  • 應用程式是否使用 SwiftData + CloudKit 同步(cloudKitDatabase: .automatic)?如果是,請標記為已知陷阱。
  • 是否有任何視圖需要以 非預設狀態 進行截圖?(例如,計時器正在倒數、表單已部分填寫、具有特定值的圖表)。如果是,每個都需要一個 static var 預設機制(請參閱下面的「預設視圖狀態」)。

步驟 3:向使用者呈現設計

在編寫程式碼之前,以此結構總結你的計畫。在繼續之前取得明確的批准:

  1. 架構(應用程式內捕獲模式、單一檔案、DEBUG 閘控)
  2. 檔案列表(你將建立/修改的確切路徑)
  3. 逐畫面截圖計畫(每個畫面如何到達 — 頁籤索引、導航路徑、工作表觸發器)
  4. 截圖順序理由(哪些畫面必須在其他畫面之前 — 請參閱陷阱 #5)
  5. 元件渲染方法(哪些元件,它們將如何被包裝)
  6. 輸出佈局(資料夾結構、命名慣例)
  7. 與此專案相關的已知陷阱(從步驟 2 標記)
  8. 需要預設的狀態(哪些視圖,需要什麼靜態變數)

步驟 4:實作

使用 templates/ 中的模板作為起點。它們是參考模式,而非複製貼上的基礎架構 — 每個專案的導航、模型和視圖都不同。模板展示了建構積木;你需為目標應用程式組合它們。

要產生的關鍵檔案:

  • <AppName>/Debug/MarketingCapture.swift — 整個截圖系統,僅限 DEBUG。包含:
    • MarketingCapture 列舉(啟動參數解析、輸出輔助函式、視窗截圖、預設變數)
    • MarketingCaptureCoordinator 類別(遍歷 [CaptureStep] 並對每個進行截圖)
    • MarketingElementHarness 列舉(卡片、小工具、圖表的 ImageRenderer 渲染)
  • <AppName>/ContentView.swift(或根視圖所在的位置)— 用於播種數據並執行協調器的 DEBUG 鉤子。
  • 任何需要預設狀態的視圖 — 盡限 DEBUG 的 .onAppear 鉤子和 .onReceive 關閉監聽器。
  • scripts/capture-marketing.sh — 建置 + 安裝 + 每個語言環境的循環。
  • .gitignore — 新增 marketing/。

步驟 5:迭代驗證

不要 將腳本交給使用者然後等待。自己在模擬器上執行它,並在宣告完成之前至少驗證一個語言環境。使用「讀取」工具讀取輸出的 PNG 檔案,以視覺方式驗證每個畫面是否顯示你預期的內容。常見的執行時期問題列在下面的「已知陷阱」中。

當你發現問題時,修正它,重新執行整個腳本(不僅僅是失敗的語言環境 — 修正可能會使先前的語言環境回歸),然後再次進行視覺驗證。

架構:基於步驟的截圖

協調器透過遍歷 CaptureStep 值列表來驅動截圖。每個步驟都是自包含的:它知道如何導航到其畫面、等待多長時間,以及之後如何清理。

struct CaptureStep {
    let name: String                        // 輸出檔案名稱,例如 "01-home"
    let navigate: @MainActor () -> Void     // 將應用程式置於正確狀態
    let settle: Duration                    // 等待動畫/載入完成
    let cleanup: (@MainActor () -> Void)?   // 在下一個步驟之前進行清理
}

協調器是一個簡單的迴圈:

for step in steps {
    step.navigate()
    try? await Task.sleep(for: step.settle)
    if let image = MarketingCapture.snapshotKeyWindow() {
        MarketingCapture.writePNG(image, name: step.name)
    }
    step.cleanup?()
    try? await Task.sleep(for: .milliseconds(400))  // 清理動畫
}

為不同導航模式建構步驟

TabView 應用程式(最常見):

// 簡單的頁籤切換 — 只需設定索引
CaptureStep(name: "01-home", navigate: { setTab(0) }, settle: .milliseconds(1800), cleanup: nil)

// 頁籤 + 顯示的工作表
CaptureStep(
    name: "05-timer-setup",
    navigate: {
        setTab(3)
        pendingBrewRecipe = someRecipe
    },
    settle: .milliseconds(2000),
    cleanup: {
        NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
        pendingBrewRecipe = nil
    }
)

NavigationStack + 路由器應用程式:

// 將一個路由推入堆疊
CaptureStep(
    name: "02-detail",
    navigate: { router.push(.itemDetail(item)) },
    settle: .milliseconds(1800),
    cleanup: { router.popToRoot() }
)

NavigationSplitView 應用程式:

// 選取側邊欄項目,然後選取詳細資訊
CaptureStep(
    name: "03-detail",
    navigate: {
        sidebarSelection = .recipes
        detailSelection = recipes.first
    },
    settle: .milliseconds(1800),
    cleanup: { detailSelection = nil }
)

排序:堆疊規則

在需要「乾淨」導航狀態的畫面之前,先截取那些畫面,然後再截取會推入同一堆疊的畫面。 巢狀 NavigationPath / @State 存在於子視圖內部,無法從協調器中彈出。因此:

好:  書架(乾淨列表) → 咖啡詳情(推入書架的堆疊)
壞:  咖啡詳情 → 書架(堆疊仍然有推入的詳情)

如果兩個畫面共用一個 NavigationStack,請先截取根層級視圖。

預設視圖狀態

某些畫面需要在特定的非預設狀態下進行截圖 — 例如計時器正在倒數、具有特定值的圖表、已部分填寫的表單。模式如下:

  1. 為每個預設值在 MarketingCapture 中新增一個 static var:

    /// 由協調器在顯示計時器視圖之前設定。
    /// 視圖在 .onAppear 中讀取此值,以跳轉到特定的已過時間。
    static var pendingElapsedSeconds: Int?
    
    /// 設為 true 以在計時器上顯示評估疊加層。
    static var pendingShowAssessment: Bool = false
    
  2. 在目標視圖中,新增一個盡限 DEBUG 的 .onAppear 來讀取預設值:

    .onAppear {
        #if DEBUG
        if MarketingCapture.isActive, let elapsed = MarketingCapture.pendingElapsedSeconds {
            phase = .active
            timerVM.elapsedTime = TimeInterval(elapsed)
            timerVM.start()
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { timerVM.pause() }
        }
        #endif
    }
    
  3. 在協調器中,在導航之前設定該變數:

    CaptureStep(
        name: "06-timer-midway",
        navigate: {
            MarketingCapture.pendingElapsedSeconds = 75
            openTimerSheet(someRecipe)
        },
        settle: .milliseconds(2400),
        cleanup: {
            MarketingCapture.pendingElapsedSeconds = nil
            NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
        }
    )
    

建立示範數據播種器

如果應用程式沒有現有的示範數據機制,請建立一個。將其放置在 <AppName>/Debug/DemoDataSeeder.swift 中,並包裹在 #if DEBUG 區塊內。

指導原則:

  • 播種足夠的數據,使每個被截取的畫面看起來都已經填充完整。根據畫面列表稽核播種的數據。
  • 使用逼真的內容:真實的地名、合理的數字、多樣的狀態(某些項目「庫存偏低」、某些「新鮮」、有些有圖片、有些沒有)。
  • 如果應用程式使用 SwiftData,直接寫入 ModelContext。如果是 Core Data,則使用管理物件上下文。如果是 REST 後端,則透過本機快取/儲存層播種。
  • 使播種冪等 — 在插入之前檢查數據是否已存在。儲存層在模擬器重新啟動之間會持續存在,而為每個語言環境重新播種會導致 CloudKit 同步問題和崩潰。
  • 包含足夠的多樣性來填充不同的 UI 狀態:除非是行銷畫面,否則不應出現空狀態。

最小形狀範例:

#if DEBUG
enum DemoDataSeeder {
    static func seedIfEmpty(in context: ModelContext) {
        let existing = (try? context.fetchCount(FetchDescriptor<Item>())) ?? 0
        guard existing == 0 else { return }

        // 具有不同狀態的項目
        let items = [
            Item(name: "...", status: .active, ...),
            Item(name: "...", status: .lowStock, ...),
            // ...足夠填充每個畫面
        ]
        items.forEach { context.insert($0) }
        try? context.save()
    }
}
#endif

元件渲染

元件透過 ImageRenderer 以 3 倍比例渲染,圓角外部為透明背景。

卡片 / 列表行

@MainActor
static func renderCards(items: [Item], theme: AppTheme) {
    let cardWidth: CGFloat = 380

    for item in items {
        let card = ItemCard(item: item, theme: theme)
            .padding(.horizontal, 16)
            .padding(.vertical, 12)
            .frame(width: cardWidth)
            .background(theme.background)
            .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))

        let renderer = ImageRenderer(content: card)
        renderer.scale = 3
        renderer.isOpaque = false
        renderer.proposedSize = .init(width

… [為翻譯而截斷]

相關技能