I'm creating a custom tableHeaderView using constraints and autolayout. The problem is that my tableHeaderView appears on top of the cells.
Here's my viewDidLoad:
let tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
//Adding subviews and setting constraints
let headerContainer = UIView()
let myView = UIView()
headerContainer.addSubview(myView)
//Setup constraints...
let myLabel = UILabel()
headerContainer.addSubview(myLabel)
//Adding many more views...
tableView.setAndLayoutTableHeaderView(header: headerContainer)
Reading this post, I've copied the suggested extension to UITableView: Is it possible to use AutoLayout with UITableView's tableHeaderView?.
extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
print(header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height) //Always 0???
header.frame.size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
}
The problem is that the header view appears on top of the cells. On printing the size of the headerContainer after layoutIfNeeded and setNeedsLayout, the height is 0...
When you are maintaining the frame yourself, it could be simplified to just this.
extension UITableView {
func setAndLayoutTableHeaderView(header: UIView) {
header.frame.size = header.systemLayoutSizeFitting(size: CGSize(width: self.frame.size.with, height: .greatestFiniteMagnitude))
self.tableHeaderView = header
}
}
Related
I have a custom UIView which is a subclass of UIScrollView. In the init(_ frame) method I add a subview as follows:
contentView = ContentView()
contentView.backgroundColor = UIColor.blue
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
// create contentView's Width and Height constraints
cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)
// activate them
cvWidthConstraint.isActive = true
cvHeightConstraint.isActive = true
cvWidthConstraint.constant = timelineWidth //It's non-zero
cvHeightConstraint.constant = 2.0*frame.height
The problem is it takes time for the contentView frame to be updated. It certainly doesn't get updated in init(_ frame) call. When exactly does contentView frame gets set and how do I catch the event of frame updation?
Inside
override func layoutSubviews() {
super.layoutSubviews()
// to do
}
You get parent view actual frame , but keep in mind as it's called multiple times
before setting the constraints, I would make sure the view is added to the parent view, like “self.addSubview(contentView)”. If “self” view is already added to the view hierachy, then the contentView layout will be effective right away. Alternatively, you can override the method viewDidMoveToSuperview, and there you check if the superview is not nil and, if so, you initialize and plug the subview contentView to it
I am trying to create a tableHeaderView that is the same height as the table view. Consider the following code:
private func setupViews() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Call async else frame size will be incorrect
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.tableView.tableHeaderView = self.makeTableHeaderView()
}
}
private func makeTableHeaderView() -> UIView {
let headerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: tableView.bounds.width, height: tableView.bounds.height))
tableView.backgroundColor = .blue
headerView.backgroundColor = .red
return headerView
}
This results in extra space at the bottom of the table and allows the table to scroll a little (not the image is the table scrolled):
Why is there extra height here? I want it to be exactly the same size as the table view frame.
Any tips or pointers would be appreciated!
Call self.makeTableHeaderView in async block is not a correct way to get right frame of the table view because a result size of the table view will get only after viewDidLayoutSubviews method (this method may be called many times).
A good place for update a size of the header view is viewWillTransition(to:with:), but there you should use systemLayoutSizeFitting(_:) to calculate a result size of the table view.
First, you want to respect the safe-area, or you'll get inconsistent results between devices.
Second, you'll want to set your header view's height after the table has been laid out. You'll probably also want to change it if/when the table frame changes (such as on device rotation).
So, implement viewDidLayoutSubviews() in your controller like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = tableView.tableHeaderView {
// height of the table view
let tableFrame = tableView.frame
// current frame of the header view
var headerFrame = headerView.frame
//Comparison necessary to avoid infinite loop
if tableFrame.size.height != headerFrame.size.height {
// table view frame height has changed
headerFrame.size.height = tableFrame.size.height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
}
}
}
Here's a complete example:
class BigHeaderViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = tableView.tableHeaderView {
// height of the table view
let tableFrame = tableView.frame
// current frame of the header view
var headerFrame = headerView.frame
//Comparison necessary to avoid infinite loop
if tableFrame.size.height != headerFrame.size.height {
// table view frame height has changed
headerFrame.size.height = tableFrame.size.height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
}
}
}
private func setupViews() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
])
tableView.tableHeaderView = makeTableHeaderView()
tableView.backgroundColor = .blue
}
private func makeTableHeaderView() -> UIView {
let headerView = UIView()
headerView.backgroundColor = .red
return headerView
}
}
I'm trying to implement a tableViewHeader (and not "viewForHeaderInSection") for my tableview in a UITableViewController "TBVC1".
I tried two solution (don't work of course...):
1ST : Added a UIView in the top of my UITableViewController in my storyboard. If I add a UILabel (with constraints etc) in this UIView, and try to display my View TBVC1.... I see nothing. The full View is empty. But, if I delete the UILabel inserted in the header UIView, I can see all my cells and the headerView background Color...
Do you have any idea why I can't put any UI component in this UIView?
2ND : If I try load a specific nib UIView like this :
Bundle.main.loadNibNamed("Title", owner: nil, options: nil)!.first as! UIView
self.tableView.setTableHeaderView(headerView: customView)
self.tableView.updateHeaderViewFrame()
extension UITableView {
/// Set table header view & add Auto layout.
func setTableHeaderView(headerView: UIView) {
headerView.translatesAutoresizingMaskIntoConstraints = false
// Set first.
self.tableHeaderView = headerView
// Then setup AutoLayout.
headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
}
/// Update header view's frame.
func updateHeaderViewFrame() {
guard let headerView = self.tableHeaderView else { return }
// Update the size of the header based on its internal content.
headerView.layoutIfNeeded()
// ***Trigger table view to know that header should be updated.
let header = self.tableHeaderView
self.tableHeaderView = header
}
}
it doesn't work too... my full View is empty...
Do you have any ideas why I don't succeed to display a simple UILabel in my UIView for the tableViewheader?
I'm not sure my way is proper, but every time I set tableHeaderView, I also set its height manually.
If you want to use auto-layout in your tableHeaderView, you can call systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) on the header view to get the calculated size and assign the height to headerView.
You can try this simple example in Playground.
import UIKit
import PlaygroundSupport
class MyViewController: UITableViewController {
override func loadView() {
super.loadView()
let headerView = UIView()
headerView.backgroundColor = .red
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Hello World!"
label.textAlignment = .center
label.textColor = .white
headerView.addSubview(label)
let attributes: [NSLayoutConstraint.Attribute] = [.leading, .top, .centerX, .centerY]
NSLayoutConstraint.activate(attributes.map {
NSLayoutConstraint(item: label, attribute: $0, relatedBy: .equal, toItem: headerView, attribute: $0, multiplier: 1, constant: 0)
})
// We have to set height manually, so use `systemLayoutSizeFitting` to get the layouted height
headerView.frame.size.height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
tableView.tableHeaderView = headerView
}
}
PlaygroundPage.current.liveView = MyViewController()
Currently when table content is scrolling, the headerLabel follows scroll and sticks to the top. How can I have avoid this behaviour with Auto Layout?
var tableView: UITableView!
let headerLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont.boldSystemFont(ofSize: 34.0)
label.textColor = .black
label.textAlignment = .center
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
tableView = UITableView(frame: CGRect(x: 0, y: barHeight, width: view.frame.width, height: view.frame.height - barHeight))
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
view.addSubview(headerLabel)
view.addSubview(tableView)
headerLabel.snp.makeConstraints { (make) in
make.top.equalTo(view).offset(35)
make.width.equalToSuperview()
}
tableView.snp.makeConstraints { (make) in
make.top.equalTo(headerLabel.snp.bottom)
make.left.bottom.right.equalToSuperview()
}
}
The headerLabel should scroll with tableView and should not look like sticky header.
Change the Tableview Style from Plain to Grouped. Your header will move with the table cell scroll.
Currently your table view and your label are siblings inside your UIViewController's view, which means your label is not part of the table view so it won't scroll with it. You can add the label to a UIView, set it's constraints and then set the tableHeaderView property of the table view. Here's a sample code with some hardcoded values:
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "some text"
label.sizeToFit()
let headerView = UIView()
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.addSubview(label)
tableView.tableHeaderView = headerView
headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 80).isActive = true
headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
label.leftAnchor.constraint(equalTo: headerView.leftAnchor, constant: 50).isActive = true
tableView.tableHeaderView?.layoutIfNeeded()
In my table view cell are 3 objects; an imageview, label and a textview.
This is how I set up my textView and its constraints:
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textColor = UIColor.blue
textView.font = UIFont.systemFont(ofSize:15.0)
textView.textAlignment = NSTextAlignment.left
textView.isScrollEnabled = false
**note that self is a UIView that contains the textView. The table view cell's content view will contain this UIView
self.addConstraints([
textView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10.0),
textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: 10.0),
textView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10.0),
textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 20.0)
])
I made isScrollEnabled = false so that the textView's height is calculated according to its content
For my UITableView I did this:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
With the above code my tableviewcell does not show the correct height. I believe its because my textview height constraint is giving the tableviewcell content view a wrong height.
How can I fix this?
Setting isScrollEnabled = false will not affect calculating textView's height.
In order to resize cell according to textView's content you should add bottom(=) constraint to your textView in addition to top(=) and optional height(>=).
Have you tried using tableView(heightForRowAt:) delegate method?
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return textView.frame.height // Assuming you have a reference to the textview
}