Swift: addSubview とか register を複数の引数に対応させる

どうも!しょーです!

最近ちょっとしたファンができて(私"が"ファン)、日々に小さな光が指すようになりました。
アイドル応援してる人ってこういう気持ちなんかな、とちょっと不思議な感情が芽生えたのである。

さて、register とか addSubview を複数の引数に対応させるという話をしようか。

view を追加する時、view.addSubview(_:) をしたり、
カスタムセルとか作成した時、tableView.register(_:) するじゃないっすか。
この子たちは基本シングルでしか引数に当てれないじゃないっすか。
コードベースの開発やってると複数対応させて一気に追加したいと思ったりしませんか。

業務ではこれらをマルチに対応できるカスタムメソッドを作成して利用してるので、それの活用をメモしたーい。

addSubview の複数対応

これは別にメモるほどでもないな、、
ただのforEachですね、、

でもこれを書いておくだけで追加される subview がまとまってコードが綺麗になる。

extension UIView {
    func addSubviews(_ views: UIView...) {
        views.forEach(addSubview(_:))
    }
}

あと実は初めて文法知ったんですが、この引数の ”...”可変長引数(Variadic Parameters) というらしいですね。
機能としては配列とほぼ一緒みたいで、処理速度的にも大きな差はないみたいです。

そこまで大きな使い分けはなさそうですが、パラメータの個数が少なかったら可変長引数の方が見やすいんじゃね?っていう話がありました。

I think that if the parameters count is small,for example,less than 5,Variadic Parameter may be a better solution,because it is easy to read.
If the count of parameters is large. Array is better solution.

Variadic Parameters v Array Parameter [closed]

うんまぁ私は普通に配列で統一したい。

tableview.register の複数対応

こちらはまぁまぁ初期設定多めです。
ですが一回書いておくとあとあと楽なんじゃないでしょうか。

private func defaultCellIdentifier<T: NSObject>(_ cellClass: T.Type) -> String {
    return String(describing: cellClass)
}

// MARK: - CellReusable
protocol CellReusable {
    static var cellIdentifier: String { get }
}

extension CellReusable where Self: UITableViewCell {
    static var cellIdentifier: String { return defaultCellIdentifier(self) }
}

// MARK: - CellReusableXib
protocol CellReusableXib: CellReusable {
    static var nibName: String { get }
    static var nibBundle: Bundle? { get }
}

extension CellReusableXib where Self: UITableViewCell {
    static var nibName: String { return cellIdentifier }
    static var nibBundle: Bundle? { return nil }
}

// MARK: - UITableView
private func cast<T, U>(_ object: T) -> U {
    guard let out = object as? U else {
        fatalError("\(object) cannot cast to \(U.self)")
    }
    return out
}

extension UITableView {
    func register(_ cellClasses: (UITableViewCell & CellReusable).Type...) {
        for cellClass in cellClasses {
            if let cellClass = cellClass as? CellReusableXib.Type {
                register(UINib(nibName: cellClass.nibName, bundle: cellClass.nibBundle),
                         forCellReuseIdentifier: cellClass.cellIdentifier)
            } else {
                register(cellClass, forCellReuseIdentifier: cellClass.cellIdentifier)
            }
        }
    }

    func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T where T: CellReusable {
        return dequeueReusableCell(for: indexPath, as: T.self)
    }

    func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath, as cellClass: T.Type) -> T where T: CellReusable {
        return cast(dequeueReusableCell(withIdentifier: cellClass.cellIdentifier, for: indexPath))
    }
}

使い方

使い方としてはこんな感じ。


// Xibなしには CellReusable を継承
class CustomCellA: UITableViewCell, CellReusable {
    ...
}
class CustomCellB: UITableViewCell, CellReusable {
    ...
}

// Xibありには CellReusableXib を継承
class CustomCellC: UITableViewCell, CellReusableXib {
    ...
}

// SomethingViewController
class ViewController: UIViewController {

    let tableView = UITableView()

    func initTableView() {
        // まとめてできちゃうよ
        tableView.register(CustomCellA.self, CustomCellB.self, CustomCellC.self)

        // 普通ならこう
        tableView.register(CustomCellA.self, forCellReuseIdentifier: "CustomCellA")
        tableView.register(CustomCellB.self, forCellReuseIdentifier: "CustomCellB")...

    }
}

extension SettingViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // カスタムメソッドのdequeueReusableCellを使う
        let cell = tableView.dequeueReusableCell(for: indexPath) as CustomCellA
        return cell
    }
}

のように使用できるはずです。
普通なら register(_:) をセルごとに行わないといけないのですが、これだと複数に対応できます。


さてデコードしていこう。
いうて大した説明はないですが、、

メインメソッドはここ。

extension UITableView {
    // CellReusable というプロトコルを作成して、該当した物を配列で与えられるようにする
    func register(_ cellClasses: (UITableViewCell & CellReusable).Type...) {
        for cellClass in cellClasses {
            // XibありのカスタムセルとXibなし両方のプロトコルを作成して処理を分ける(XibはUINib()が必要なので)
            if let cellClass = cellClass as? CellReusableXib.Type {
                // Xibありタイプなら、プロトコルのプロパティでString等の値を与えられるようにする
                register(UINib(nibName: cellClass.nibName, bundle: cellClass.nibBundle),
                         forCellReuseIdentifier: cellClass.cellIdentifier)
            } else {
                // Xibなし
                register(cellClass, forCellReuseIdentifier: cellClass.cellIdentifier)
            }
        }
    }
}

protocol extension を用いることでプロパティを初期化します。
そうすることで、registerメソッド内で値の取得ができます。

このような拡張メソッドを作成しておくとちょっと便利です。
あと応用すればCollectionViewにも使える!

まとめ

addSubviews(_:) の方はサクッとしたメソッドですが、register(_:) の方はこれだけでもかなりSwiftの文法が練り込まれてますよね。
すごいSwiftyというんですかね(よう知らん)。
where Self くらいはギリギリ使うかもしれませんが、ジェネリクスはまだ使い慣れていないので勉強していきたいなぁと思いました。

ハイハイ!
お疲れっす!