I can create simple custom viewForHeaderInSection in programmatically like below. But I want to do much more complex things maybe connection with a different class and reach their properties like a tableView cell. Simply, I want to see what I do.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if(section == 0) {
let view = UIView() // The width will be the same as the cell, and the height should be set in tableView:heightForRowAtIndexPath:
let label = UILabel()
let button = UIButton(type: UIButtonType.System)
label.text="My Details"
button.setTitle("Test Title", forState: .Normal)
// button.addTarget(self, action: Selector("visibleRow:"), forControlEvents:.TouchUpInside)
view.addSubview(label)
view.addSubview(button)
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
let views = ["label": label, "button": button, "view": view]
let horizontallayoutContraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[label]-60-[button]-10-|", options: .AlignAllCenterY, metrics: nil, views: views)
view.addConstraints(horizontallayoutContraints)
let verticalLayoutContraint = NSLayoutConstraint(item: label, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1, constant: 0)
view.addConstraint(verticalLayoutContraint)
return view
}
return nil
}
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
Is there anyone to explain how can I create a custom tableView header view using xib? I have encountered with old Obj-C topics but I'm new with Swift language. If someone explain as detailed, It would be great.
1.issue: Button #IBAction doesn't connect with my ViewController. (Fixed)
Solved with File's Owner, ViewController base class (clicked left outline menu.)
2.issue: Header height problem (Fixed)
Solved adding headerView.clipsToBounds = true in viewForHeaderInSection: method.
For constraint warnings this answer solved my problems:
When I added ImageView even same height constraint with this method in viewController, it flow over tableView rows look like picture.
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 120
}
If I use, automaticallyAdjustsScrollViewInsets in viewDidLoad, In this case image flows under navigationBar. -fixed-
self.automaticallyAdjustsScrollViewInsets = false
3.issue: If button under View (Fixed)
#IBAction func didTapButton(sender: AnyObject) {
print("tapped")
if let upView = sender.superview {
if let headerView = upView?.superview as? CustomHeader {
print("in section \(headerView.sectionNumber)")
}
}
}
The typical process for NIB based headers would be:
Create UITableViewHeaderFooterView subclass with, at the least, an outlet for your label. You might want to also give it some identifier by which you can reverse engineer to which section this header corresponds. Likewise, you may want to specify a protocol by which the header can inform the view controller of events (like the tapping of the button). Thus, in Swift 3 and later:
// if you want your header to be able to inform view controller of key events, create protocol
protocol CustomHeaderDelegate: class {
func customHeader(_ customHeader: CustomHeader, didTapButtonInSection section: Int)
}
// define CustomHeader class with necessary `delegate`, `#IBOutlet` and `#IBAction`:
class CustomHeader: UITableViewHeaderFooterView {
static let reuseIdentifier = "CustomHeader"
weak var delegate: CustomHeaderDelegate?
#IBOutlet weak var customLabel: UILabel!
var sectionNumber: Int! // you don't have to do this, but it can be useful to have reference back to the section number so that when you tap on a button, you know which section you came from; obviously this is problematic if you insert/delete sections after the table is loaded; always reload in that case
#IBAction func didTapButton(_ sender: AnyObject) {
delegate?.customHeader(self, didTapButtonInSection: section)
}
}
Create NIB. Personally, I give the NIB the same name as the base class to simplify management of my files in my project and avoid confusion. Anyway, the key steps include:
Create view NIB, or if you started with an empty NIB, add view to the NIB;
Set the base class of the view to be whatever your UITableViewHeaderFooterView subclass was (in my example, CustomHeader);
Add your controls and constraints in IB;
Hook up #IBOutlet references to outlets in your Swift code;
Hook up the button to the #IBAction; and
For the root view in the NIB, make sure to set the background color to "default" or else you'll get annoying warnings about changing background colors.
In the viewDidLoad in the view controller, register the NIB. In Swift 3 and later:
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: CustomHeader.reuseIdentifier)
}
In viewForHeaderInSection, dequeue a reusable view using the same identifier you specified in the prior step. Having done that, you can now use your outlet, you don't have to do anything with programmatically created constraints, etc. The only think you need to do (for the protocol for the button to work) is to specify its delegate. For example, in Swift 3:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CustomHeader") as! CustomHeader
headerView.customLabel.text = content[section].name // set this however is appropriate for your app's model
headerView.sectionNumber = section
headerView.delegate = self
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44 // or whatever
}
Obviously, if you're going to specify the view controller as the delegate for the button in the header view, you have to conform to that protocol:
extension ViewController: CustomHeaderDelegate {
func customHeader(_ customHeader: CustomHeader, didTapButtonInSection section: Int) {
print("did tap button", section)
}
}
This all sounds confusing when I list all the steps involved, but it's really quite simple once you've done it once or twice. I think it's simpler than building the header view programmatically.
In matt's answer, he protests:
The problem, quite simply, is that you cannot magically turn a UIView in a nib into a UITableViewHeaderFooterView merely by declaring it so in the Identity inspector.
This is simply not correct. If you use the above NIB-based approach, the class that is instantiated for the root view of this header view is a UITableViewHeaderFooterView subclass, not a UIView. It instantiates whatever class you specify for the base class for the NIBs root view.
What is correct, though, is that some of the properties for this class (notably the contentView) aren't used in this NIB based approach. It really should be optional property, just like textLabel and detailTextLabel are (or, better, they should add proper support for UITableViewHeaderFooterView in IB). I agree that this is poor design on Apple's part, but it strikes me as a sloppy, idiosyncratic detail, but a minor issue given all the problems in table views. E.g., it is extraordinary that after all these years, that we still can't do prototype header/footer views in storyboards at all and have to rely on these NIB and class registration techniques at all.
But, it is incorrect to conclude that one cannot use register(_:forHeaderFooterViewReuseIdentifier:), an API method that has actively been in use since iOS 6. Let’s not throw the baby out with the bath water.
See previous revision of this answer for Swift 2 renditions.
Rob's answer, though it sounds convincing and has withstood the test of time, is wrong and always was. It's difficult to stand alone against the overwhelming crowd "wisdom" of acceptance and numerous upvotes, but I'll try to summon the courage to tell the truth.
The problem, quite simply, is that you cannot magically turn a UIView in a nib into a UITableViewHeaderFooterView merely by declaring it so in the Identity inspector. A UITableViewHeaderFooterView has important features that are key to its correct operation, and a plain UIView, no matter how you may cast it, lacks them.
A UITableViewHeaderFooterView has a contentView, and all your custom subviews must be added to this, not to the UITableViewHeaderFooterView.
But a UIView mysteriously cast as a UITableViewHeaderFooterView lacks this contentView in the nib. Thus, when Rob says "Add your controls and constraints in IB", he is having you add subviews directly to the UITableViewHeaderFooterView, and not to its contentView. The header thus ends up incorrectly configured.
Another sign of the issue is that you are not permitted to give a UITableViewHeaderFooterView a background color. If you do, you'll get this message in the console:
Setting the background color on UITableViewHeaderFooterView has been deprecated. Please set a custom UIView with your desired background color to the backgroundView property instead.
But in the nib, you cannot help setting a background color on your UITableViewHeaderFooterView, and you do get that message in the console.
So what's the right answer to the question? There's no possible answer. Apple has made a huge goof here. They have provided a method that allows you to register a nib as the source of your UITableViewHeaderFooterView, but there is no UITableViewHeaderFooterView in the Object Library. Therefore this method is useless. It is impossible to design a UITableViewHeaderFooterView correctly in a nib.
This is a huge bug in Xcode. I filed a bug report on this matter in 2013 and it is still sitting there, open. I refile the bug year after year, and Apple keeps pushing back, saying "It has not been determined how or when the issue will be resolved." So they acknowledge the bug, but they do nothing about it.
What you can do, however, is design a normal UIView in the nib, and then, in code (in your implementation of viewForHeaderInSection), load the view manually from the nib and stuff it into the contentView of your header view.
For example, let's say we want to design our header in the nib, and we have a label in the header to which we want to connect an outlet lab. Then we need both a custom header class and a custom view class:
class MyHeaderView : UITableViewHeaderFooterView {
weak var content : MyHeaderViewContent!
}
class MyHeaderViewContent : UIView {
#IBOutlet weak var lab : UILabel!
}
We register our header view's class, not the nib:
self.tableView.register(MyHeaderView.self,
forHeaderFooterViewReuseIdentifier: self.headerID)
In the view xib file, we declare our view to be a MyHeaderViewContent — not a MyHeaderView.
In viewForHeaderInSection, we pluck the view out of the nib, stuff it into the contentView of the header, and configure the reference to it:
override func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let h = tableView.dequeueReusableHeaderFooterView(
withIdentifier: self.headerID) as! MyHeaderView
if h.content == nil {
let v = UINib(nibName: "MyHeaderView", bundle: nil).instantiate
(withOwner: nil, options: nil)[0] as! MyHeaderViewContent
h.contentView.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
v.topAnchor.constraint(equalTo: h.contentView.topAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: h.contentView.bottomAnchor).isActive = true
v.leadingAnchor.constraint(equalTo: h.contentView.leadingAnchor).isActive = true
v.trailingAnchor.constraint(equalTo: h.contentView.trailingAnchor).isActive = true
h.content = v
// other initializations for all headers go here
}
h.content.lab.text = // whatever
// other initializations for this header go here
return h
}
It's dreadful and annoying, but it is the best you can do.
Create a UITableViewHeaderFooterView and its corresponding xib file.
class BeerListSectionHeader: UITableViewHeaderFooterView {
#IBOutlet weak var sectionLabel: UILabel!
#IBOutlet weak var abvLabel: UILabel!
}
Register the nib similarly to how you register a table view cell. The nib name and reuse identifier should match your file names. (The xib doesn't have a reuse id.)
func registerHeader {
let nib = UINib(nibName: "BeerListSectionHeader", bundle: nil)
tableView.register(nib, forHeaderFooterViewReuseIdentifier: "BeerListSectionHeader")
}
Dequeue and use similarly to a cell. The identifier is the file name.
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "BeerListSectionHeader") as! BeerListSectionHeader
let sectionTitle = allStyles[section].name
header.sectionLabel.text = sectionTitle
header.dismissButton?.addTarget(self, action: #selector(dismissView), for: .touchUpInside)
return header
}
Don't forget the header height.
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return BeerListSectionHeader.height
}
I don't have enough reputation to add comment to Matt answer.
Anyway, the only thing missing here is to remove all subviews from UITableViewHeaderFooterView.contentView before adding new views. This will reset reused cell to initial state and avoid memory leak.
Related
I kind of have a weird layout here, it's kind of like this (also see pics):
-UITableViewCell 1
----UIView 2
--------UITableView 3
The controller the the UITableView (1) is like that:
//mainTableView (1) controller
var cellHeights = [CGFloat]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let card = CardSource.orangeCards[indexPath.row]
cell.configureCardInCell(card)
cellHeights.insert(card.frame.height + 15, at: indexPath.row)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row]
}
but the problem is that when the screen first loads, the UIViews overlap because the cells are too small because the smaller table view (the one in the UIView) hasn't loaded yet and it's height isn't defined. Proof of this is that when I scroll to the bottom of the main table view then scroll back up cellForRowAt is called again and the two views don't overlap anymore (see pics). So what I basically want is a way to load the small table view and define it's height before the bigger table view loads (or if you have any other solutions, that'd be welcome too)
I know my question isn't very clear, I'm not really good at explaining stuff, so don't hesitate to ask me questions in the comments.
Many thanks!
When the view first loads
After scrolling down then back up
EDIT:
I found this:
static var pharmacyOrangeCard: CardView {
let view = Bundle.main.loadNibNamed("Pharmacy Orange Card", owner: self, options: nil)?.first as! PharmacyTableCardView
print(view.frame.height)
return view
}
prints the correct height. But then, when I try to access it from the controller above, it gives me a smaller number! In the meanwhile, I applied these constraints:
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
card.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
card.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
But I don't think that affects height, does it?
EDIT 2:
Okay, so I've changed this constraint:
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
to this:
card.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 17.5).isActive = true
card.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -17.5).isActive = true
So these constraints seem to play a role because now I have this:
enter image description here
By the way, I don't know if that matters but I'm using XIB files for each of these "cards", and the height isn't constrained, so maybe that plays a role?
SOLVING EDIT:
I solved the problem by doing:
override func viewDidAppear(_ animated: Bool) {
mainTableView.reloadData()
}
Once a cell loaded on the screen, you cannot change height for that cell for better UI-Experience,
and in hierarchy heightForRowAt get called before cellForRowAt.
So you had 2 options to choose for a solution to your problem
first:: get your heights values ready before your table view try to loads cells in it (get heights array ready before setting delegate and datasource values to your tableView)
second:: whenever you need to update your tableView cells to re-established with respect to new height values, call this each time after you have updated your height values
yourTableView.reloadData()
I’m new to swift dev and stack overflow, I’ll really appreciate if someone could shed some light over a search bar sizing issue I’m having. I’m not being able to correctly size a search bar in the screen. I created a simple version of my project to simplify the reproduction of the odd behavior. It’s a single ViewController with a tableView object to display results of a searchBar search. I don’t want the search bar to roll up along with the table cells, so I created a UIView as placeholder for the searchBar just above the tableView in the Main.storyboard and added necessary constraints to them, so they could be resized correctly with different screen sizes and orientations — indeed, auto-layout seems to be working fine.
Please see the image links as I still can't post images here. Main.storyboard image
I also don’t want to use a UISearchBar object in the storyboard because I want to have control over it in my code later. So I added the SearchController searchBar programmatically as a UIview subview with (apparently all) the needed constraints to be resized along with the UIview. Also, as you can see, tableView and the UIView widths are a little smaller than the screen width, it’s one difference to other posts I found, they were all fully stretched to screen edges — I don’t know if this is important or not. I'm using swift 4 on xcode 9.0.1 (9A1004).
The issue:
As you can see the searchBar right edge passes over the UIView frame right edge. When it's tapped the cancel button passes over the screen limit as well.
searchBar image 1
It's even more weird, when orientation is changed to landscape, searchBar width initially doesn’t stretch with the UIView. But when searchBar is tapped its width returns to pass over the screen limit. And when cancel is tapped searchBar width continues greater than the screen width. This behavior occurs no matter screen size and orientation I choose.
What I tried so far:
This project contains all the recommended properties and constraints I could find, with the exception of the property setTranslatesAutoresizingMaskIntoConstraints which I didn't manage to use. On swift 4, I only found a similar one translatesAutoresizingMaskIntoConstraints, but I couldn’t make it work, it returns “Cannot call value of non-function type 'Bool’”.
// searchController.searchBar.setTranslatesAutoresizingMaskIntoConstraints(true)
searchController.searchBar.translatesAutoresizingMaskIntoConstraints(true) (!) Cannot call value of non-function type 'Bool'
The post Display UISearchController's searchbar programmatically is a very good one, it’s the most similar to mine (except that it extends to the left, and my case it does do the right), but it didn’t work as well. I also realized that #kcstricks sets the searchBar width to self.view.frame.size.width instead of the UIview frame width.
// self.searchController.searchBar.frame.size.width = self.view.frame.size.width
self.searchController.searchBar.frame.size.width = searchBarPlaceholderView.frame.size.width
By changing it, solved the initial searchBar behavior. Now it's initially shown correctly:
searchBar image 2
But once it’s tapped the odd behavior returns and never gets back to normal again.
searchBar image 3
searchBar image 4
I also tried moving the main code to viewDidAppear method, but no results.
Where am I failing at?
Other researched stack overflow posts:
Fixed UISearchBar using UISearchController - Not using header view of UITableView
UISearchController's searchBar doesn't fill full width
UISearchBar width doesn't change when its embedded inside a UINavigationBar
Thank you very much in advance.
Complete code:
// ViewController.swift
// SearchBarTests
// Copyright © 2017 equilibrio. All rights reserved.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchResultsUpdating {
#IBOutlet var myTableView: UITableView!
#IBOutlet var searchBarPlaceholderView: UIView!
var searchController: UISearchController!
var selectedBeers = [Beer]()
struct Beer {
var type = String()
var examples = String()
}
var beers = [Beer(type: "American Lager", examples: "Budweiser, Coors, Pabst Blue Ribbon"),
Beer(type: "German Helles", examples: "Victory Helles Lager, Stoudt's Gold Lager"),
Beer(type: "German Pilsner", examples: "Tröegs Sunshine Pils, Bavaria, Sierra Nevada's Nooner Pilsner"),
Beer(type: "Belgian Gueuze", examples: "")]
// Delegates and Datasources
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.selectedBeers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "cell")
cell.textLabel?.text = self.selectedBeers[indexPath.row].type
cell.detailTextLabel?.text = self.selectedBeers[indexPath.row].examples
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Row \(indexPath.row) selected")
}
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text! == "" {
selectedBeers = beers
} else {
selectedBeers = beers.filter { $0.type.lowercased().contains(searchController.searchBar.text!.lowercased()) }
}
self.myTableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
selectedBeers = beers
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.dimsBackgroundDuringPresentation = false
self.searchController.searchBar.delegate = self
definesPresentationContext = true
self.searchBarPlaceholderView.addSubview(self.searchController.searchBar)
self.searchController.searchBar.searchBarStyle = UISearchBarStyle.minimal
// self.searchController.searchBar.frame.size.width = self.view.frame.size.width
self.searchController.searchBar.placeholder = "Type desired beer style..."
self.searchController.searchBar.frame.size.width = searchBarPlaceholderView.frame.size.width
self.searchController.searchBar.sizeToFit()
// searchController.searchBar.setTranslatesAutoresizingMaskIntoConstraints(false)
// searchController.searchBar.translatesAutoresizingMaskIntoConstraints(true)
self.myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.myTableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I had the same issue. I added frame adjusting code to all over the place. For example when i choose an item from tableview suggestion list i call a function. Inside that function i wrote code below, so searchbar came back to width i wanted after animating. I hope somebody can give more professional solution. It doesnt look nice in my code but works.
func(youCallbyTapping) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
var searchBarFrame = self.fromSearchController.searchBar.frame
searchBarFrame.size.width = self.myFrom.frame.size.width - 15
self.fromSearchController.searchBar.frame = searchBarFrame
self.toSearchController.searchBar.frame = searchBarFrame
}
}
I am in the process of updating my app to iOS10 with Swift 2.3 and Xcode 8 Beta 1 and I have found that there is a UITableViewHeaderFooterContentView which is blocking touches to the UIButton on my subclass of UITableViewHeaderFooterView.
On the Xcode 8 Beta 1 simulator the UIButton works on iOS9.3 but not iOS10.
1) Is there any documentation for this?
2) How can I ensure my UI elements are on top of the new Content View in iOS10? (or allow touches through the UITableHeaderFooterContentView)
Thanks!
Table Header
import UIKit
class TableHeader: UITableViewHeaderFooterView {
#IBOutlet weak var dayLabel: UILabel!
#IBOutlet weak var dateLabel: UILabel!
#IBOutlet weak var addNewEventButton: UIButton!
}
Code In View Controller
dateCell.addNewEventButton is the UIButton that is no longer receiving touches in iOS10
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tintColor = TintManager().getTintColour()
let dateCell:TableHeader = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableHeader") as! TableHeader
//dateCell.bringSubviewToFront(dateCell.addNewEventButton)
dateCell.dayLabel.text = Dates.day.uppercaseString
dateCell.dateLabel.text = Dates.date
dateCell.backgroundView = UIView(frame: dateCell.frame)
dateCell.backgroundView!.backgroundColor = tintColor
dateCell.dayLabel.textColor = UIColor.whiteColor()
dateCell.dateLabel.textColor = UIColor.whiteColor()
dateCell.addNewEventButton.backgroundColor = tintColor
dateCell.addNewEventButton.tag = section
dateCell.addNewEventButton.layer.cornerRadius = 20.0
if (savedEventView.superview === self.view) {
dateCell.addNewEventButton.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
dateCell.addNewEventButton.addTarget(self, action: #selector(ViewController.userPressedAddButtonToInsertSavedEvent(_:)), forControlEvents:.TouchUpInside)
} else {
dateCell.addNewEventButton.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
dateCell.addNewEventButton.addTarget(self, action: #selector(ViewController.userPressedAddNewEventOnTableViewHeader(_:)), forControlEvents:.TouchUpInside)
}
return dateCell
}
The offending view is in fact the contentView of the UITableViewHeaderFooterView (see the Apple Docs). So you should be able just to use sendSubview(toBack:) in order to stop it interfering with touches.
However, it seems that under iOS9 the UITableViewHeaderFooterView fails to correctly initialise the contentView if the view is loaded from a NIB. Although the contentView property is not optional, it is in fact nil, and you get a BAD ACCESS error if you try to access it. Nor can you set a value for contentView (either in code or as an outlet in IB) because it's a read only property (*). So the only solution I can think of is to use #available to conditionally include code to move the contentView to the back, if you are running on iOS 10 or newer. I would put the relevant code into your subclass:
override func awakeFromNib() {
if #available(iOS 10, *) {
self.sendSubview(toBack: contentView)
}
}
(*) Indulging in wild speculation, my guess is that Apple based the UITableViewHeaderFooterView code heavily on UITableViewCell. Since IB has UITableViewCells in its object library (and notice these include the cell's contentView), it can ensure that the cell's contentView is correctly instantiated. But since there is no UITableViewHeaderFooterView in the object library, there's no way to get the contentView loaded correctly. Looks like they fixed it in iOS10 by instantiating an empty contentView. Pity they didn't also add UITableViewHeaderFooterView to the library.
I am having trouble getting my UITableView to appear full height in my Stack View.
My view tree looks as follows:
- View
- Scroll View
- Stack View
- Table View
- Image View
- Map View
The table view is dynamically populated with data, which works fine. The issue is that only one row is visible at a time and I have to scroll through the list. What I would like to see happen is for the table view to take as much vertical room as it needs to display all the cells.
I did try adjusting table height as follows, but that just ends up with table that no longer scrolls, though even if it did work I would rather have something more dynamic:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.detailsTableView.frame.size.height = 200
}
I am suspecting that it is probably an aspect of the 'stack view' that needs adjusting, but I am not sure at this point. Can anyone suggest an appropriate way?
I had been encountering the same issue and realized you need a self sizing table view. I stumbled on this answer and created a subclass like #MuHAOS suggested. I did not encounter any issues.
final class IntrinsicTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
A UIStackView will compress views wherever it can, to counteract this set a height anchor and width anchor to the UITableView or a priority for its height and width. Here is a working example of how we can be in charge of the dimensions of a table within a stack view.
An extension to instantiate and centrally position the UIStackView
First of all I've written a UIStackView extension so that I don't need to include all the code inside the view controller. Your positioning and setup will be different because you are placing your stack view inside a scroll view, but separating this code out means you can make your own adjustments.
extension UIStackView {
convenience init(axis:UILayoutConstraintAxis, spacing:CGFloat) {
self.init()
self.axis = axis
self.spacing = spacing
self.translatesAutoresizingMaskIntoConstraints = false
}
func anchorStackView(toView view:UIView, anchorX:NSLayoutXAxisAnchor, equalAnchorX:NSLayoutXAxisAnchor, anchorY:NSLayoutYAxisAnchor, equalAnchorY:NSLayoutYAxisAnchor) {
view.addSubview(self)
anchorX.constraintEqualToAnchor(equalAnchorX).active = true
anchorY.constraintEqualToAnchor(equalAnchorY).active = true
}
}
We don't set a size for the UIStackView only a position, it is the things contained within it that determine its size. Also note the setting of translatesAutoresizingMaskIntoConstraints to false in the UIStackView extension. (It is only required that we set this property for the stack view, its subviews simply inherit the behaviour.)
UITableView class with data source code
Next I've created a basic table class for demo purposes.
class MyTable: UITableView, UITableViewDataSource {
let data = ["January","February","March","April","May","June","July","August","September","October","November","December"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SauceCell", forIndexPath: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Setup of stack view and table in view controller
Finally, the important stuff. As soon as we add our table to the stack view all the frame information is disregarded. So we need the final two lines of code to set the width and height for the table in terms that Auto Layout can understand.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let table = MyTable(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "SauceCell")
table.dataSource = table
let stack = UIStackView(axis: .Vertical, spacing: 10)
stack.anchorStackView(toView: view, anchorX: stack.centerXAnchor, equalAnchorX: view.centerXAnchor, anchorY: stack.centerYAnchor, equalAnchorY: view.centerYAnchor)
stack.addArrangedSubview(table)
table.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 1).active = true
table.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.5).active = true
}
}
Note that we use addArrangedSubview: not addSubview: when adding views to the stack view.
(I've written blogposts about UIStackView as well as others about Auto Layout in general that might help too.)
I am trying to set the height of a view that is on top of my prototype cell in a table view controller. I use IB to set it's height (size inspector) and set it to 61 like so (the green view is the 'header' view):
But whenever I run the app, its' height ends up being 568.0. I have an IBOutlet called testUIView for the view in my table view controller, and I do: println("testUIView Height->\(testUIView.frame.height)") and indeed ends up being 568.0 at runtime.
Here is a screenshot showing its' height at runtime:
So my question is: How can I set the view's height so it is 61 at runtime so it indeed looks like my first screenshot (size-wise)?
I tried to set its' height property inside override func viewWillLayoutSubviews() but it did not let me assign a value to the height testUIView.frame.height = CGFloat(61.0).
Any help is appreciated! Thanks in advance!
Cheers!
Here is a solution which uses section header views rather than the actual table header view:
If you'd like to use a header for you UITableView instead you can design another prototype cell in Interface Builder, make a custom class based on a UITableViewCell and assign it to the prototype cell in interface builder on the class inspector.
Then in your controller you're going to use
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
In that function you're actually going to create a reusable cell from your table view but cast as the custom cell you made for the header. You will have access to all of it's properties like a regular UITableViewCell, then you're just going to return the cell's view
return cell.contentView
Another method you're going to use is
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 61.0
}
That one is pretty self explanatory.
Swift 3.0.1
public override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 61.0
}
Swift 3/Xcode 8:
Add this in viewDidLoad():
let HEADER_HEIGHT = 100
tableView.tableHeaderView?.frame.size = CGSize(width: tableView.frame.width, height: CGFloat(HEADER_HEIGHT))
Enjoy!
The accepted answer doesn't actually answer the question. It instead offers an alternative by using the SECTION header. This question has been answered by others but I will duplicate the answer here with a few more instructions.
Loading the view
Table views are as old as iPhones and therefore you sometimes have to force it to do what you want.
First we need to load the header and manually set its height. Otherwise the view will take more height than it needs. We do this on the viewDidLayoutSubviews callback:
lazy var profileHeaderView: ProfileHeaderView = {
let headerView = ProfileHeaderView()
return headerView
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeHeaderToFit()
}
private func sizeHeaderToFit() {
profileHeaderView.setNeedsLayout()
profileHeaderView.layoutIfNeeded()
var frame = profileHeaderView.frame
frame.size.height = profileHeaderView.calculateHeight()
profileHeaderView.frame = frame
tableView.tableHeaderView = profileHeaderView
}
As you can see, I like to put my views inside lazy vars. This ensures that they are always created but only when I start using them.
You can also see that I'm calculating the height. In some cases, your height is fixed and therefore you can just set the frame height to a hardcoded value.
Set some priorities
We will likely see some constraint warnings appear in our debugger. This happens because the table view first forces a 0x0 size before using the size we specified above At this moment, your constraints and the height of the view are in conflict with each other.
To clear these, we simply set the constraint priorities. First you should wrap your header view components inside another view (I generally always do this for header views). This will make managing constraints much easier on your header view.
We then need to set the bottom constraint priorities to high:
containerView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
containerView.setContentHuggingPriority(.defaultHigh, for: .vertical)
Here is a more complete example:
WARNING: Thought it is still useful as a guide for laying out your views, do not use this code if you're creating your views using nibs or storyboards.
class ProfileHeaderView: UIView {
lazy var containerView: UIView = {
let view = UIView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
// We do this because the view is not created using storyboards or nibs.
fatalError("init(coder:) has not been implemented")
}
private func setupLayout() {
self.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
containerView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
containerView.setContentHuggingPriority(.defaultHigh, for: .vertical)
// Set the rest of your constraints against your containerView not self and add your subviews to your containerView not self
}
}
Here is the example of the constraints set using snap-kit:
containerView.snp.makeConstraints() { make in
make.top.equalTo(self.snp.top)
make.leading.equalTo(self.snp.leading)
make.trailing.equalTo(self.snp.trailing)
make.bottom.equalTo(self.snp.bottom).priority(.high)
}
Make sure you add your constraints to the containerView not self and use containerView to add your subviews and rest of your constraints.
It has to be one of the strangest issues in iOS.
If you do just want a fixed height, as of 2019 you can:
public override func viewDidLayoutSubviews() {
var frame = tableView.tableHeaderView!.frame
frame.size.height = 68
tableView.tableHeaderView!.frame = frame
}
Strange stuff.
In swift 4.1 and Xcode 9.4.1
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
return 75.0
} else {
return 50.0
}
}
If you use .xib file with UIVIew for your HeaderView, you can use self-sizing header like this
override func layoutSubviews() {
super.layoutSubviews()
// Manually set the view's frame based on layout constraints.
// The parent UITableView uses the header view's frame height when laying out it's subviews.
// Only the header view's height is respected.
// The UITableView ignores the view frame's width.
// Documentation: https://developer.apple.com/documentation/uikit/uitableview/1614904-tableheaderview
frame.size = systemLayoutSizeFitting(
.init(
width: frame.size.width,
height: 0
),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
}