概要
UITableViewでUIを組んでいて、例えば検索キーワードなどをタグのUIにして並べるようなときに使えるテクニックを説明します。
タグUIの実装は、isScrollEnabledをfalseにしたUICollectionView
で実装しています。
これはあくまで一例なので、もっといい方法があればぜひコメントいただければと思います。
気をつける点
ただUICollectionViewのUIをTableViewに持たせるだけなのであれば比較的簡単かと思われますが、iOSは画面回転などでCollectionViewの行数が変わり、当然CollectionViewのサイズ自体が変わります。
iPadに関してはSplit Viewでの表示やSlide Overでの表示でもCollectionViewサイズが変わります。
それらのUIの各種変更にも対応する形で実装していきます。
完成図
全体がUITableViewで、UITableViewの中にUICollectionView持ちのセルが含まれています。
縦 | 横 |
---|---|
UICollectionViewを持つUITableViewCellの実装
タグ表示用CollectionViewを持つUITableViewCellを実装します。
以下は実装のポイントです。
必要なプロパティの準備
以下のプロパティを持ちます。
private var observer: NSKeyValueObservation?
var collectionView: UICollectionView!
左寄せ用のUICollectionViewFlowLayoutの用意
CollectionViewに左寄せ用のUICollectionViewFlowLayoutを適用します。
https://stackoverflow.com/questions/22539979/left-align-cells-in-uicollectionview
有志が各種左寄せ用レイアウトを用意しているので、それを使いましょう。
このとき、CollectionViewFlowLayoutのestimatedItemSize
はUICollectionViewFlowLayout.automaticSize
に設定しておくことを忘れないでください。
systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority)のオーバーライド
func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
をオーバーライドして、collectionView.collectionViewLayout.collectionViewContentSizeを返します。
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return collectionView.collectionViewLayout.collectionViewContentSize
}
TableViewのcellForRowAt用のデータ処理時にcontentSizeのobserveを作成する
例えばconfigure
などのデータ処理用メソッドを用意して、func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
の実行時に呼び出した場合、configure内部でobserverを作成し、その後データ反映処理を行います。
observerはCollectionViewのContentSizeが変化するたびに動作します。
ContentSizeが変わったら、CollectionViewを最新の高さに対応するため、tableViewのreloadData()をコールします。
このとき、tableViewのcellForRowAtは仕組み上多重に動作するので、データをチェックして、違うデータの時のみCollectionViewのreloadData()を実行します。
private var array: [String] = []
func configure(array: [String]) {
observer = collectionView.observe(\.contentSize, options: [.new]) { collectionView, change in
if (change.newValue?.height) != nil {
// コンテンツのサイズが画面回転やデータによって変更するたび、TableViewを再描画する
self.tableView?.reloadData()
}
}
// データが前回のものと同じ場合、collecviewViewのreloadDataを行わない
if self.array != array {
self.array = array
collectionView.reloadData()
}
}
// UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
array.count
}
// https://stackoverflow.com/questions/15711645/how-to-get-uitableview-from-uitableviewcell
extension UITableViewCell {
var tableView: UITableView? {
var view = superview
while let v = view, v.isKind(of: UITableView.self) == false {
view = v.superview
}
return view as? UITableView
}
}
prepareForReuse
のときにobserverをnilにしておきます。
override func prepareForReuse() {
super.prepareForReuse()
observer = nil
}
まとめ
この記事では、タグ表示用のCollectionView入りUITableViewCellに対して全ての実装を行うことで実現を行なっていますが、ポイントとしては2点あり、
-
systemLayoutSizeFitting
でcollectionView.collectionViewLayout.collectionViewContentSize
を返す - collectionView.contentSizeが変わったことを何らかの方法で検知し、
TableView.reloadData()
を行う
ということができれば、今回の実装に拘らなくともタグを並べての表示が可能かと思います。