Animation is weird when hiding subView in UIStackView in Cell - ios

Here is the situation. A title label, a "read more" button and a content label in a stackView. And the stackView is in a cell, set Auto-Layout. The height of tableView's cell is set to AutoDimension. When I tap button, content label will show or hide.
The button's action method is
#IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
self.contentLabel.isHidden.toggle()
}, completion: nil)
}
Here is the result in slow animations:
As you can see, when the content is going to show, line 2 is presented firstly, i.e. content is presented from the center. When the content is going to hide, the content label is hidden instantly, and button is stretched to the frame that content label had before hiding. This animation is strange.
Furthermore, if I set the stackView's spacing to 10, the case becomes worse. The title label was also affected:
I adjusted everything I can,stackView's distribution, three subViews' content mode and content hugging/compression priority. I can't find an appropriate way to fix it.
Here is the ideal result:
I achieved it by a little tricky way:
#IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.contentLabel.isHidden.toggle()
}
}, completion: nil)
}
I'm not sure this is the most appropriate way to fix it. So I want to know why this weird animation happens and if there is a more appropriate way to fix it. Thanks!

Animating to hide/reveal multi-line labels can be problematic, particularly when used in a stack view.
If you give it a try, you'll find that even outside of a table view cell - just the stack view in a view - you will see the same issue when toggling the .isHidden property of the label. This is due to the fact that UILabel vertically centers its text.
Here is another approach, which doesn't use a stack view (background colors for clarity):
Top Label is set to 1 line; Read More is a normal button, Bottom Label is set to 0 lines.
You will notice the pink rectangle. That is a UIView which I've named ShimView - more about that shortly.
The Top Label is constrained Top: 4, Leading: 8, Trailing: 8
The Button is constrained Top: 0 (to topLabel), Leading: 8, Trailing: 8
The Bottom Label is constrained Top: 0 (to button), Leading: 8, Trailing: 8
The "shim view" is constrained Trailing: 8, Top: 0 (*to the top of bottom label*), Bottom: 4 (to the contentView)
The "shim view" is also given a Height constraint of 21, with Priority: 999 -- and that Height constraint is connected to an IBOutlet in the cell class.
The key is that we will adjust the shim's Height constraint's .constant to expand/collapse the cell.
On init, we set the .constant to 0 - this will leave the Bottom Label at its content-determined height, but won't be visible because it will be clipped by the cell's contentView.
When we want to "reveal/conceal" the label, we'll animate the height .constant of the shim.
Result:
And, the result after clearing the background colors:
Here is the code:
//
// ExpandCollapseTableViewController.swift
//
// Created by Don Mag on 6/19/18.
//
import UIKit
class ExpandCollapseCell: UITableViewCell {
#IBOutlet var topLabel: UILabel!
#IBOutlet var theButton: UIButton!
#IBOutlet var bottomLabel: UILabel!
#IBOutlet var theShim: UIView!
#IBOutlet var shimHeightConstraint: NSLayoutConstraint!
var myCallBack: (() -> ())?
#IBAction func didTap(_ sender: Any) {
myCallBack?()
}
}
class ExpandCollapseTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandCollapseCell", for: indexPath) as! ExpandCollapseCell
cell.topLabel.text = "Index Path - \(indexPath)"
cell.bottomLabel.text = "Line 1\nLine 2\nLine 3\nLine 4"
// init to "collapsed"
// in actual use, this would be tracked so the row would remain expanded or collapsed
// on reuse (when the table is scrolled)
cell.shimHeightConstraint.constant = 0
if true {
cell.topLabel.backgroundColor = .clear
cell.theButton.backgroundColor = .clear
cell.bottomLabel.backgroundColor = .clear
cell.theShim.backgroundColor = .clear
}
cell.myCallBack = {
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.tableView.beginUpdates()
cell.shimHeightConstraint.constant = (cell.shimHeightConstraint.constant == 0) ? cell.bottomLabel.frame.size.height : 0
self.tableView.layoutIfNeeded()
self.tableView.endUpdates()
}
}
return cell
}
}

I have had to do a similar animation several times before. The way to solve this is to define the height of your stack view and independently your cell's content view. Then when you want the cell's height to change, you only update the content view's height constraint.
A good way to determine a views height is to use the intrinsicContentSize property. Override this if you need a different value from the inherited one.
Another way to be notified of a views size change is to create a subclass with a delegate or a closure which is called from the subclassed views frame property and passes the new size to whoever is listening for it.

Related

UITableView + AutoLayout + Intrinsic Size: Where/when to calculate size?

I have create a custom view MyIntrincView which calculates its height automatically when setting its content. This works fine both in simulator and InterfaceBuilder.
However, when placing MyIntrinsicView inside a UITableViewCell, the cell height is not calculated correctly. Instead of automatically adopting the cell height to the intrinsic height of the view, all cell keep the same, initial height.
// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
#IBDesignable class MyIntrinsicView: UIView {
#IBInspectable public var someProperty: Int = 5 {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
calcContent()
}
func calcContent() {
height = CGFloat(20 * someProperty)
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
#IBOutlet private var myIntrinsicView: MyIntrinsicView!
var someProperty: Int {
get { return myIntrinsicView.someProperty }
set {
myIntrinsicView.someProperty = newValue
// Cell DOES NOT rezise without manualle calling layoutSubviews()
myIntrinsicView.layoutSubviews()
}
}
}
...
// Simple tableView delegate which should create cells of different heights
// by giving each cell a different someProperty
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "IntrinsicCell", for: indexPath) as? MyIntrinsicCell ?? MyIntrinsicCell()
// Give all cell a different intrinsic height by setting someProperty to rowIndex
cell.someProperty = indexPath.row
return cell
}
I would expect, that each cell has a different height (20 * someProperty = 20 * indexPath.row). However, instead all cell have the same, initial height.
Only when explicitly calling myIntrinsicView.layoutSubviews() the cells are created with the correct height.
It seems that the tableView does not call myIntrinsicView.layoutSubviews(). Why is this?
When using a UILabel instad of MyIntrinsicView as cell content with different text lengths, everything works as expected. Thus the overall tableView setup is correct (= cell sizes are calculated automatically) and there has to be way to use intrinsic sizes correctly in UITableView as well. So, what exactly is the correct way to do this?
As with your previous question here I think you're not really understanding what intrinsicContentSize does and does not do.
When setting the text of a UILabel, yes, its intrinsicContentSize changes, but that's not all that happens.
You also don't want to do what you're trying inside layoutSubviews() ... if your code does trigger a change, you'll get into an infinite recursion loop (again, as with your previous question).
Take a look at modifications to your code:
// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
#IBDesignable class MyIntrinsicView: UIView {
#IBInspectable public var someProperty: Int = 5 {
didSet {
calcContent()
setNeedsLayout()
}
}
func calcContent() {
height = CGFloat(20 * someProperty)
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
#IBOutlet private var myIntrinsicView: MyIntrinsicView!
var someProperty: Int {
get { return myIntrinsicView.someProperty }
set {
myIntrinsicView.someProperty = newValue
}
}
}
Two side notes...
1 - Your posted code shows you calling myIntrinsicView.layoutSubviews()... from Apple's docs:
You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.
2 - For the direction it looks like you're headed, you would probably be better off manipulating constraint constants, rather than intrinsic content size.

Swift - StackView and Adding Constraints With Programmatically Added Views To Stack - Adjust Cell Hight After Load?

I'm trying to make a custom tableView cell that has a stackView from an array images.
I used interface builder and added a horizontal stackView with:
Center X
Center Y
Top +12
Bottom +12
I'm using a loop to add Arranged Subview and it works fine. But the images are super small, even though they shouldn't be due to their CGRect. But they are getting adjusted by the stackView.
The idea was to keep the stack centered, while also using the top/bottom to adjust the hight of the cell.
I've reasoned that the issue I'm facing with the images being small is because the stackView is initialized empty. So the stackViews hight is very tiny. And because imageViews are being added inside of it, they are then being resized to fit.
How can you get a cell/stackView to adjust their hight after being displayed?
Note: I've also been having issues with trying to add a top & bottom constraint to the imageView programmatically.
for pictogram in pictograms {
let imageView = UIImageView()
let size = self.bounds.width / CGFloat(pictograms.count + 1)
print("size: ", size)
imageView.frame = CGRect(x: 0, y: 0, width: size, height: size)
imageView.contentMode = .scaleAspectFit
imageView.image = pictogram.image
pictogramStack.addArrangedSubview(imageView)
}
You are using a UIStackView in the wrong way. Stack views will arrange the subviews for you - no need to be calculating widths and setting frames.
Layout your cell prototype like this:
Note that the Bottom constraint has Priority: 999. Auto-layout needs to make multiple "passes" to lay out stack views (particularly in table view cells). Using a priority of 999 for the bottom constraint will avoid Constraint Conflict error / warning messages.
Set the stack view properties like this:
With Distribution: Fill Equally we don't have to do any let size = self.bounds.width / CGFloat(pictograms.count + 1) kind of calculations.
Also, to make design-time a little easier, give the stack view a Placeholder intrinsic height:
That will have no effect at run-time, but allows you to clearly see your cell elements at design time.
Now, when you "fill" the stack view with image views, no .frame = setting, and the only constraint you need to add is Height == Width:
NSLayoutConstraint.activate([
// image view should have 1:1 ratio
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
])
Here is a complete example:
class HorizontalImagesStackCell: UITableViewCell {
#IBOutlet var theStack: UIStackView!
func addImages(_ pictograms: [String]) -> Void {
// cells are reused, so remove any previously added image views
theStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
pictograms.forEach { s in
// make sure we can load the image
if let img = UIImage(named: s) {
// instantiate an image view
let imageView = UIImageView()
// give it a background color so we can see its frame
imageView.backgroundColor = .green
// scale aspect fit
imageView.contentMode = .scaleAspectFit
// set the image
imageView.image = img
NSLayoutConstraint.activate([
// image view should have 1:1 ratio
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
])
// add it to the stack
theStack.addArrangedSubview(imageView)
}
}
}
}
class ImagesInStackTableViewController: UITableViewController {
// we'll display 5 rows of images
// going from 5 images to 4 images ... to 1 image
let myData: [Int] = [5, 4, 3, 2, 1]
let myImages: [String] = [
"img1", "img2", "img3", "img4", "img5"
]
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HCell", for: indexPath) as! HorizontalImagesStackCell
// get the first n number of images
let a: [String] = Array(myImages.prefix(myData[indexPath.row]))
cell.addImages(a)
return cell
}
}
Using these 5 images:
We get this result (image views are set to .scaleAspectFit and have green backgrounds so we can see the frames):

UITableViewCell height dynamically calculated with autolayout based on row content

I've prepared UITableViewCell in nib file with all constraints from top to bottom. Calculating row height in table view seems to work.
But if I want to activate/deactivate some other constraints in the cell based on some row data condition then I have problem because it seems like the height is not calculated then. What is the problem? In the following example TableRowDM.hidden means should subtitle be hidden or not. I'm hidding this UILabel by activating not activated constraint (height with constant = 0) and setting vertical gap between titleLabel and subtitleLabel to 0.
struct TableRowDM {
let title: String
let subTitle: String
let hidden: Bool
}
let cellId: String = "CellView"
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let rowData: [TableRowDM] = [
TableRowDM(title: "Row 1", subTitle: "Short title 1th row", hidden: false),
TableRowDM(title: "Row 2", subTitle: "Short title 2th row", hidden: true),
TableRowDM(title: "Row 3", subTitle: "Very long text in subtitle at 3th row to test text wrapping and growing subtitle height", hidden: false),
TableRowDM(title: "Row 4", subTitle: "Long text in subtitle at 4th row", hidden: false),
TableRowDM(title: "Row 5", subTitle: "Long text in subtitle at 5th row", hidden: true),
TableRowDM(title: "Row 6", subTitle: "Long text in subtitle at 6th row", hidden: false),
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 50
tableView.tableFooterView = UIView(frame: CGRect.zero)
tableView.register(UINib(nibName: cellId, bundle: nil), forCellReuseIdentifier: cellId)
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CellView
let row: TableRowDM = self.rowData[indexPath.row]
cell.title.text = row.title
cell.subtitle.text = row.subTitle
if row.hidden {
// hide subtitle
cell.subtitleHeight.isActive = true
cell.titleToSubtitleGap.constant = 0
} else {
// show subtitle
cell.subtitleHeight.isActive = false
cell.titleToSubtitleGap.constant = 16
}
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowData.count
}
}
It seems that this is not working and subtitleLabel is not hiding and row height is always the same. When contents of subtitleLabel and titleLabel changes then the height is adjusted to the text inside those UIViews but when I try to manipulate with constraints then it is not working. Any help? What Am I doing wrong? The CellView is very simple view with such subviews
-----------------
|
- titleLabel ----
|
| <- titleToSubtitleGap
|
- subtitleLabel -
|
-----------------
I found a solution for my question. It is not documented anywhere but it works.
What is important. If You are supporting landscape/portrait orientations then it is important to set right priorities in NSLayoutConstraints that defines vertical unbroken chain of constraints and views (with defined heights) as stated here - they wrote:
Next, lay out the table view cell’s content within the cell’s content
view. To define the cell’s height, you need an unbroken chain of
constraints and views (with defined heights) to fill the area between
the content view’s top edge and its bottom edge. If your views have
intrinsic content heights, the system uses those values. If not, you
must add the appropriate height constraints, either to the views or to
the content view itself.
What they didn't wrote is that all those vertical constraints that define finally the height of the table cell contentView must have priorities less than required (1000) if You need to support many device orientations. I set in my examples those priorities to High (750).
Next it is important to make changes with those constraints in updateConstraints method. So if You are designing a UIView and when some property of this view need to change constraints then You need to do it something like this in Your view so updateConstraints method will be called in right moment. After setting the property new value call needsUpdateConstraints() so AutoLayout will know that constraints in our UIView changed.
import UIKit
class ListItemTableViewCell: UITableViewCell {
#IBOutlet weak var subtitleToTitleGap: NSLayoutConstraint!
#IBOutlet weak var subtitleHeightZero: NSLayoutConstraint!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subtitleLabel: UILabel!
public var isSubtitleHidden: Bool {
get {
return self.subtitleLabel.isHidden
}
set {
// Here we are changing property that change layout in our view
self.subtitleLabel.isHidden = newValue
self.needsUpdateConstraints() // Here we are telling AutoLayout to recalculate constraints in `updateConstraints` method.
}
}
#if !TARGET_INTERFACE_BUILDER
// We need to block this code from running in Interface Builder
override func updateConstraints() {
if self.isSubtitleHidden {
// Hide Subtitle
self.subtitleToTitleGap.constant = 0.0
self.subtitleHeightZero.autoInstall() // This makes subtitle height to be 0
} else {
// Show Subtitle
self.subtitleToTitleGap.constant = 2.0
self.subtitleHeightZero.autoRemove() // This will make subtitle height to be self calculated
}
super.updateConstraints() // don't forget to call this after You finished constraints changes
}
#endif
}
And the most important and it was the most difficult for me to find out is to hide implementation of updateConstraints from interface builder because it was causing IB to crash from time to time in xib editor. Look above for this example. The method is wrapped in #if preprocessor macro. So I think thats it. Good Luck!

Make UIButton stick on bottom of UITableView

I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.

Setting tableView header's height in Swift

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

Resources