こんな感じのテーブルを作る必要があったので、手法を模索しました。
基本 テーブルビューの構造
テーブルビューの構造は下記のようになっています。
- テーブルヘッダー
- セクション
- セクションヘッダー
- セル
- セクションフッター
- テーブルフッター
スクロールに応じて表示される内容は下記のようにかわります。
1.テーブルヘッダー+セクションヘッダー+セクションフッター
2.セクションヘッダー+セクションフッター
3.セクションヘッダー+セクションフッター+テーブルフッター
テーブルヘッダーを常に表示させるUIを作成する場合、標準の振る舞いでは実装出来ないため、一番綺麗な方法が無いかを模索しました。
固定ヘッダーの実装方法はいくつかありますが、大きく次の2パターンです。
パターン1
1.contentInsetで余白を設定する
2.ヘッダービューはTableViewの子要素に設定する
3.余白の位置にヘッダーが来るようにスクロールに応じて位置/サイズを調節する
self.tableView.addSubview(self.headerView)
self.tableView.contentInset.top = HEADER_HEIGHT_MAX
self.tableView.contentOffset.y = HEADER_HEIGHT_MAX * -1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y + scrollView.contentInset.top
var frame = self.headerView.frame
frame.origin.y = scrollView.contentOffset.y
frame.size.height = max(HEADER_HEIGHT_MIN, HEADER_HEIGHT_MAX - offsetY)
self.headerView.frame = frame
}
問題点
- セクションヘッダーの固定位置がcontentInsetでズレる
- セクションヘッダー、セクションフッターがヘッダーの上に重なってしまう
利用可能な時
ヘッダーをtableHeaderViewに入れる変わりに使うだけで良いので、かなりシンプルに使うことが出来ます。
セクションヘッダー、セクションフッターを使わない構造であればこのやり方で作ることが出来ます。
パターン2
1.ダミーのテーブルヘッダーを置く
2.テーブルの余白は縮小時のサイズに指定する
3.ヘッダーはテーブルに重ねるように設置する
4.スクロールに応じてヘッダーのサイズを調節する
self.view.addSubview(self.headerView)
self.tableView.tableHeaderView = self.dummyHeaderView
self.tableView.contentInset.top = HEADER_HEIGHT_MIN
self.tableView.contentOffset.y = HEADER_HEIGHT_MIN * -1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
var frame = self.headerView.frame
frame.origin.y = 0 //場所は固定
frame.size.height = max(HEADER_HEIGHT_MIN, HEADER_HEIGHT_MAX - offsetY)
self.headerView.frame = frame
self.tableView.scrollIndicatorInsets.top = frame.size.height
}
ヘッダーにダミーのViewを入れる必要があったり、縮小時の大きさをcontentInset.topに入れるなど少しトリッキーです。
また、ヘッダー部分を触った時にスクロールさせるにはisUserInteractionEnabled=false
に設定する必要があるという問題点があります、
そのため、ヘッダー部分にタップ可能な要素を設置することが出来ません。
利用可能な時
ヘッダーに操作可能なUIが無い時はこの方法で問題ありません。
ベストプラクティス
パターン1はシンプルですがセクションヘッダーが使えません。
パターン2はダミーを置くなど構造が少し複雑な上、操作可能なUIを置くことが出来ません。
全てを満たすためには、パターン2をベースにしてヘッダーのタップイベントは通過するのが最もシンプルな解決手段です。
func hitTest()
を使うことでイベントを通過させることが可能です。
isUserInteractionEnabled=true
の要素のみイベントを通過させないようにすればイベントも発生します。
class HeaderCoverView : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else { return nil }
if view == self {
return nil
}
if view.isUserInteractionEnabled {
return view
}
return nil
}
}
ただ、この方法の場合ヘッダー部分の子要素がレイアウトや管理のために入れ子になっている場合など、子要素に対してもhitTest()
の実装が必要です。
hitTestでイベントを透過する場合、複雑なジェスチャー操作を含めることは難しいかもしれません。