UIKitをSwiftUIみたいにリアルタイムプレビューしたい！ - SwiftUIを知らない人のためのXcode Previewsの使い方

# UIKitでも使えるリアルタイムプレビュー

SwiftUIではXcode11から追加されたXcode Previewsという機能を使って、コードの変更結果をリアルタイムでプレビューすることができます。
実はこの機能は、UIKit（UIView/UIViewController）でも利用することができます。

この記事では、SwiftUIまだ触ってないよって人向けに、UIKitをリアルタイムプレビューする方法を解説します。
この記事で紹介しているコードは[こちら](https://github.com/turara/UIKitXcodePreviewsSample)においてあります。

## リアルタイムプレビューのイメージ

ViewController上のTableViewの表示をデータを変更しながらチェック
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499615/8a8710ae-554e-5819-1c34-ea1fa16d2531.gif">

カスタムしたTableViewCellの表示をいろんなパターンで同時にチェック
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499615/cebbda94-9d02-648b-b4a1-11ae75c9e461.png">

# 前提

Xcode Previewsを使うためにはデプロイメントターゲットをiOS13以降する必要があります。
iOS13より前をターゲットにしているプロジェクトに取り入れる方法については、[Xcode Previewsを用いたUIKitベースのプロジェクトの開発効率化](https://engineering.mercari.com/blog/entry/2019-12-13-155700/)が参考になります。

## 環境

- Xcode 11.6
- Swift 5.2.4
- デプロイメントターゲット 13.0

# UIViewをプレビューする

ここでは例として、カスタムTableViewCellをプレビューできるようにしていきます。
データを受け取ってテキストと画像をセットする`configure`メソッドを追加したものです。

```swift:MyTableViewCell.swift
final class MyTableViewCell: UITableViewCell {
    static let reuseIdentifier = "\(MyTableViewCell.self)"
    
    func configure(with prop: CellProp) {
        textLabel?.text = prop.title
        imageView?.image = UIImage(systemName: prop.imageType.rawValue)
    }
}
```

## SwiftUIのViewでUIViewをラップする

Xcode PreviewsはSwiftUIのView用に作られているので、`MyTableViewCell`をSwiftUIのViewでラップする必要があります。
UIViewをラップするためのプロトコルとして、`UIViewRepresentable`というものが用意されています。

```swift:MyTableViewCell.swift
import SwiftUI

// SwiftUIのViewはstructで定義する
struct MyTableViewCellWrapper: UIViewRepresentable {
    ...
}
```

`UIViewRepresentable`に準拠させるには、`makeUIView/updateUIView`の二つのメソッドを定義します。
また、プレビュー時に使用するデータをセットするために、プロパティ`var prop: CellProp`を追加しています。

```swift:MyTableViewCell.swift
struct MyTableViewCellWrapper: UIViewRepresentable {
    var prop: CellProp
    
    // 初期化メソッド
    func makeUIView(context: Context) -> MyTableViewCell {
        // 単純に初期化して返す
        MyTableViewCell(style: .default, reuseIdentifier: MyTableViewCell.reuseIdentifier)
    }

    // SwiftUI側から更新がかかったときに呼ばれるメソッド
    func updateUIView(_ cell: MyTableViewCell, context: Context) {
        // 更新用のメソッドはupdateUIView内で呼ぶ
        cell.configure(with: prop)
    }
}
```

## プレビュー用のstructを追加する

最後のステップとして、Xcode Previewsがプレビューを表示するときの設定を読み取れるように、`PreviewProvider`プロトコルに準拠したstructを定義します。

このプロトコルに準拠させるために、`static var previews: some View`を追加します。
`View`はSwiftUIにおける基本的なViewを表すstructです。`some`はSwiftUIのViewであれば、なんでもいいよという意味です。

```swift:MyTableViewCell.swift
struct MyTableViewCell_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MyTableViewCellWrapper(prop: ("Pencil", .pencil))
            MyTableViewCellWrapper(prop: ("Scribble", .scribble))
            MyTableViewCellWrapper(prop: ("Scissors", .scissors))
        }
        .previewLayout(.fixed(width: 320, height: 50))
    }
}
```

`Group`は主にプレビューのために使われるSwiftUIのViewで、クロージャーに複数のViewを並べることができます。
`previewLayout`メソッドでサイズを指定してやることで、次の画像の様にプレビュー時のサイズを調整することができます[^1]。

[^1]: いろいろなサイズでプレビューしたい場合は、Group内のそれぞれのViewに対して呼ぶこともできます。

Option+Command+リターンを入力（またはGUIからCanvasをクリック）すると、プレビュー用の画面が開きます。編集中のファイルに、`PreviewProvider`プロトコルに準拠したstructがある場合は、そのプレビューが表示されます[^2]。

[^2]: 自動でプレビューが始まらない場合は、右上のOption+Command+Pを入力（または右上の"Resume"ボタンをクリック）します。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499615/5ff72fe9-ee93-cbd9-7a00-5dc60322037e.png"> <img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499615/cebbda94-9d02-648b-b4a1-11ae75c9e461.png">
MyTableViewCellWrapperに渡す`prop`の値やプレビューサイズを編集すると、リアルタイムでプレビューが更新されます。
また、プレビューの右に表示されている再生ボタンを押すと、"Live Preview"がはじまり、タップイベントやスクロールなどのユーザーインタラクションをプレビュー画面上でテストすることができます[^3]。

[^3]: ただし、`print`関数などを実行してもコンソールに表示する機能はまだないようです。

# UIViewControllerをプレビューする

UIViewControllerもUIViewとほとんど同じ手順でプレビュー可能です。
まずは、`UIViewControllerRepresentable`に準拠したstructでUIViewControllerをラップします。

```swift:ViewController.swift
struct ViewController_Wrapper: UIViewControllerRepresentable {
    // プレビュー用のデータを返すスタブ
    var presenter: PresenterStub
    
    // 初期化メソッド
    func makeUIViewController(context: Context) -> ViewController {
        let vc = ViewController()
        vc.presenter = presenter
        presenter.view = vc
        return vc
    }
    
    // SwiftUI側から更新がかかったときに呼ばれるメソッド
    func updateUIViewController(_ vc: ViewController, context: Context) {
        // 更新用のメソッド
        presenter.didPushFetchButton()
    }
}
```

今回はプレビュー用のデータを渡しやすいように、ViewControllerが表示するデータは`PresenterInput`というプロトコルで管理するようにしています。

```swift:Presenter.swift
protocol PresenterInput {
    var data: [CellProp] { get }
    func didPushFetchButton()
}
```

```swift:ViewController.swift
final class ViewController: UIViewController {
    var presenter: PresenterInput!
    ...
}
```

`PreviewProvider`に準拠したstruct内でViewを初期化する際に、プレビュー用のデータを返すスタブを注入します。
このように実装することで、プレビューしたいデータごとにスタブを用意して、複数のデータについての表示を同時に確認することも可能です。

```swift:ViewController.swift
struct ViewController_Wrapper: UIViewControllerRepresentable {
    // プレビュー用のデータを返すスタブ
    var presenter: PresenterStub
    ...    
}

struct ViewController_Previews: PreviewProvider {
    static var previews: some View {
        let presenter = PresenterStub()
        return ViewController_Wrapper(presenter: presenter)
    }
}
```

次の画像は、`PesenterStub`内のコードを変更したときに、リアルタイムで表示が変わっている様子です。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499615/8a8710ae-554e-5819-1c34-ea1fa16d2531.gif">

# おわりに

Viewのレイアウトをコードでやっちゃう派の人は、Xcode Previewsの利点を最大限に活用できるのではないでしょうか。
ストーリーボード派の方も、来るSwiftUI時代に備えて、この機会に一度チャレンジしてみてはいかがでしょうか！

本記事で紹介したコードは[こちら](https://github.com/turara/UIKitXcodePreviewsSample)に置いてあります。参考にしてみてください。

# 参考

[Xcode Previewsを用いたUIKitベースのプロジェクトの開発効率化](https://engineering.mercari.com/blog/entry/2019-12-13-155700/)
[Introducing SwiftUI](https://developer.apple.com/tutorials/swiftui/)