· Workflow · 9 phút đọc

Tự động hóa ảnh chụp màn hình App Store cho SwiftUI mà không cần Fastlane (2026)

Tự động hóa ảnh chụp màn hình App Store cho SwiftUI mà không cần Fastlane (2026)
TL;DR. Fastlane không phải là cách duy nhất để tự động hóa ảnh chụp màn hình cửa hàng iOS — nó chỉ là cách nổi tiếng nhất. Apple cung cấp sẵn mọi thứ bạn cần: XCUITest + XCUIScreenshot để chụp ứng dụng đang chạy thực, API attachments mới của Swift Testing để làm điều tương tự với ít code lặp lại hơn, và ImageRenderer để render trực tiếp một view SwiftUI thành PNG mà không cần chạy simulator. Các mã nguồn mở như swift-snapshot-testingAppScreenshotKit bọc quanh những công cụ đó. Không cái nào trong số đó dựng hình carousel marketing — khung, chú thích, nền, bản sao 50 locale. Đó là một bước riêng biệt, và đó là nơi Mokbi phù hợp: nó dựng hình và bản địa hóa các ảnh chụp màn hình bạn đã chụp. Nó không chụp chúng.

snapshot của Fastlane là câu trả lời mặc định cho "tự động hóa ảnh chụp màn hình App Store của tôi", và có lý do chính đáng cho điều đó — nó trưởng thành và thân thiện với CI. Nhưng nó cũng là một Ruby toolchain, một Snapfile, một SnapshotHelper.swift bạn phải copy vào test target, và một lượng thiết lập không nhỏ cho thứ mà bên dưới chỉ là một lớp bọc mỏng quanh chính các API XCUITest bạn đã có sẵn. Nếu bạn xây dựng bằng SwiftUI và không muốn thêm một Ruby dependency vào dự án iOS của mình, bạn có thể tự động hóa ảnh chụp màn hình chỉ với Xcode và Swift. Bài viết này là bản đồ trung thực về các lựa chọn native trong năm 2026, kèm code bạn có thể dán trực tiếp, cùng một ranh giới rõ ràng về công cụ nào làm việc gì. Để so sánh trực diện về đánh đổi, xem Mokbi vs Fastlane.

Một điểm cần nói trước, vì nó tránh được nhiều nhầm lẫn: chụp frame (điều khiển ứng dụng đến một trạng thái và lưu PNG của những gì đang hiển thị trên màn hình) và dựng hình ảnh chụp màn hình cửa hàng (frame đó bên trong một mockup thiết bị, với tiêu đề, nền và bản sao đã dịch, xuất ở mọi kích thước yêu cầu) là hai vấn đề hoàn toàn khác nhau. Mọi thứ trong bài viết này giải quyết vấn đề thứ nhất. Không cái nào giải quyết vấn đề thứ hai.

Tại sao bỏ qua Fastlane?

Không có gì sai với Fastlane. Lý do các developer chọn con đường native là thực tế, không phải ý thức hệ:

  • Không có Ruby trong vòng lặp. Fastlane là một Ruby gem với vòng đời phiên bản riêng (Bundler, xung đột gem, thỉnh thoảng CI hỏng sau một bản nâng cấp Ruby). Nếu dự án của bạn vốn thuần Swift, đó là cả một runtime bạn phải duy trì cho một việc.
  • Bạn đã có sẵn test target. snapshot tạo ra UI test bên dưới. Nếu bạn đang viết UI test dù sao đi nữa, chụp một ảnh chỉ tốn thêm hai dòng — không cần công cụ mới.
  • SwiftUI đã thay đổi cuộc chơi. Với ImageRenderer bạn có thể render một màn hình thành PNG mà không cần khởi chạy ứng dụng chút nào, điều Fastlane không làm được — nó luôn phải điều khiển simulator.
  • Ít mắt xích hơn trong CI. Một lệnh xcodebuild test đơn thuần tạo ra ảnh chụp màn hình dễ suy luận hơn một Fastlane lane phải shell ra xcodebuild rồi xử lý hậu kỳ một result bundle.

Mặt trái: Fastlane gộp các phần phiền phức (thu thập attachment từ bundle .xcresult, tổ chức chúng theo locale × thiết bị, tạo bản xem trước HTML, và tải lên qua deliver) vào một lệnh duy nhất. Đi theo hướng native thì một phần công việc thu thập đó trở thành việc của bạn. Đó là toàn bộ quyết định, và đáng để cân nhắc có ý thức.

Lựa chọn 1: XCUITest + XCUIScreenshot (chụp ứng dụng đang chạy thực)

Đây là nền tảng mà chính Fastlane được xây dựng trên đó. Một UI test target khởi chạy ứng dụng thực của bạn trong simulator, điều khiển nó bằng cùng API finder/tap như bất kỳ UI test nào, và XCUIScreen.main chụp toàn bộ màn hình thiết bị ở độ phân giải native. Bạn bọc ảnh chụp màn hình đó trong một XCTAttachment và đặt lifetime = .keepAlways để Xcode giữ nó lại ngay cả khi test pass (mặc định sẽ loại bỏ attachment khi thành công).

// ScreenshotUITests.swift  — runs in a UI test target
import XCTest

final class ScreenshotUITests: XCTestCase {
    func testCaptureCarouselScreens() {
        let app = XCUIApplication()
        // A launch argument your app reads to seed demo data + hide
        // anything that would make a noisy screenshot (debug banners, etc).
        app.launchArguments += ["-FASTLANE_SNAPSHOT", "NO", "-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Drive the app to the state you want, then capture the WHOLE screen.
        snapshot("01_Home")

        app.buttons["openLibrary"].tap()
        snapshot("02_Library")
    }

    private func snapshot(_ name: String) {
        // XCUIScreen.main captures the full device screen at native
        // resolution — exactly what App Store Connect wants.
        let shot = XCUIScreen.main.screenshot()
        let attachment = XCTAttachment(screenshot: shot)
        attachment.name = name
        attachment.lifetime = .keepAlways // or Xcode discards it when the test passes
        add(attachment)
    }
}

Ảnh chụp màn hình nằm bên trong bundle .xcresult. Bạn trích xuất chúng sau khi chạy — hoặc bằng cách mở result bundle trong Xcode và kéo chúng ra, hoặc trong CI bằng một công cụ như xcparse / xc-screenshot để duyệt qua bundle và ghi ra các PNG có tên vào một thư mục. Bước trích xuất này chính xác là phần mà Fastlane tự động hóa cho bạn, và đó là việc chính bạn phải gánh khi đi theo hướng native.

Bản địa hóa mà không cần sửa code. Thủ thuật hay ở đây là test plan của Xcode: tạo một test plan, thêm một cấu hình cho mỗi ngôn ngữ/vùng (mỗi cấu hình đặt ngôn ngữ + vùng của ứng dụng), và cùng một test chạy một lần cho mỗi cấu hình. Bạn có được ảnh chụp màn hình đã bản địa hóa — giao diện ứng dụng được render theo từng locale — mà không cần đụng vào test code. Chạy trên một simulator lớp iPhone 6.9-inch và một simulator lớp iPad 13-inch để đạt các độ phân giải App Store Connect yêu cầu.

Điểm mạnh và điểm yếu:

  • Pixel ứng dụng thực. View đang chạy thực sự — font, theme, dữ liệu động, tất cả đều native — ở đúng độ phân giải của simulator.
  • Không Fastlane, không Ruby. Xcode + Swift thuần túy. Chạy được dưới xcodebuild test trong bất kỳ CI nào.
  • Locale là cấu hình test plan, không phải code. Thêm một cấu hình cho mỗi ngôn ngữ rồi chạy lại; không có nhánh riêng theo locale trong Swift.
  • Bạn tự lo việc trích xuất. Lấy PNG ra khỏi bundle .xcresult là việc của bạn (một script làm một lần, nhưng là việc thật).
  • Chi phí bảo trì. Các finder (buttons["openLibrary"], tap) sẽ hỏng khi giao diện thay đổi, giống như bất kỳ UI test nào.

Lựa chọn 2: Swift Testing attachments (phiên bản hiện đại, nhẹ hơn)

Swift Testing (framework dựa trên macro của Apple, đi kèm Xcode 26) bổ sung một API Attachment làm được điều XCTAttachment làm, với ít nghi thức hơn và cách gọi gọn hơn. Bạn vẫn điều khiển ứng dụng bằng XCUIApplication và chụp bằng XCUIScreen.main.screenshot() — những thứ đó thuộc về XCUITest — nhưng bạn ghi lại ảnh bằng Attachment.record(...) từ một hàm @Test. API attachment không quan tâm ảnh được tạo ra như thế nào; nó chấp nhận bất kỳ kiểu ảnh phù hợp nào và ghi nó vào result bundle của test.

// ScreenshotTests.swift  — Swift Testing (Xcode 26+)
import Testing
import XCTest // still needed for XCUIApplication / XCUIScreen

@MainActor
struct ScreenshotTests {
    @Test func homeAndLibrary() async throws {
        let app = XCUIApplication()
        app.launchArguments += ["-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Attachment.record attaches to THIS test's result bundle.
        let home = XCUIScreen.main.screenshot()
        Attachment.record(home.image, named: "01_Home.png")

        app.buttons["openLibrary"].tap()
        let library = XCUIScreen.main.screenshot()
        Attachment.record(library.image, named: "02_Library.png")
    }
}

Về mặt chức năng, cách này chụp cùng loại pixel ứng dụng thực như Lựa chọn 1 — đó cùng là cơ chế chụp màn hình bên dưới. Điều bạn đạt được là ergonomics gọn gàng hơn của Swift Testing (test có tham số, mặc định async, không cần subclass XCTestCase) nếu dự án của bạn đã chuyển sang Swift Testing. Điều bạn không tránh khỏi là cùng một bước trích xuất: PNG vẫn nằm trong result bundle và vẫn phải được lấy ra để upload. Dùng cách này khi bạn đã dùng Swift Testing rồi; nếu không, Lựa chọn 1 cho kết quả giống hệt.

Lựa chọn 3: ImageRenderer (không cần chạy simulator chút nào)

Đây là lựa chọn mà Fastlane về mặt cấu trúc không thể cung cấp. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) nhận một view SwiftUI và render trực tiếp cây view của nó thành UIImage / CGImage — không khởi chạy ứng dụng, không có UI automation trên simulator, không có finder nào có thể hỏng. Bạn đưa cho nó một view có kích thước đúng bằng pixel App Store yêu cầu và đọc lại byte PNG.

// Render a SwiftUI view straight to a PNG — no simulator UI run.
import SwiftUI

@MainActor
func renderScreen() -> Data? {
    let view = HomeScreen()                 // your real SwiftUI screen
        .frame(width: 1320, height: 2868)   // 6.9-inch iPhone, exact pixels at 1x

    let renderer = ImageRenderer(content: view)
    renderer.scale = 1                       // size is already in target pixels
    renderer.isOpaque = true                 // App Store rejects alpha channels

#if canImport(UIKit)
    return renderer.uiImage?.pngData()
#else
    guard let cg = renderer.cgImage else { return nil }
    let rep = NSBitmapImageRep(cgImage: cg)
    return rep.representation(using: .png, properties: [:])
#endif
}

Có hai điều quan trọng với đầu ra cho cửa hàng. Đặt isOpaque = true — App Store Connect từ chối ảnh chụp màn hình có kênh alpha, và render đục hoàn toàn tránh được điều đó. Và lưu ý scale: nếu bạn định kích thước view theo điểm (points) thay vì pixel mục tiêu, hãy đặt renderer.scale bằng scale thiết bị để đầu ra không bị mờ; nếu bạn định kích thước frame đúng bằng pixel mục tiêu (như ở trên), giữ scale ở mức 1.

Điểm cần lưu ý lớn nhất là những gì ImageRenderer không thể render. Nó render SwiftUI, không phải các view dựa trên UIKit/AppKit: web view, map view, trình phát media, xem trước camera, và một số control hệ thống sẽ hiện ra dưới dạng placeholder. Với một màn hình SwiftUI sạch và xác định, nó tuyệt vời — tức thì, headless, đúng kích thước hoàn hảo, chạy ở bất cứ đâu kể cả trên thiết bị thật. Với một màn hình nhúng platform view, bạn lại quay về Lựa chọn 1–2 để chụp đầu ra compositor thực.

  • Nhanh vượt trội. Vài mili giây mỗi ảnh, không cần khởi động simulator, không cần điều khiển UI. Rất tốt trong CI.
  • Đúng kích thước, một cách đơn giản. Định kích thước frame thành 1320 × 2868 (hoặc bất kỳ kích thước yêu cầu nào) và đó chính là đầu ra của bạn — không cần đoán mò "resize về App Store".
  • Bạn tự xây dựng trạng thái. Vì không có ứng dụng đang chạy, bạn truyền dữ liệu giả trực tiếp vào view — xác định, không cần launch argument gieo dữ liệu demo.
  • Chỉ SwiftUI. Các view dựa trên nền tảng render thành placeholder. Hãy kiểm tra màn hình trước khi tin tưởng nó.

Lựa chọn 4: các mã nguồn mở bọc quanh những cách trên

Hai dự án mã nguồn mở xuất hiện liên tục, giải quyết các vấn đề liền kề.

pointfree/swift-snapshot-testing là thư viện snapshot SwiftUI phổ biến nhất, và đáng để nói chính xác về mục đích của nó: nó được xây dựng cho kiểm tra regression, không phải để tạo ra tài nguyên cho cửa hàng. Lần chạy đầu tiên ghi lại một PNG tham chiếu và cố ý fail; các lần chạy sau so sánh với tham chiếu đó để phát hiện ngày một thay đổi padding hay font làm lệch giao diện của bạn.

// pointfree/swift-snapshot-testing — built for regression, not store assets.
import SnapshotTesting
import SwiftUI
import Testing

@MainActor
struct HomeSnapshotTests {
    @Test func home_iPhone16ProMax() {
        let view = HomeScreen()
        // First run records the reference PNG and FAILS on purpose;
        // later runs diff against it. To re-record: record: .all
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }
}

Bạn có thể tái sử dụng các ảnh tham chiếu đã ghi làm ảnh chụp màn hình thô, và các strategy .image(layout: .device(...)) render ở kích thước thiết bị thực — nhưng điều đó đi ngược lại tính nghiêm ngặt của công cụ (nó muốn khớp pixel hoàn hảo; khác biệt antialiasing giữa các máy gây ra lỗi không ổn định). Hãy dùng nó để bảo vệ giao diện của bạn khỏi regression, không phải làm pipeline ảnh chụp màn hình chính.

AppScreenshotKit (bởi shitamori1272) được xây dựng riêng cho việc này. Bạn khai báo một view SwiftUI với macro @AppScreenshot chỉ định lớp thiết bị và locale, bọc màn hình thực của bạn trong DeviceView để nó render bên trong khung thiết bị Apple, và xuất từ một hàm Swift Testing. Nó tổ chức đầu ra thành một cây thư mục gọn gàng Screenshots/<locale>/<device>/.

// AppScreenshotKit — declarative SwiftUI screenshots, runs under Swift Testing.
import AppScreenshotKit
import SwiftUI

@AppScreenshot(.iPhone69Inch(), options: .locale([Locale(identifier: "en_US")]))
struct HomeShot: View {
    var body: some View {
        DeviceView { HomeScreen() } // your real screen, wrapped in a device frame
    }
}

// In a test target:
import AppScreenshotKitTestTools
import Testing

@Test @MainActor
func exportScreens() throws {
    let out = URL(fileURLWithPath: "Screenshots")
    let exporter = AppScreenshotExporter(option: .file(outputURL: out))
    try exporter.export(HomeShot.self) // → Screenshots/en_US/iPhone_6_9_inch/...
}

Cũng đáng biết về các công cụ liên quan trong lĩnh vực này: các CLI như storescreens điều khiển toàn bộ pipeline App Store Connect (chụp XCUITest song song trên nhiều simulator, render có khung kèm chú thích, upload metadata qua API của Apple) từ một file cấu hình duy nhất. Chúng thực sự hữu ích nếu bạn muốn một chuỗi duy nhất, có thể lặp lại, thân thiện với CI. Chúng cũng mang theo quan điểm riêng về bố cục chú thích — đây chính là điểm mà một trình chỉnh sửa hình ảnh chuyên biệt thường thắng thế cho bề mặt marketing.

Phần mà không cách nào ở trên làm được: dựng hình + bản địa hóa 50 ngôn ngữ

Đây là đoạn văn rõ ràng và trung thực. Mọi lựa chọn ở trên đều tạo ra frame thô — giao diện ứng dụng của bạn ở độ phân giải thiết bị (hoặc, với AppScreenshotKit và các công cụ tương tự, một frame bên trong mockup thiết bị đơn giản). Carousel App Store thực sự chuyển đổi không phải là frame thô: đó là frame bên trong một mockup thiết bị có phong cách, trên nền, dưới một tiêu đề ngắn, thường dưới dạng chuỗi đa bảng, lặp lại trên mọi locale bạn nhắm đến. Bước dựng hình đó là việc của Mokbi. Bạn mang các frame đã chụp (từ XCUITest, ImageRenderer, simulator, bất cứ đâu), thả vào trình chỉnh sửa trên trình duyệt, thêm khung thiết bị và chú thích và nền, dịch chú thích sang đến 50 ngôn ngữ App Store chỉ một cú nhấp, và xuất hàng loạt ở mọi kích thước yêu cầu. Miễn phí để thiết kế với xem trước có watermark; xuất không watermark và xuất bản lên cửa hàng không giới hạn đi kèm gói đăng ký — Solo €29.99/mo (1 ứng dụng) hoặc Studio €49.99/mo (tối đa 5 ứng dụng), không có mua một lần. Để nói rõ ranh giới: Mokbi không chụp ảnh chụp màn hình nguồn — nó không chạy ứng dụng hay render view của bạn. Việc chụp thuộc về các công cụ Swift native ở trên; Mokbi dựng hình và bản địa hóa những gì các công cụ đó tạo ra.

Một quy trình SwiftUI kết hợp thực tế

  1. Chụp frame nguồn một cách native. Với các màn hình SwiftUI xác định, render chúng bằng ImageRenderer ở đúng kích thước pixel (Lựa chọn 3) — đây là cách nhanh nhất và không cần simulator. Với các màn hình có platform view hoặc khi bạn muốn trạng thái đang chạy thực, hãy viết một chụp bằng XCUITest (Lựa chọn 1) hoặc Swift Testing (Lựa chọn 2) và chạy trên một simulator lớp iPhone 6.9-inch và lớp iPad 13-inch.
  2. Bản địa hóa việc chụp nếu bạn cần. Thêm một cấu hình test plan cho mỗi ngôn ngữ để giao diện ứng dụng render theo từng locale, hoặc truyền một locale vào view bạn đưa cho ImageRenderer. Cách này bản địa hóa pixel ứng dụng — không phải tiêu đề marketing của bạn.
  3. Dựng hình carousel marketing. Thả frame vào Mokbi, thêm khung/chú thích/nền có phong cách, xây dựng chuỗi đa bảng. Đây là bước mà công cụ native không làm được.
  4. Dịch chú thích và xuất. Dịch bản sao tiêu đề một cú nhấp sang các locale App Store mục tiêu, rồi xuất hàng loạt mọi kích thước yêu cầu.
  5. Ở lần thay đổi giao diện tiếp theo, chụp lại và mở lại. Chạy lại renderer hoặc UI test, mở lại dự án Mokbi đã lưu, hoán đổi frame, xuất lại. Phần dựng hình và bản dịch được giữ nguyên.

Khi nào nên bỏ qua tự động hóa hoàn toàn

Nếu bạn ra mắt một đến ba lần một năm, việc xây dựng bất kỳ hệ thống chụp ảnh chụp màn hình nào — native hay Fastlane — sẽ tốn nhiều giờ hơn số giờ nó từng tiết kiệm được. Chạy ứng dụng trong iOS Simulator ở đúng lớp thiết bị, điều hướng đến từng màn hình, dùng lệnh chụp màn hình của simulator (nó chụp đúng độ phân giải thiết bị), và bạn đã có PNG sẵn sàng cho App Store trong mười phút. Sau đó dựng hình và bản địa hóa một lần rồi tiếp tục. Tự động hóa ở trên chỉ thực sự có lợi khi "chụp lại bằng tay" không còn là việc mười phút nữa — ra mắt thường xuyên, nhiều locale, hoặc nhiều ứng dụng trong danh mục. Hãy chọn công cụ phù hợp với nhịp phát hành, không phải những gì trông có vẻ nghiêm túc. Logic tương tự áp dụng trên các nền tảng khác; xem phiên bản React Native của vấn đề nàyđánh đổi Fastlane vs không cần code tổng quát hơn.

Dù bạn chụp bằng cách nào, hãy kiểm tra lại mục tiêu trước khi xuất: lấy đúng kích thước pixel và quy tắc định dạng (PNG/JPEG, RGB, không có kênh alpha) từ hướng dẫn kích thước ảnh chụp màn hình App Store. Apple từ chối kích thước lệch dù chỉ một pixel mà không có ngoại lệ, nên việc này đáng bỏ ra ba mươi giây.

Đọc tiếp

Mở trình chỉnh sửa →