LoginSignup
10
15

More than 5 years have passed since last update.

固定ヘッダーのUITableViewを作るための方法について

Posted at

こんな感じのテーブルを作る必要があったので、手法を模索しました。

ezgif-1-47fdecaf7a.gif

基本 テーブルビューの構造

テーブルビューの構造は下記のようになっています。

  • テーブルヘッダー
  • セクション
    • セクションヘッダー
    • セル
    • セクションフッター
  • テーブルフッター

スクロールに応じて表示される内容は下記のようにかわります。

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
}

pic1.png

問題点

  • セクションヘッダーの固定位置がcontentInsetでズレる
  • セクションヘッダー、セクションフッターがヘッダーの上に重なってしまう

利用可能な時

ヘッダーをtableHeaderViewに入れる変わりに使うだけで良いので、かなりシンプルに使うことが出来ます。
セクションヘッダー、セクションフッターを使わない構造であればこのやり方で作ることが出来ます。

パターン2

1.ダミーのテーブルヘッダーを置く
2.テーブルの余白は縮小時のサイズに指定する
3.ヘッダーはテーブルに重ねるように設置する
4.スクロールに応じてヘッダーのサイズを調節する

pic2.png

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でイベントを透過する場合、複雑なジェスチャー操作を含めることは難しいかもしれません。

10
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
15