GenericなUITableViewCellを使う


P.S この記事の内容は古くなりました。

iOS14にて、よく利用するセルのパターンとそのカスタマイズのためにUIContentConfigurationというオブジェクトが提供されました。

まずそちらを参考にしてください。

まえがき

TwitterのTLで少し前に「UITableViewCellのサブクラスを毎回作るのではなく、GenericsなTableViewCellにUIViewを載せて対応する」という方針を採用されておられる方がいて、良いなぁと思ったので作ってみました。

やったこと

GenericなUITableViewCellをつくる/つかう

GenericCell.swift
@dynamicMemberLookup public class GenericTableViewCell<View : UIView>: UITableViewCell {

    public let customView: View = .init(frame: .zero)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)

        commonInit()
    }

    public subscript<U>(dynamicMember keyPath: ReferenceWritableKeyPath<View, U>) -> U {
        get {
            customView[keyPath:keyPath]
        }

        set {
            customView[keyPath:keyPath] = newValue
        }
    }

    private func commonInit() {
        contentView.addSubview(customView)

        customView.translatesAutoresizingMaskIntoConstraints = false
        customView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        customView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
        customView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        customView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
    }

}

この構造のメリット

UITableViewCellをサブクラスしてViewを構築していく時の問題として、例えば表示内容は全く同じなのにUICollectionViewを利用した別の画面を作る必要が出た時に移行がすごく面倒です。

なので、普段からUIViewベースでViewを作成してGenericなTableViewCellに載せる構成にしておくことで、プロジェクトの変更・拡張に対してロバストです。

この構造のデメリット

Viewスタックが一層余分に増え、そのままではアクセスもcustomViewプロパティ経由になるので少し冗長になります。

なので、後者の問題を消すためにdynamicMemberLookupを採用し、直接内部Viewへのアクセス可能にしています。

使い方

TableViewへのセルの登録

Register.swift
tableView.register(GenericTableViewCell<YourCustomView>.self, forCellReuseIdentifier: "Cell")

セルの表示

Use.swift
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! GenericTableViewCell<YourCustomView>

CollectionViewCellでは?

CollectionViewCellでも同じような仕組みを作る事ができます。実装はほぼ変わりません。

追記

コメントで頂いたのですが、この記事では

public let customView: View = .init(frame: .zero)

としていますが、これではNib経由でのUIViewのインスタンス化が出来ません。

Nib経由でのインスタンス化に対する柔軟性が欲しい場合は、<View: UIView>とせずに生成は別の具体型に任せるFactoryMethodのようなデザインにすると良いでしょう。

詳しくはコメント欄をチェックしてみてください!