Xcode Previewsを活用したXibに頼らないView開発

## はじめに
私は今までUIKitのレイアウト開発時にIBDesignableとIBInspectableを用いたカスタムViewを愛用しており、xib上で表示を確認しながら開発をしていました。
以前からIBDesignableはdeprecatedになっていましたが、Xcode16ではIBInspectableがInterface Builderからアクセスできなくなってしまいました。
```
IBInspectable properties are no longer accessible in Interface Builder. 
Current values in IB Documents are still compiled into the NIBs and used at runtime. (135140474) (FB15011501)
```
https://developer.apple.com/documentation/xcode-release-notes/xcode-16_1-release-notes

IBDesignableもIBInspectableもまだ一応機能しているため、Runtime Attributesに
Attributes inspectorで設定していた値を直接設定してあげることで今まで通り使うことは出来ます。
ただ、手間や将来的な廃止リスクを考慮すると今後も使い続けるかは悩ましいところです。

IBDesignable/IBInspectableを使った開発では主に以下のことができました。
1. カスタムViewを用いたレイアウトのリアルタイムプレビュー
1. Interface Builder上から設定した値をカスタムViewで使うことができる

1のリアルタイムプレビューに関しては動作不安定な部分もありましたが、やはりあると便利でした。
2についても個人的には非常に便利だと思って使っていました。
これを用いて枠線や、影、角丸など基本的なものからGif再生用のViewに対して再生速度や繰り返しの設定などViewの振る舞いに関することを隠蔽することに役立ってきました。こうすることによってViewはデータバインディングのみに集中できるのでコード量も減り、シンプルで保守もしやすくまとまりのいいコードにできてました。

## UIKitの新たなレイアウト開発アプローチの必要性
そんなわけで、今後はUIKitを使ったレイアウト開発を見直す必要が出てきましたが、代替手段を見つけることは出来ませんでした。
そこで、[Xcode Previewsを用いたUIKitベースのプロジェクトの開発効率化](https://engineering.mercari.com/blog/entry/2019-12-13-155700)を元に以下の方針で開発を進めます。

* xibを使わないコードベース開発：レイアウトや動作をコードで記述
* Preview用のSwiftUIラッパー作成：UIKitコンポーネントをプレビュー可能に
* 外部から状態を注入可能な設計：表示内容をインターフェースで切り替え可能に

これによりレイアウトのリアルタイムプレビューを復活させることができます。プレビューの安定性や適用範囲の拡大といった面でIBDesignable/IBInspectableを使った開発に比べてパワーアップしています。さらにxibファイルを使わなくなるので、レビューがしやすくなるというメリットもあります。
一方、コード量の増加やカスタムViewによって共通化できていた部分は諦める必要があります。

## Viewの状態を外部から差し込めるプロトコルの用意
まずは、Xcode Previewsを用いてリアルタイムプレビューの効果を最大化させるために、外部から表示内容を切り替えらるインターフェースを用意します。

```Swift
public protocol InputAppliable {
    associatedtype Input: Hashable
    func apply(input: Input)
}
```
これを新しく作成するViewに準拠させます。
```Swift
extension ExampleView: InputAppliable {
    struct Input: Hashable {
        let titleText: String
    }
    
    func apply(input: Input) {
        titleLabel.text = input.titleText
    }
}
```
私はViewのプロパティを部分更新させなければいけないような状況があまりないため、Inputをstructで実装することにしましたが、プロパティの部分更新が多い場合や状態遷移を明確にしたい場合は、引用記事のようにenumを選択するのが良いと思います。

## プレビュー用のラッパーを用意
Xcode PreviewsはSwiftUIで使える機能のため、プレビュー用のSwiftUIラッパーを用意します。
```Swift
struct UIViewPreview<View: UIView>: UIViewRepresentable {
    let view: View
    
    init(_ builder: @escaping () -> View) {
        view = builder()
    }
    
    func makeUIView(context: Context) -> UIView {
        view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {}
}

```
## コードベースのプレビュー可能なUIViewの作成
ここまでで準備が整ったので、実際UIViewを作ってプレビューしていきます。
```Swift
final class ExampleView: UIView {
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 14, weight: .bold)
        label.textColor = .black
        return label
    }()

    private let subtitleLabel: UILabel = {
        ... // 中略
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private extension ExampleView {
    func setupView() {
        addSubview(titleLabel)
        addSubview(subtitleLabel)
        
        NSLayoutConstraint.activate([
            titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16),
            subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
        ])
    }
}

extension ExampleView: InputAppliable {
    ... // 前述のため略
}

#Preview("Default", traits: .sizeThatFitsLayout) {
    UIViewPreview {
        let view = ExampleView()
        view.apply(input: ExampleView.Input(
            titleText: "Hello, World!",
            subtitleText: "Welcome to the preview"
        ))
        return view
    }
}
```

![スクリーンショット 2024-12-15 1.25.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/3f47fb9e-2442-7f84-a6ce-3f1e905efcbd.png)

### (応用編) UICollectionViewCellのプレビュー
ここまででUIKitをコードベースで構築し、Xcode Previewsでリアルタイムプレビューしながら開発できることができました。
次は使う機会が多いであろうUICollectionViewCellをXcode Previewsを使って開発してみたいと思います。
ただ、ここで一つ問題があります。
CollectionViewCellの場合ラベルなどを可変長にする場合があると思いますが、先ほど作ったUIViewのラッパークラスの場合、その動作を確認することができません。

そこで、新たにUICollectionViewをSwiftUIでラップするためのクラスを作りました。
コード量が多いので詳しくみたいという方は[Git](https://github.com/gdate/GDateKit/blob/develop/Examples/GDateKitExample/GDateKitExample/ExampleCell.swift)の方を参照ください。
要点をまとめると以下要件のUICollectionViewラッパーを作ります
* 動的データのプレビュー: アイテムリストを受け取り、動的にセルを生成
* カスタムセルの登録: ジェネリクスを用いて汎用的に利用可能にする
* レイアウト設定: 引数でレイアウトを受け取れるようにする

次に表示確認用のUICollectionViewCellを作ります
こちらも全体が見たい場合は[Git](https://github.com/gdate/GDateKit/blob/develop/Examples/GDateKitExample/GDateKitExample/ExampleCell.swift)の方を確認してください。
```Swift
final class ExampleCell: UICollectionViewCell {
    private let titleLabel: UILabel = {
        let label = UILabel()
        ... // 中略
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private extension ExampleCell {
    func setupView() {
        ... // 中略（layerの表示設定）
        
        contentView.addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
            ... // 中略
        ])
    }
}

extension ExampleCell: InputAppliable {
    struct Input: Hashable {
        let titleText: String
    }
    
    func apply(input: Input) {
        titleLabel.text = input.titleText
    }
}
```
先ほど作ったCollectionViewのラッパークラスにこのCellとアイテムリスト、レイアウトを渡してあげることで、実際のCellの動作を再現することができます。

```Swift
// ExampleCell.swift
... // 略

@available(iOS 17.0, *)
#Preview(traits: .sizeThatFitsLayout) {
    let items = [
        ExampleCell.Input(titleText: "Short"),
        ExampleCell.Input(titleText: "This is a medium length text."),
        ExampleCell.Input(titleText: "Here is a longer text that you can use to see how it looks."),
    ]
    
    let layoutProvider: () -> UICollectionViewCompositionalLayout = {
        UICollectionViewCompositionalLayout { _, _ in
            ... // 中略
            return section
        }
    }
    UICollectionViewPreview(
        items: items,
        cellProvider: { ExampleCell() },
        layoutProvider: layoutProvider,
        cellConfigurator: { cell, input in
            cell.apply(input: input)
        }
    )
    .frame(height: 69)
}

```
![スクリーンショット 2024-12-15 23.00.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/132fb11b-bccf-282c-0656-1c3073d11272.png)

## 感想
UIKitのViewをXcode Previewsを使い、リアルタイムプレビューをしながら開発を実際にしてみましたが、今のところかなりいいなと感じています。
アプリをビルドすることなくレイアウトの確認ができ、また、様々なViewの状態を一度に確認できるのは開発効率が向上しました。
ただ、Interface Builderを使わずにコードでAuto Layoutを設定するのは慣れが必要だなと感じています。
引用記事にも書いていましたが、極力StackViewを使い、Auto Layoutの指定は最小限に抑える
ことが重要だと実感しました。
そうすることで、コード量も減らせますし、レビューの際の見通しもよくなりました。
あとは、Viewをどの単位で分割するべきかというのも悩みどころです。個人的にはStackViewでまとめられる最小単位で分割し、土台となるViewではそれらを呼び出し、Auto Layoutの指定をしてあげるような分け方にすると比較的コード量が抑えられ、View単位での責務の分離もしやすいと感じています。