iOS tableHeaderView in UITableViewController never displayed - ios

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()

Related

How to create "real" UITableView(Cell) margins where cells cannot be selected outside their content frame?

I would like to create a UITableView layout where the table view itself fills the complete screen (-> scrollbars are shown at screen endges) but the cells are horizontally centered with a fixed width. It should only be possible to select a cell / tap on it within this fixed width but not within the margins:
Simply giving the UITableView and fixed with + center alignment does work, but in this case the scrollbars are not at the screen edges and it is not possible to scroll using the complete screen but only within the tableView frame.
I tried different other solutions:
let cellWidth = 200
let widthDiff = (tableView.frame.width - cellWidth) / 2
// Solution 1: Change horizontal content inset
tableView.contentInset.left = widthDiff
tableView.contentInset.rigth = widthDiff
// => Cells still use the complete width but can be scrolled horizontally by widthDiff
// => Does NOT work
// Solution 2: Setting layout margins on tableView
tableView.layoutMargins.left = widthDiff
tableView.layoutMargins.rigth = widthDiff
// Solution 3: Setting layout margins on cells...
...
cell.layoutMargins.left = widthDiff
cell.layoutMargins.rigth = widthDiff
// Solution 4: Manually center the cell content using constraints within the cell layout.
// => Layout looks correct in all three cases, but while the cell content
// has the correct margins, the cells itselfs still use the complete
// screen width and is still possible to tap/select sells outside
// their frame.
// => Does NOT work
So, I was not able to find a solution which fulfills all three requirements:
Fixed width, centered cells with left and right margins
Scrollbars at screen edges / table view can be scrolled using the complete screen
Cells can only be selected / tapped on within their content frame
Is there a solution using UITableView properties and methods?
EDIT: As requested this image shows what it should look like:
Cells are centered in the middle with some margins on both sides
Scrollbars are at the screen endge
But: The cells still occupy the complete width. When tapping in the area of the margins cell is still selected and its selected-background uses the complete width. This should be avoided.
From touches point of view you seem to want that cells are selectable only at specific position but table view can be selected everywhere where table view is (for scrolling).
From views point of view you wish to limit cells to specific location but want to draw table view everywhere (scrollbars at the edge).
Then I would say that table view needs to stretch through whole screen and the cell content should be limited. This would best be done with simply constraining a custom view within your cell. The selection of cells would then need to be custom. Consider something like the following:
(I intentionally did some parts programmatically to show what is being done. But I would put most of this in storyboard).
class ViewController: UIViewController {
#IBOutlet private var tableView: UITableView?
var dataSource: [Bool] = []
var cellWidth: CGFloat = 200.0
override func viewDidLoad() {
super.viewDidLoad()
tableView?.allowsSelection = false
dataSource = .init(repeating: false, count: 100)
tableView?.reloadData()
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCell()
cell.setupIfNeeded(width: cellWidth)
cell.isCellSelected = dataSource[indexPath.row]
cell.onMiddleViewPressed = { [weak self, weak cell] in
self?.dataSource[indexPath.row] = true
cell?.isCellSelected = true
}
return cell
}
}
class MyCell: UITableViewCell {
private var isSetup: Bool = false
private var middleView: UIView?
var onMiddleViewPressed: (() -> Void)?
var isCellSelected: Bool = false {
didSet {
middleView?.backgroundColor = isCellSelected ? .blue : .gray
}
}
func setupIfNeeded(width: CGFloat) {
guard isSetup == false else { return }
isSetup = true
let middleView = UIView(frame: .zero)
middleView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(middleView)
contentView.addConstraint(.init(item: middleView, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1.0, constant: 0.0))
middleView.addConstraint(.init(item: middleView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: width))
contentView.addConstraint(.init(item: middleView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0.0))
contentView.addConstraint(.init(item: middleView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0.0))
middleView.backgroundColor = isCellSelected ? .blue : .gray
middleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onMiddleViewSelected)))
self.middleView = middleView
}
#objc private func onMiddleViewSelected() {
onMiddleViewPressed?()
}
}
To create a toggle in selection you would then simply do:
cell.onMiddleViewPressed = { [weak self, weak cell] in
guard let self = self else { return }
self.dataSource[indexPath.row].toggle()
cell?.isCellSelected = self.dataSource[indexPath.row]
}
To use a single selection you would do
cell.onMiddleViewPressed = { [weak self, weak cell, weak tableView] in
guard let self = self else { return }
if let currentSelectionIndex = self.dataSource.firstIndex(of: true), currentSelectionIndex != indexPath.row {
self.dataSource[currentSelectionIndex] = false
let targetIndexPath = IndexPath(row: currentSelectionIndex, section: 0)
if tableView?.indexPathsForVisibleRows?.contains(targetIndexPath) == true {
tableView?.reloadRows(at: [targetIndexPath], with: .none)
}
}
self.dataSource[indexPath.row].toggle()
cell?.isCellSelected = self.dataSource[indexPath.row]
}
so this is nothing too heavy. And a pretty standard procedures in cases like having UISwitch (or similar components) for selection where selecting the cell does something completely different (navigate to details for instance).
Perhaps you also need transparent cells and table view. This is just:
tableView?.backgroundColor = .clear
cell.backgroundColor = .clear
cell.contentView.backgroundColor = .clear

Using constraints with tableHeaderView of UITableView, tableHeaderView appears on top of cells

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
}
}

Problem with dynamic UITableView cell heights

I want my cells to have dynamic height. I use the below code:
let tableView: UITableView = {
let view = UITableView()
view.register(MyTableViewCell.self, forCellReuseIdentifier: MyTableViewCell.reuseIdentifier)
view.rowHeight = UITableView.automaticDimension
view.estimatedRowHeight = 150
view.separatorStyle = .singleLine
view.isScrollEnabled = true
return view
}()
The cell contains only label that is given one constraint- to be centered inside a cell:
private func setupView() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { maker in
maker.center.equalToSuperview()
}
}
the label's definition:
let titleLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
The label's text is then assigned in cellForRowAt method but in each case returns same hight even though the text is sometimes 4 lines and cell's hight should be stretched.
What is there that I'm missing in the above code? Thanks
The cell's content needs to have autolayout constraints setup in such way that there is constraint connection from the top to the bottom of the cell, for the automatic dimensions to work.
private func setupView() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { maker in
maker.edges.equalToSuperview()
//or add .top, .left, .right, .bottom constraints individually,
//if you need to add .offset() to each of the sides
}
}
You should give the label top , bottom , leading and trailing constraints to make the height dynamic

Load view from XIB as a subview of a scrollview

I'm still a SO and Swift newbie, so please, be patient and feel free to skip this question :-)
In the body of a XIB's awakeFromNib, I want to load some views as subviews of a UIScrollView (basically, the XIB contains a scrollview, a label and a button).
The scrollview perfectly works if in a loop I load views I create on the fly, eg.
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 150))
customView.frame = CGRect(x: i*300 , y: 0, width: 300, height: 150)
customView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(customView)
But I have a different goal.
In another XIB I have an image view and a stackview with some labels. This XIB is connected in the storyboard to a class SingleEvent that extends UIView.
I want to do the following:
use the XIB as a sort of "blueprint" and load the same view multiple times in my scrollview;
pass to any instance some data;
Is this possible?
I tried to load the content of the XIB this way:
let customView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as? SingleEvent
and this way:
let customView = SingleEvent()
The first one makes the app crash, while the second causes no issue, but I can't see any effect (it doesn't load anything).
The content of my latest SingleEvent is the following:
import UIKit
class SingleEvent: UIView {
#IBOutlet weak var label:UILabel!
#IBOutlet weak var imageView:UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
}
func loadViewFromNib() -> UIView {
let myView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as! UIView
return myView
}
}
Thanks in advance, any help is appreciated :-)
There are a number of approaches to loading custom views (classes) from xibs. You may find this method a bit easier.
First, create your xib like this:
Note that the Class of File's Owner is the default (NSObject).
Instead, assign your custom class to the "root" view in your xib:
Now, our entire custom view class looks like this:
class SingleEvent: UIView {
#IBOutlet var topLabel: UILabel!
#IBOutlet var middleLabel: UILabel!
#IBOutlet var bottomLabel: UILabel!
#IBOutlet var imageView: UIImageView!
}
And, instead of putting loadNibNamed(...) inside our custom class, we create a UIView extension:
extension UIView {
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
}
To load and use our custom class, we can do this:
class FromXIBViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create an instance of SingleEvent from its xib/nib
let v = UIView.fromNib() as SingleEvent
// we're going to use auto-layout & constraints
v.translatesAutoresizingMaskIntoConstraints = false
// set the text of the labels
v.topLabel?.text = "Top Label"
v.middleLabel?.text = "Middle Label"
v.bottomLabel?.text = "Bottom Label"
// set the image
v.imageView.image = UIImage(named: "myImage")
// add the SingleEvent view
view.addSubview(v)
// constrain it 200 x 200, centered X & Y
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalToConstant: 200.0),
v.heightAnchor.constraint(equalToConstant: 200.0),
v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
With a result of:
And... here is an example of loading 10 instances of SingleEvent view and adding them to a vertical scroll view:
class FromXIBViewController: UIViewController {
var theScrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20.0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to the view
view.addSubview(theScrollView)
// constrain it 40-pts on each side
NSLayoutConstraint.activate([
theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
])
// add a stack view to the scroll view
theScrollView.addSubview(theStackView)
// constrain it 20-pts on each side
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 20.0),
theStackView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -20.0),
theStackView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 20.0),
theStackView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -20.0),
// stackView width = scrollView width -40 (20-pts padding on left & right
theStackView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, constant: -40.0),
])
for i in 0..<10 {
// create an instance of SingleEvent from its xib/nib
let v = UIView.fromNib() as SingleEvent
// we're going to use auto-layout & constraints
v.translatesAutoresizingMaskIntoConstraints = false
// set the text of the labels
v.topLabel?.text = "Top Label: \(i)"
v.middleLabel?.text = "Middle Label: \(i)"
v.bottomLabel?.text = "Bottom Label: \(i)"
// set the image (assuming we have images named myImage0 thru myImage9
v.imageView.image = UIImage(named: "myImage\(i)")
theStackView.addArrangedSubview(v)
}
}
}
Result:
Ok, I see. The problem probably in fact that loadViewFromNib function return UIView from xib, but you doesn't use it any way.
Let's try this way:
1) Make your loadViewFromNib function static
// Return our SingleEvent instance here
static func loadViewFromNib() -> SingleEvent {
let myView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as! SingleEvent
return myView
}
2) Remove all inits in SingleEvent class
3) Init it in needed place like this:
let customView = SingleView.loadViewFromNib()
To pass data inside view you can create new function in SingleView class:
func configureView(with dataModel:DataModel) {
//Set data to IBOutlets here
}
And use it from outside like this:
let customView = SingleView.loadViewFromNib()
let dataModel = DataModel()
customView.configureView(with: dataModel)

Setting Image and Text to Button with AutoLayout

I'm trying to create a button with a drop down arrow to the right of the text programatically like so:
The solutions I've seen have used title and image insets, but is there a way to set these with autoLayout programatically? Depending on the option selected, the text in the button could change and the text lengths will be different, so I'm not sure if title and edge insets are the way to go.
This is an example of where a UIStackView is placed in the main VC container view (in my case the UIStackView takes up all available space inside the VC). Basic user information is added in this case a telephone number.
I create a telephone number container view (UIView), a UILabel to contain the tel. no. and an UIImageView for the drop down arrow.
let telNoContainerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let telNoLabel: UILabel = {
let view = UILabel()
let font = UIFont.systemFont(ofSize: 15)
view.font = font
view.backgroundColor = UIColor.white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let telNoImageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = UIColor.white
view.tintColor = ACTION_COLOR
view.image = UIImage(named: "Chevron")?.withRenderingMode(.alwaysTemplate)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
In setBasicInfoViews() simply add the telNoContainerView to he UIStackView. Then the UILabel and the UIImageView are added to the contain view telNoContainerView. Afterward the constraints are added as needed.
You will need to change the constraints to fit your UI design.
fileprivate func setBasicInfoViews(){
infoStackView.addArrangedSubview(telNoContainerView)
telNoContainerView.addSubview(telNoLabel)
telNoContainerView.addSubview(telNoImageView)
NSLayoutConstraint.activate([
telNoLabel.topAnchor.constraint(equalTo: telNoContainerView.topAnchor, constant: 0.0),
telNoLabel.bottomAnchor.constraint(equalTo: telNoContainerView.bottomAnchor, constant: 0.0),
telNoLabel.leadingAnchor.constraint(equalTo: telNoContainerView.leadingAnchor, constant: 0.0),
telNoLabel.trailingAnchor.constraint(equalTo: telNoContainerView.trailingAnchor, constant: 0.0)
])
NSLayoutConstraint.activate([
telNoImageView.centerYAnchor.constraint(equalTo: telNoLabel.centerYAnchor, constant: 0.0),
telNoImageView.heightAnchor.constraint(equalToConstant: 30.0),
telNoImageView.widthAnchor.constraint(equalToConstant: 30.0),
telNoImageView.trailingAnchor.constraint(equalTo: telNoLabel.trailingAnchor, constant: 0.0)
])
}
No, there isn't a way to set the image and title layout properties on a UIButton using AutoLayout.
If you want a fully custom layout for an Image and Title in a UIButton, I would suggest creating a UIView and add a title and an image as subviews using AutoLayout and then add a tap gesture recognizer to the UIView
buttonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonAction)))

Resources