I researched quite a bit but just couldn't find the answer to my question.
I have created a table (style: .grouped) in Xcode with two headers I want to make sticky. The upper one should collapse on scrolling until the height of the others cells is reached, the lower one should just stay like it is – so pretty similar to the behaviour right now (see below) – just sticking to the top.
My code is as follows:
- HomeTableViewController.swift
import UIKit
class HomeTableViewController: UITableViewController {
var headerView: HeaderView = {
let nib = UINib(nibName: "HeaderView", bundle: nil)
return nib.instantiate(withOwner: HomeTableViewController.self, options: nil).first as! HeaderView
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
headerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
headerView.titleLabel.text = "Title"
headerView.scrollView = tableView
headerView.frame = CGRect(
x: 0,
y: tableView.safeAreaInsets.top,
width: view.frame.width,
height: 250)
tableView.backgroundView = UIView()
tableView.backgroundView?.addSubview(headerView)
tableView.contentInset = UIEdgeInsets(
top: 250,
left: 0,
bottom: 0,
right: 0)
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
tableView.contentInset = UIEdgeInsets(top: 250 + tableView.safeAreaInsets.top,
left: 0,
bottom: 0,
right: 0)
headerView.updatePosition()
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sectionHeaderLabelView = UIView()
sectionHeaderLabelView.backgroundColor = .systemBackground
let sectionHeaderLabel = UILabel()
sectionHeaderLabel.text = "Subtitle"
sectionHeaderLabel.textColor = .label
sectionHeaderLabel.font = UIFont.systemFont(ofSize: 20.0, weight: .semibold)
sectionHeaderLabel.frame = CGRect(x: view.frame.width * 0.05, y: 0, width: view.frame.width, height: 58)
sectionHeaderLabelView.addSubview(sectionHeaderLabel)
let sectionHeaderButton = UIButton()
sectionHeaderButton.frame = CGRect(x: view.frame.width * 0.58, y: 0, width: view.frame.width * 0.5, height: 58)
sectionHeaderButton.setTitle("alle anzeigen →", for: .normal)
sectionHeaderButton.setTitleColor(.secondaryLabel, for: .normal)
sectionHeaderButton.titleLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .semibold)
sectionHeaderButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
sectionHeaderLabelView.addSubview(sectionHeaderButton)
return sectionHeaderLabelView
}
@objc func buttonAction(sender: UIButton!) {
print("Button tapped")
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
headerView.updatePosition()
}
// MARK: Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 20
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "providerCell", for: indexPath) as! HomeTableViewCell
[...]
return cell
}
[...]
}
- HeaderView.swift (just for the collapsing "top" header)
import Foundation
import UIKit
class HeaderView: UIView {
@IBOutlet private(set) var titleLabel: UILabel!
@IBOutlet weak var topBlurView: UIVisualEffectView!
@IBOutlet weak var bottomBlurView: UIVisualEffectView!
@IBOutlet weak var scrollView: UIScrollView?
private var cachedMinimumSize: CGSize?
override func layoutSubviews() {
super.layoutSubviews()
topBlurView.effect = .none
bottomBlurView.effect = .none
}
// Calculate and cache the minimum size the header's constraints will fit.
// This value is cached since it can be a costly calculation to make, and
// we want to keep the framerate high.
private var minimumHeight: CGFloat {
get {
guard let scrollView = scrollView else { return 0 }
if let cachedSize = cachedMinimumSize {
if cachedSize.width == scrollView.frame.width {
return cachedSize.height
}
}
// Ask Auto Layout what the minimum height of the header should be.
let minimumSize = systemLayoutSizeFitting(CGSize(width: scrollView.frame.width, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .defaultLow)
cachedMinimumSize = minimumSize
return minimumSize.height
}
}
func updatePosition() {
guard let scrollView = scrollView else { return }
// Calculate the minimum size the header's constraints will fit
let minimumSize = minimumHeight
// Calculate the baseline header height and vertical position
let referenceOffset = scrollView.safeAreaInsets.top
let referenceHeight = scrollView.contentInset.top - referenceOffset
// Calculate the new frame size and position
let offset = referenceHeight + scrollView.contentOffset.y
let targetHeight = referenceHeight - offset - referenceOffset
var targetOffset = referenceOffset
if targetHeight < minimumSize {
targetOffset += targetHeight - minimumSize
}
// Update the header's height and vertical position.
var headerFrame = frame;
headerFrame.size.height = max(minimumSize, targetHeight)
headerFrame.origin.y = targetOffset
frame = headerFrame;
}
}
Could anyone assist kindly on how to make these two stick to the top when scrolling?
All the best!
