LoginSignup
6
2

More than 3 years have passed since last update.

SwiftUIのPreview実装をそのまま使って、Screenshot撮影を自動化する

Posted at

はじめに

SwiftUIのPreview機能を活用して、実装したPreview用のコードをそのままScreenshot撮影自動化に利用できる方法の紹介です。
わざわざテスト用のコードを実装することなく、既存の記述をほぼそのままCIとして組み込めるので、かなり簡単に対応できると思います。
これを使って、画像diffから意図しないUI変更を検知したり、簡単なUIカタログのようなものを作ることもできます。

完成形

先に完成形を記載しておきます

Previewの実装

struct ContentView_Previews: PreviewProvider {
    // Previewableに適合したenumを定義して、previewプロパティで状態に応じたViewを返すだけです
    enum Context: String, Previewable {
        case red
        case green
        case blue

        var preview: some View {
            switch self {
            case .red:
                return ContentView()
                    .foregroundColor(.red)
            case .green:
                return ContentView()
                    .foregroundColor(.green)
            case .blue:
                return ContentView()
                    .foregroundColor(.blue)
            }
        }
    }

    static var previews: some View {
        // Previewableに定義してある便利関数で、各caseごとのViewをGroupで括った上で返却します
        Context.groupedAllContext
    }
}

Preview画面での実際の表示はこんな感じ

テストコードの実装

テスト用の実装はこれだけでOKです

class PreviewScreenshotSampleTests: FBSnapshotTestCase {
    override func setUp() {
        super.setUp()
        // ここは、今回利用したFBSnapshotTestCaseのための実装です
        self.recordMode = true
    }

    func test() {
        // Previewableに適合した型の、screenshotメソッドを呼び出すだけです。
        ContentView_Previews.Context.screenshot(self)
    }
}

画像はこんな感じで出力されます:rocket::rocket::rocket:
Previewで表示されていたものと同じ画面が撮影できているのがわかると思います。

上記の通り、 Previewable に適合した型の preview プロパティから、スクリーンショットを撮影したい状態のViewを返してあげるだけで、SwiftUIのPreviewとUnitTest両方から同じ状態の画面を参照することができました。

以下では仕組みの方の解説をします。

ライブラリの準備

今回は ios-snapshot-test-case を利用してスクリーンショットを撮影しました。
特にライブラリに依存した実装はないので、お好みのライブラリでスクリーンショットを撮影できるはずです。

ios-snapshot-test-case用の設定としてSchemeに以下の環境変数を設定します。
これにより、指定したディレクトリに画像が保存されます。

Preiewableの定義

実装はこれだけです。詳細はコメントに記述しています。

protocol Previewable: CaseIterable, Hashable, RawRepresentable {
    associatedtype Preview: View
    var preview: Preview { get }
}

// Groupを使ってPreiewを分けたり、Stackを使って縦に並べるための便利関数です。
// 無くても問題ありません
extension Previewable where Self.AllCases: RandomAccessCollection, Self.RawValue == String {
    static var groupedAllContext: some View {
        Group {
            ForEach(Self.allCases, id: \.self) {
                // Stringのenumで定義しておけば、case名がそのままPreiew内に表示されるようにしています
                $0.preview.previewDisplayName($0.rawValue)
            }
        }
    }

    static var stackedAllContext: some View {
        VStack {
            ForEach(Self.allCases, id: \.self) {
                $0.preview.previewDisplayName($0.rawValue)
            }
        }
    }
}
extension Previewable where Self.AllCases: RandomAccessCollection, Self.RawValue == String {
    static func screenshot(_ testCase: FBSnapshotTestCase) {
        // 定義されてある全てのケースを1つずつ撮影していく
        Self.allCases.forEach { (ctx) in
            ctx.screenshot(for: testCase)
        }
    }

    func screenshot(_ testCase: FBSnapshotTestCase) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        // previewプロパティから対象の状態のViewを生成します
        window.rootViewController = UIHostingController(rootView: self.preview)
        window.makeKeyAndVisible()
        let expectation = testCase.expectation(description: "")
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            // ここで任意のスクリーンショット撮影処理を実行します。
            // 今回はFBSnapshotTestCaseのメソッドを使うため、画面に表示されたぐらいのタイミングで、UIViewの型でSwiftUIの画面を取り出して受け渡します
            // identifierにrawValueを渡すことで、出力されるファイルの名前を定義したcaseの名前になるようにしています
            testCase.FBSnapshotVerifyView(window.rootViewController!.view, identifier: self.rawValue)
            expectation.fulfill()
        }
        testCase.wait(for: [expectation], timeout: 5.0)
    }
}

以上で必要な実装は終わりです。

サンプルコード

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2