SwiftUIのPreviewを使ってUIドキュメント生成を自動化する

SwiftUIのPreviews機能を利用するとシミュレータを起動することなくViewの動作を確認できます。

> Generate dynamic, interactive previews of your custom views.

[Previews in Xcode | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/previews-in-xcode)

プレビューを利用して開発する中で、プレビューのスナップショットが一覧で見られると便利だと思ったので、それを行うライブラリを作ってみました。

※ SwiftUIのプレビュー機能を利用しているViewが対象となります。プレビューされていないViewのスナップショットは作成されません。

## つくったもの

[yyokii/UIPreviewCatalog](https://github.com/yyokii/UIPreviewCatalog) というものを作りました。

`createCatalog(previewItems:)`を実行すると以下のようにPreviewの画像一覧とそれらをまとめたマークダウンファイルが出力されます。

<img width="400" alt="outputs" src="https://user-images.githubusercontent.com/20992687/134666975-f0b173cc-8367-404b-9c6a-89c079d27752.png">
<img width="400" alt="output_markdown" src="https://user-images.githubusercontent.com/20992687/134667079-bb2d7de4-444b-474a-a5da-21ed8d1f499e.png">

これはライブラリであり、対象のアプリへの導入が必要です。
しかし、もしこれがCLIツールやMacアプリとして提供できれば導入が1度で終了します。
そのような方針も検討したのですが、実現方法が分からなかったので今回は断念しました。

## 開発の動機

開発において以下のように感じることはないでしょうか。

* 仕事で新しいプロジェクトがはじまったけど、このアプリではどんなViewが使われているのだろう

* オープンソースのアプリをコードリーディングしてView作成の参考にしたいから、どんなViewが作られているかざっとみてみたいなあ

これらはアプリを実際に動かせばだいたい解決します。
しかし手動ですと漏れが生じることがありますし、操作を面倒に思うかもしれません。

そこで、SwiftUIでプレビューをしているならそれを一覧化すれば役に立つのではないかと思ったので作ってみようと思いました。

つまり、「アプリで使われてるViewをざっと見られる」ものをコードで作り、

* プロジェクトのキャッチアップ
* 同じようなViewを作ってしまうことの防止
* 新しくViewを作成する際に似たようなViewが既にあればそれに気付きやすくなること

などに役立てようと思いました。

同じような目的でUIに関するドキュメントを書類としてまとめていたり、デバッグビューを作成しているプロジェクトもあるでしょう。素晴らしいです。  
本ライブラリはそこまではできていないけど、簡易的なものを出力してみたい、といったユースケースに合うものと思います。

なお、このアイデアはとあるコミュニティで輪読会のイベントをしており、その議論の中で出たものを参考にしています。
元々は「各画面とその画面で利用されているクラス等の情報がマークダウンで出力されるといいよね」というものでしたが、SwiftUIのプレビュー情報に限ってなら比較的簡単にできそうと思い、元のアイデアを少し変更して作りました。
輪読会のイベントとそこでの議論がなければ開発に至りませんでした。参加されていた人には感謝しています:pray:

## 使い方

大きく3つの手順があります。

1. ライブラリの導入
2. `PreviewItem`配列の生成
3. スナップショットとMarkdownファイルの生成

### 1. ライブラリの導入

アプリの機能に影響しないものなので、テストターゲットに依存関係を追加します。
ユニットテストで実行が可能です。

[Swift Package Manager](https://swift.org/package-manager/#conceptual-overview)を利用してインストール可能です。  
Xcodeや`Package.swift`マニフェストを通じてインストールできます。

### 2. `PreviewItem`配列の生成

このライブラリに`PreviewItem`という`struct`があり、それがスナップショットを作成する対象となります。  
以下のように自分で定義することもできますが、既存のコード生成ツールを使用して簡略化することもできます。  
作成したファイルはテストターゲットに含まれれるようにします。

```swift
import SwiftUI
import UIPreviewCatalog
@testable import DemoUIPreviewCatalog

let previewItems: [PreviewItem] = [
    .init(name: "BlueView_Previews", previews: BlueView_Previews._allPreviews),
    .init(name: "ContentView_Previews", previews: ContentView_Previews._allPreviews),
]
```

#### [Sourcery](https://github.com/krzysztofzablocki/Sourcery)を利用してコード生成

すでに完成しているアプリにおいて、上記のように手動で`PreviewItem`配列を作成するのは面倒です。そこで、[Sourcery](https://github.com/krzysztofzablocki/Sourcery)を利用して`PreviewProvider`に準拠している`struct`を抽出し、`PreviewItem`配列を生成してみます。

まず[Sourcery](https://github.com/krzysztofzablocki/Sourcery)をインストールします。  
そしてソース生成用のテンプレートファイルを作成します。  
[PreviewItem.stencil](https://github.com/yyokii/DemoUIPreviewCatalog/blob/main/PreviewItem.stencil)がテンプレートファイルですので、こちらの中身と同じものを作成してください。

そして[Sourcery](https://github.com/krzysztofzablocki/Sourcery)のコマンドを利用してコードを生成します。  
以下にサンプルを示します。  

```
$ sourcery \
   --sources /Users/{Foo}/Desktop/DemoUIPreviewCatalog/DemoUIPreviewCatalog \
   --templates /Users/{Foo}/Desktop/PreviewItem.stencil   \
   --output /Users/{Foo}/Desktop/DemoUIPreviewCatalog/DemoUIPreviewCatalogTests \
   --args mainTarget=DemoUIPreviewCatalog
```

`--sources` : ソースの解析対象のパスを指定します。ここではアプリのメインターゲットが指定されています。

`--templates`: テンプレートファイルのパスを指定します。先ほど作成したものを利用します。

`--output` : 生成されたコードを配置するパスを指定します。テストターゲットに含まれるようにしています。

`--args` : 任意の変数を与えます。生成されたファイルはテストターゲットに配置されますが、メインターゲットのPreview情報を利用しているのでその参照を解決するために`@testable import DemoUIPreviewCatalog`と記述する必要があります。従ってここではアプリのメインターゲットのモジュール名を指定しています。

これを実行することで次のようなファイルが生成されます。  

```swift
import SwiftUI
import UIPreviewCatalog
@testable import DemoUIPreviewCatalog

let previewItems: [PreviewItem] = [
    .init(name: "BlueView_Previews", previews: BlueView_Previews._allPreviews),
    .init(name: "ContentView_Previews", previews: ContentView_Previews._allPreviews),
]
```

### 3. スナップショットとMarkdownファイルの生成

XcodeのEdit Schemeで、環境変数を設定します。
Name : `PREVIEW_CATALOG_PATH`  
Value : 画像とマークダウンの出力先です。例えば、 `$(SOURCE_ROOT)`。  

<img width="500" alt="add_environment_variable" src="https://user-images.githubusercontent.com/20992687/134667267-edf3290e-1790-47f0-8858-e8f78b057c5e.png">

スナップショットとMarkdownファイルを出力します。  
下記のように`createCatalog(previewItems:)`を実行することでファイルが出力されます。

```swift
func testOutputUIPreviewCatalog() {
    let catalog = UIPreviewCatalog(config: .defaultConfig)
    do {
        try catalog.createCatalog(previewItems: previewItems)
    } catch {
        print(error.localizedDescription)
        XCTFail()
    }
}
```

以上の内容は[README](https://github.com/yyokii/UIPreviewCatalog/blob/main/README.md#usage)にも記載しています。

## 課題

現状アニメーションがついているViewに対して実行すると、空白になってしまいます。
もしアイデアがありましたら、コメントやプルリクエスト頂けると嬉しいです！
[Animation Support · Issue #6 · yyokii/UIPreviewCatalog](https://github.com/yyokii/UIPreviewCatalog/issues/6)

## おわりに

本内容が誰かの参考になると幸いです。

良いなと思ったらスター頂けると嬉しいです🌟
[yyokii/UIPreviewCatalog](https://github.com/yyokii/UIPreviewCatalog)