🍣

スクロールに合わせて画像の高さをコードで動的に変える

2021/12/17に公開

実装要件

スクロールに合わせて画面最上部に設定されている画像の高さが更新される

SectionHeaderとtableHeaderViewの間の空白がない状態を維持したままスクロールできる

上にスクロールするとSectionHeaderは画面最上部で止まる

全体完成イメージ

スクロールに合わせて画面最上部に設定されている画像の高さが更新される

基本的な考え方は以下

  • UITableViewを使ってtableHeaderViewにUIImageViewを持つCustomViewを設定
  • スクロール時にoffsetの値に応じてCustomViewの持つUIImageViewのボトムの制約と高さの制約を都度更新する

ソースコード

ViewController.swift

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    private lazy var tableView: UITableView = {
        let view = UITableView()
        view.delegate = self
        view.dataSource = self
        view.contentInsetAdjustmentBehavior = .never
        view.register(SectionHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
        view.register(ContentCell.self, forCellReuseIdentifier: "Cell")
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        makeConstraints()
    }

    private func makeConstraints() {
        view.addAutoLayoutedSubview(tableView)
        NSLayoutConstraint.activate(tableView.fillConstraintsWithTopSafeArea())
    }

    private func setupTableView() {
        let headerView = StretchyTableHeaderView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: StretchyTableHeaderView.imageHeihgt))
        self.tableView.tableHeaderView = headerView

        let view = UIView()
        view.backgroundColor = .orange
        tableView.tableHeaderView?.addAutoLayoutedSubview(view)
        NSLayoutConstraint.activate([
            view.centerXAnchor.constraint(equalTo: tableView.tableHeaderView!.centerXAnchor),
            view.widthAnchor.constraint(equalTo: tableView.tableHeaderView!.widthAnchor),
            view.heightAnchor.constraint(equalToConstant: 30),
            view.topAnchor.constraint(equalTo: tableView.tableHeaderView!.bottomAnchor)
        ])
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? ContentCell else {
            fatalError("Failed To get a CustomCell")
            return UITableViewCell() }
        return cell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        1
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let sectionHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? SectionHeaderView
        return sectionHeader
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 50
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let headerView = self.tableView.tableHeaderView as! StretchyTableHeaderView
        headerView.scrollViewDidScroll(scrollView: scrollView)
    }
}
StretchyTableHeaderView.swift

class StretchyTableHeaderView: UIView {
    static let imageHeihgt = 328.3333333333333
    private var imageViewHeight = NSLayoutConstraint()
    private var imageViewBottom = NSLayoutConstraint()

    private lazy var imageView: UIImageView = {
        let view = UIImageView(image: UIImage(named: "headerImage"))
        view.backgroundColor = .yellow
        view.contentMode = .scaleAspectFill
        return view
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        setConstraints()
    }

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

    func setConstraints() {
        self.addAutoLayoutedSubview(imageView)
        imageViewBottom = imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        imageViewHeight = imageView.heightAnchor.constraint(equalTo: self.heightAnchor)

        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            imageViewBottom,
            imageViewHeight,
        ])
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {
        let offsetY = -scrollView.contentOffset.y
        imageView.clipsToBounds = offsetY <= 0
        imageViewBottom.constant = offsetY >= 0 ? 0 : -offsetY/4.7
        imageViewHeight.constant = max(offsetY, 0)
    }
}

詳細はgithub: https://github.com/yuk1ch1/StretchyHeaderImageViewSample

補足

swift
+ imageViewBottom.constant = offsetY >= 0 ? 0 : -offsetY/4.7
- imageViewBottom.constant = offsetY >= 0 ? 0 : -offsetY/2

ここはもともとは/2でボトムの制約を更新していたのですが、このあとに追記する機能のために調整した結果4.7になりました

追加対応: SectionHeaderとtableHeaderViewの間の空白がない状態を維持したままスクロールさせる&SectionHeaderは画面上部で止まる

追加要件は以下

  • tableHeaderViewとSectionHeaderの間にはスペースがあるのでこれを埋めたい
  • スクロールするとtableHeaderViewとSectionHeaderの間のスペースがない状態を維持して動く
  • 画面上部でSectionHeaderは固定されそれ以上上へSectionHeaderを持っていくことができない

解決方法

自分は上記のコードの通りスペースを埋める用のUIViewを作りSectionHeaderにaddSubviewで追加して対応しました

失敗

TableViewのstyleをgroupedにすればスペースは消えます
ただ上にスクロールしていくとSectionHeaderが画面上部で止まらず消えてしまう

参考

https://johncodeos.com/how-to-make-a-stretchy-header-in-ios-using-swift/

Discussion