UITableView with UITableViewCells + AutoLayout - Not As Smooth As It *Should* Be - ios

I recently posted a question about a UITableView with custom UITableCells that was not smooth when the cell's subviews were positioned with AutoLayout. I got some comments suggesting the lack of smoothness was due to the complex layout of the cells. While I agree that the more complex the cell layout, the more calculation the tableView has to do to get the cell's height, I don't think 10-12 UIView and UILabel subviews should cause the amount of lag I was seeing as I scrolled on an iPad.
So to prove my point further, I created a single UIViewController project with a single UITableView subview and custom UITableViewCells with only 2 labels inside of their subclass. And the scrolling is still not perfectly smooth. From my perspective, this is the most basic you can get - so if a UITableView is still not performant with this design, I must be missing something.
The estimatedRowHeight of 110 used below is a very close estimate to the actual row height average. When I used the 'User Interface Inspector' and looked at the heights of each cell, one by one, they ranged from 103 - 124.
Keep in mind, when I switch the code below to not use an estimatedRowHeight and UITableViewAutomaticDimension and instead implement func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {, calculating the height with frame values, the UITableView scrolls like butter.
Screenshot of App (for reference)
Source code of the App (where the scrolling is not perfectly smooth)
// The custom `Quote` object that holds the
// properties for our data mdoel
class Quote {
var text: String!
var author: String!
init(text: String, author: String) {
self.text = text
self.author = author
}
}
// Custom UITableView Cell, using AutoLayout to
// position both a "labelText" (the quote itself)
// and "labelAuthor" (the author's name) label
class CellQuote: UITableViewCell {
private var containerView: UIView!
private var labelText: UILabel!
private var labelAuthor: UILabel!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = UIColor.whiteColor()
containerView = UIView()
containerView.backgroundColor = UIColor(
red: 237/255,
green: 237/255,
blue: 237/255,
alpha: 1.0
)
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leadingAnchor.constraintEqualToAnchor(contentView.leadingAnchor, constant: 0).active = true
containerView.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor, constant: 0).active = true
containerView.topAnchor.constraintEqualToAnchor(contentView.topAnchor, constant: 4).active = true
containerView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor, constant: 0).active = true
labelText = UILabel()
labelText.numberOfLines = 0
labelText.font = UIFont.systemFontOfSize(18)
labelText.textColor = UIColor.darkGrayColor()
containerView.addSubview(labelText)
labelText.translatesAutoresizingMaskIntoConstraints = false
labelText.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
labelText.topAnchor.constraintEqualToAnchor(containerView.topAnchor, constant: 20).active = true
labelText.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true
labelAuthor = UILabel()
labelAuthor.numberOfLines = 0
labelAuthor.font = UIFont.boldSystemFontOfSize(18)
labelAuthor.textColor = UIColor.blackColor()
containerView.addSubview(labelAuthor)
labelAuthor.translatesAutoresizingMaskIntoConstraints = false
labelAuthor.topAnchor.constraintEqualToAnchor(labelText.bottomAnchor, constant: 20).active = true
labelAuthor.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
labelAuthor.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true
labelAuthor.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor, constant: -20).active = true
self.selectionStyle = UITableViewCellSelectionStyle.None
}
func configureWithData(quote: Quote) {
labelText.text = quote.text
labelAuthor.text = quote.author
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// The UIViewController that is a
class ViewController: UIViewController, UITableViewDataSource {
var tableView: UITableView!
var dataItems: [Quote]!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.dataSource = self
tableView.registerClass(CellQuote.self, forCellReuseIdentifier: "cellQuoteId")
tableView.backgroundColor = UIColor.whiteColor()
tableView.separatorStyle = UITableViewCellSeparatorStyle.None
tableView.estimatedRowHeight = 110
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
tableView.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 20).active = true
tableView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
tableView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
dataItems = [
Quote(text: "One kernel is felt in a hogshead; one drop of water helps to swell the ocean; a spark of fire helps to give light to the world. None are too small, too feeble, too poor to be of service. Think of this and act.", author: "Michael.Frederick"),
Quote(text: "A timid person is frightened before a danger, a coward during the time, and a courageous person afterward.", author: "Lorem.Approbantibus."),
Quote(text: "There is only one way to defeat the enemy, and that is to write as well as one can. The best argument is an undeniably good book.", author: "Lorem.Fruitur."),
// ... many more quotes ...
]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - UITableViewDataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataItems.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellQuoteId") as! CellQuote
cell.configureWithData(dataItems[indexPath.row])
return cell
}
}
I like matt's suggestion below, but am still trying to tweak it to work for me:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var cellHeights: [CGFloat] = [CGFloat]()
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if cellHeights.count == 0 {
var cellHeights = [CGFloat]()
let numQuotes: Int = dataItems.count
for index in 0...numQuotes - 1 {
let cell = CellQuote()
let quote = dataItems[index]
cell.configureWithData(quote)
let size = cell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
cellHeights.append(size.height)
}
self.cellHeights = cellHeights
}
return self.cellHeights[indexPath.row]
}
}

I've never found the automatic row height mechanism to be as smooth as the old calculated layout techniques that we used to use before auto layout came along. The bottleneck, as you can readily see by using Instruments, is that the runtime must call systemLayoutSizeFittingSize: on every new cell as it scrolls into view.
In my book, I demonstrate my preferred technique, which is to calculate the heights for all the cells once when the table view first appears. This means that I can supply the answer to heightForRowAtIndexPath instantly from then on, making for the best possible user experience. Moreover, if you then replace your call to dequeueReusableCellWithIdentifier with the much better and more modern dequeueReusableCellWithIdentifier:forIndexPath, you have the advantage that the cell comes to you with its correct size and no further layout is needed after that point.

the clear background from your two text labels is causing the performance issues. add these lines and you should see a performance increase:
labelText.backgroundColor = containerView.backgroundColor
labelAuthor.backgroundColor = containerView.backgroundColor
a good way to check any other potential issues is by turning on 'Color Blended Layers' in the iOS Simulator's 'Debug' menu option
UPDATE
usually what i do for dynamic cell heights is create a prototype cell and use it for sizing. here is what you'd do in your case:
class CellQuote: UITableViewCell {
private static let prototype: CellQuote = {
let cell = CellQuote(style: .Default, reuseIdentifier: nil)
cell.contentView.translatesAutoresizingMaskIntoConstraints = false
return cell
}()
static func heightForQuote(quote: Quote, tableView:UITableView) -> CGFloat {
prototype.configureWithData(quote)
prototype.labelText.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40
prototype.labelAuthor.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40
prototype.layoutIfNeeded();
return CGRectGetHeight(prototype.contentView.frame)
}
// existing code here
}
in your viewDidLoad remove the rowHeight and estimatedRowHeight lines and replace with becoming the delegate
class ViewController {
override func viewDidLoad() {
// existing code
self.tableView.delegate = self
// existing code
}
// get prototype cell height
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let quote = dataItems[indexPath.row]
return CellQuote.heightForQuote(quote, tableView: tableView)
}

Related

UIKit: Constraints on a UITableViewCell don't work properly for the first few cells in a table view

I have the following UITableViewCell subclass:
import Foundation
import UIKit
class FlagListTableViewCell: UITableViewCell, ReusableView {
private let flagImageView = UIImageView()
private var cellHeightConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.cellHeightConstraint = self.flagImageView.heightAnchor.constraint(equalToConstant: 0)
self.cellHeightConstraint.isActive = true
self.flagImageView.contentMode = .scaleAspectFit
self.flagImageView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.flagImageView)
// Constraints the top, leading, trailing, and bottom anchors of `flsgImageView` to `self.contentView`
self.flagImageView.pinEdges(to: self.contentView)
}
#available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func bind(to country: Country) {
let image = UIImage(named: country.flagImageName)!
self.flagImageView.image = image
let imageViewWidth = self.contentView.frame.width
let imageWidth = CGFloat(self.flagImageView.image!.cgImage!.width)
let imageHeight = CGFloat(self.flagImageView.image!.cgImage!.height)
let scaledHeight = imageHeight * (imageViewWidth / imageWidth)
self.cellHeightConstraint.constant = scaledHeight
}
}
I have a UITableViewController subclass which displays ~250 of these cells (as reusable cells).
When I display the table view however, the first three cells (or however many the device screen size fits) have incorrect constraints for some reason and cause the cells to look incorrect. However, for all other cells, they look fine. Additionally, when I scroll down and then back up to the first three cells, they look fine as well.
Anyone know what could be causing this and how to fix it?
I have tried the following solutions, and none have worked:
Implementing the heightForRowAt delegate method and returning UITableView.automaticDimension, and also implementing the estimatedHeightForRowAt delegate and returning various values for it
Implementing layoutSubviews method for the table view cell class to update the constraint, and then calling setNeedsLayout and layoutIfNeeded.
Here is a screenshot of the cells with the incorrect constraints:
And here is a screenshot of the cells working as intended:
Edit:
I call bind(to:) in the table view's diffable data source's cell provider:
private lazy var dataSource: FlagListDataSource = {
let source = FlagListDataSource(tableView: self.tableView, cellProvider: self.cellProvider)
source.viewModel = self.viewModel
return source
}()
private func cellProvider(tableView: UITableView, indexPath: IndexPath, countryId: Country.ID) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(FlagListTableViewCell.self, for: indexPath)
let country = self.viewModel.country(id: countryId)
cell.bind(to: country)
return cell
}
It looks like there's a conflict in how you're setting constraints for your flagImageView; you're activating a height constraint for it, but then you're also pinning all four of its edges. And you're setting the content mode to .scaleAspectFit but it seems like what you actually want is .scaleAspectFill.
Try these 2 changes in your init and see if that works.
self.cellHeightConstraint.isActive = false
self.flagImageView.contentMode = .scaleAspectFill

UITableViewCell with a StackView inside with dynamically added WKWebViews and UIImageViews

As the title says, I'm trying to display the following layout:
As you see, the dynamic stack view is a container where content is added dynamically. This content is variable and is decided on run time. Basically, it can be webviews (with variable content inside), ImageViews (with variable height), and videos (this view would have a fixed view).
I configured the CellView with automatic row height, and provided an estimated row height, both in code and in Xcode. Then on the tableView_cellForRow at the method of the ViewController, the cell is dequeued and the cell is rendered with content.
During this setup process, the different labels and views are filled with content, and the dynamic container too. The webviews are added to the stackview with the following code:
var webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.isScrollEnabled = false
webView.navigationDelegate = myNavigationDelegate
webView = addContentToWebView(content, webView)
container.addArrangedSubview(webView)
I'm testing this with only a webview inside the stackview and having already problems with the height of the row.
The webview is rendered correctly inside the stackview, but not completely (the webview was bigger as the estimated rowheight). I used the navigation delegate to calculate the height of the added webview and resize the StackContainer accordingly, with the following code:
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let h = height as! CGFloat
print("Height 3 is \(h)")
self.dynamicContainerHeightContraint.constant = h
})
}
})
And indeed, the stackcontainer is resized and expanded to match the height of the webview that is inside.
But the row remains with the same estimated height, and if the webview is very big in height, then all the other views disappear (they are pushed outside the bounds of the row.
Is there a way to tell the row to autoresize and adapt to its contents? Or maybe I'm using the false approach?
I suppose the problem is that the height of the views added to the stackview is not known in advance, but I was expecting a way to tell the row to recalculate its height after adding all the needed stuff inside...
Thank you in advance.
Table views do not automatically redraw their cells when a cell's content changes.
Since you are changing the constant of your cell's dynamicContainerHeightContraint after the cell has been rendered (your web view's page load is asynchronous), the table does not auto-update -- as you've seen.
To fix this, you can add a "callback" closure to your cell, which will let the cell tell the controller to recalculate the layout.
Here is a simple example to demonstrate.
The cell has a single label... it has a "label height constraint" var that initially sets the height of the label to 30.
For the 3rd row, we'll set a 3-second timer to simulate the delayed page load in your web view. After 3 seconds, the cell's code will change the height constant to 80.
Here's how it looks to start:
Without the callback closure, here's how it looks after 3 seconds:
With the callback closure, here's how it looks after 3 seconds:
And here's the sample code.
DelayedCell UITableViewCell class
class DelayedCell: UITableViewCell {
let myLabel = UILabel()
var heightConstraint: NSLayoutConstraint!
// closure to tell the controller our content changed height
var callback: (() -> ())?
var timer: Timer?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.clipsToBounds = true
myLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(myLabel)
let g = contentView.layoutMarginsGuide
// we'll change this dynamically
heightConstraint = myLabel.heightAnchor.constraint(equalToConstant: 30.0)
// use bottom anchor with Prioirty: 999 to avoid auto-layout complaints
let bc = myLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
bc.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain label to all 4 sides
myLabel.topAnchor.constraint(equalTo: g.topAnchor),
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// activate bottom and height constraints
bc,
heightConstraint,
])
}
func fillData(_ str: String, testTimer: Bool) -> Void {
myLabel.text = str
// so we can see the label frame
// green if we're testing the timer in this cell
// otherwise yellow
myLabel.backgroundColor = testTimer ? .green : .yellow
if testTimer {
// trigger a timer in 3 seconds to change the height of the label
// simulating the delayed load of the web view
timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.heightChanged), userInfo: nil, repeats: false)
}
}
#objc func heightChanged() -> Void {
// change the height constraint
heightConstraint.constant = 80
myLabel.text = "Height changed to 80"
// run this example first with the next line commented
// then run it again but un-comment the next line
// tell the controller we need to update
//callback?()
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
timer?.invalidate()
}
}
}
DelayTestTableViewController UITableViewController class
class DelayTestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DelayedCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DelayedCell
// we'll test the delayed content height change for row 2
let bTest = indexPath.row == 2
cell.fillData("Row \(indexPath.row)", testTimer: bTest)
// set the callback closure
cell.callback = { [weak tableView] in
guard let tv = tableView else { return }
// this will tell the tableView to recalculate row heights
// without reloading the cells
tv.performBatchUpdates(nil, completion: nil)
}
return cell
}
}
In your code, you would make the closure callback after this line:
self.dynamicContainerHeightContraint.constant = h

Cells in UITableView overlapping. Many cells in one place

I'm trying to make table view with random numbers of labels in. Everything is working till I try too scroll it. Than many some of cells appear in one place. It looks like this:
Screen from simulation
To make random row height in viewDidLoad() I put this:
tableView.estimatedRowHeight = 50.0
tableView.rowHeight = UITableViewAutomaticDimension
The code going to write randoms number of labels with random number of lines is here:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HarvestPlan", for: indexPath) as! HarvestPlanCell
let currentSpecies = harvestPlan[indexPath.row]
var kindLabels = [UILabel]()
cell.kindsNamesView.bounds.size.width = 100
for kind in currentSpecies.kinds {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = kind.fullName
label.bounds.size.width = 100
label.sizeToFit()
label.bounds.size.width = 100
cell.kindsNamesView.addSubview(label)
kindLabels.append(label)
}
var previous: UILabel!
for label in kindLabels {
label.widthAnchor.constraint(equalToConstant: 100).isActive = true
label.heightAnchor.constraint(equalToConstant: label.bounds.height).isActive = true
label.leadingAnchor.constraint(equalTo: cell.kindsNamesView.leadingAnchor).isActive = true
if previous == nil {
label.topAnchor.constraint(equalTo: cell.kindsNamesView.topAnchor).isActive = true
}
if previous != nil {
label.topAnchor.constraint(equalTo: previous.bottomAnchor).isActive = true
}
if label == kindLabels.last {
label.bottomAnchor.constraint(equalTo: cell.kindsNamesView.bottomAnchor).isActive = true
}
previous = label
}
return cell
Someone have some idea how to repair it? I'm looking for answer since one week and I did't find anything about it...
Thank you #Paulw11, prepareForReuse was this what I was looking for. If someone have similar problem, the answer is code below added to UITableViewCell:
override func prepareForReuse() {
super.prepareForReuse()
for view in kindsNames.subviews { //take all subviews from your view
view.removeFromSuperview() //delete it from you view
}
}
Cheers

How to determine the "z-index of a shadow"

I'm sorry for the title, can not synthesize differently the issue. Here's the problem:
In my UICollectionView have some cells that want to put a shadow, they are arranged very close to each other which makes the shadow of each one reach the neighbor (first image), when what I want, is that it only reaches the background (second image).
What I've thought or tried:
I can not put a view behind the cells, adding their frames to it, and apply shadow in this view because the cells has dynamic movement (UIDynamics CollectionView Layout).
I tried, in the subclass of UICollectionViewLayout, put all these cells in the same z-index. Did not work. Find out why:
var zIndex: Int
(...) Items with the same value have an undetermined
order.
I would like some help with my problem, please. Thanks!
From the TheEye response, I decided to implement UIDecorationViews. All great now.
// MARK: At UICollectionViewCustomLayout:
public override init() {
super.init()
// Register the NIB of the view that will hold the shadows:
let nib = UINib(nibName: "Shadow", bundle: nil)
self.registerNib(nib, forDecorationViewOfKind: "shadow")
}
public override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAtt: UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: "shadow", withIndexPath: indexPath)
layoutAtt.frame = (layoutAttributesForItemAtIndexPath(indexPath)?.frame)!
layoutAtt.zIndex = -1
return layoutAtt
}
public override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var atts = [UICollectionViewLayoutAttributes]()
let numberOfItems:Int = (self.collectionView?.numberOfItemsInSection(0))!
for index in 0..<numberOfItems {
let layoutItem = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: index, inSection: 0))!
let frameItem = layoutItem.frame
if CGRectIntersectsRect(frameItem, rect) {
atts.append(layoutAttributesForDecorationViewOfKind("shadow", atIndexPath: NSIndexPath(forItem: index, inSection: 0))!)
atts.append(layoutItem)
}
}
return atts
}
// MARK: At the Nib Class File:
// Here I created a additional view to append the shadow.
// Thats because awakeFromNib() is called before the UICollectionViewCustomLayout
// have a chance to layout it, but I want to make
// use of shadowPath to gain performance, thats why
// I make use of the additional UIView with autolayout stuffs.
#IBOutlet weak var shadowView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
shadowView.layer.shadowOffset = CGSizeMake(0, 0)
shadowView.layer.shadowColor = UIColor.yellowColor().CGColor
shadowView.layer.shadowRadius = 3
shadowView.layer.shadowOpacity = 1
let shadowFrame: CGRect = shadowView.layer.bounds
let shadowPath: CGPathRef = UIBezierPath(rect: shadowFrame).CGPath
shadowView.layer.shadowPath = shadowPath
shadowView.clipsToBounds = false
self.clipsToBounds = false
}
You could try to define the shadows as supplementary views, aligned with their respective cells, and give them a lower z order than the cells.

How to add spacing between UITableViewCell

Is there any way to add spacing between UITableViewCell?
I have created a table and each cell only contain an image. The image is assigned to the cell like this:
cell.imageView.image = [myImages objectAtIndex:indexPath.row];
but this make the image enlarged and fit into the whole cell, and there are no spacing between the images.
Or lets say in this way, the height of image are e.g. 50, and I want to add 20 spacing between the images. Is there any way to accomplish this?
My easy solution using Swift :
// Inside UITableViewCell subclass
override func layoutSubviews() {
super.layoutSubviews()
contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
}
Result
Swift Version
Updated for Swift 3
This answer is somewhat more general than the original question for the sake of future viewers. It is a supplemental example to the basic UITableView example for Swift.
Overview
The basic idea is to create a new section (rather than a new row) for each array item. The sections can then be spaced using the section header height.
How to do it
Set up your project as described in UITableView example for Swift. (That is, add a UITableView and hook up the tableView outlet to the View Controller).
In the Interface Builder, change the main view background color to light blue and the UITableView background color to clear.
Replace the ViewController.swift code with the following.
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// These strings will be the data for the table view cells
let animals: [String] = ["Horse", "Cow", "Camel", "Sheep", "Goat"]
let cellReuseIdentifier = "cell"
let cellSpacingHeight: CGFloat = 5
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// These tasks can also be done in IB if you prefer.
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
tableView.delegate = self
tableView.dataSource = self
}
// MARK: - Table View delegate methods
func numberOfSections(in tableView: UITableView) -> Int {
return self.animals.count
}
// There is just one row in every section
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// Set the spacing between sections
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return cellSpacingHeight
}
// Make the background color show through
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = UIColor.clear
return headerView
}
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
// note that indexPath.section is used rather than indexPath.row
cell.textLabel?.text = self.animals[indexPath.section]
// add border and color
cell.backgroundColor = UIColor.white
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
cell.clipsToBounds = true
return cell
}
// method to run when table view cell is tapped
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// note that indexPath.section is used rather than indexPath.row
print("You tapped cell number \(indexPath.section).")
}
}
Note that indexPath.section is used rather than indexPath.row in order to get the proper values for the array elements and tap positions.
How did you get the extra padding/space on the right and left?
I got it the same way you add spacing to any view. I used auto layout constraints. Just use the pin tool in the Interface Builder to add spacing for the leading and trailing constraints.
The way I achieve adding spacing between cells is to make numberOfSections = "Your array count" and make each section contains only one row. And then define headerView and its height.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return yourArry.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 1;
}
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return cellSpacingHeight;
}
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *v = [UIView new];
[v setBackgroundColor:[UIColor clearColor]];
return v;
}
I needed to do the same concept of having UITableCells have a "space" between them. Since you can't literally add space between cells you can fake it by manipulating the UITableView's cell height and then adding a UIView to the contentView of your cell. Here is a screen shot of a prototype I did in another test project when I was simulating this:
Here is some code (Note: there are lots of hard coded values for demonstration purposes)
First, I needed to set the heightForRowAtIndexPath to allow for different heights on the UITableViewCell.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [self.newsArray objectAtIndex:[indexPath row]];
if ([text isEqual:#"December 2012"])
{
return 25.0;
}
return 80.0;
}
Next, I want to manipulate the look and feel of the UITableViewCells so I do that in the willDisplayCell:(NewsUITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method.
- (void)tableView:(UITableView *)tableView willDisplayCell:(NewsUITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (cell.IsMonth)
{
UIImageView *av = [[UIImageView alloc] initWithFrame:CGRectMake(20, 20, 20, 20)];
av.backgroundColor = [UIColor clearColor];
av.opaque = NO;
av.image = [UIImage imageNamed:#"month-bar-bkgd.png"];
UILabel *monthTextLabel = [[UILabel alloc] init];
CGFloat font = 11.0f;
monthTextLabel.font = [BVFont HelveticaNeue:&font];
cell.backgroundView = av;
cell.textLabel.font = [BVFont HelveticaNeue:&font];
cell.textLabel.textColor = [BVFont WebGrey];
}
if (indexPath.row != 0)
{
cell.contentView.backgroundColor = [UIColor clearColor];
UIView *whiteRoundedCornerView = [[UIView alloc] initWithFrame:CGRectMake(10,10,300,70)];
whiteRoundedCornerView.backgroundColor = [UIColor whiteColor];
whiteRoundedCornerView.layer.masksToBounds = NO;
whiteRoundedCornerView.layer.cornerRadius = 3.0;
whiteRoundedCornerView.layer.shadowOffset = CGSizeMake(-1, 1);
whiteRoundedCornerView.layer.shadowOpacity = 0.5;
[cell.contentView addSubview:whiteRoundedCornerView];
[cell.contentView sendSubviewToBack:whiteRoundedCornerView];
}
}
Note that I made my whiteRoundedCornerView height 70.0 and that's what causes the simulated space because the cell's height is actually 80.0 but my contentView is 70.0 which gives it the appearance.
There might be other ways of accomplishing this even better but it's just how I found how to do it. I hope it can help someone else.
I was in the same boat. At first I tried switching to sections, but in my case it ended up being more of a headache than I originally thought, so I've been looking for an alternative. To keep using rows (and not mess with how you access your model data), here's what worked for me just by using a mask:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
let verticalPadding: CGFloat = 8
let maskLayer = CALayer()
maskLayer.cornerRadius = 10 //if you want round edges
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.frame = CGRect(x: cell.bounds.origin.x, y: cell.bounds.origin.y, width: cell.bounds.width, height: cell.bounds.height).insetBy(dx: 0, dy: verticalPadding/2)
cell.layer.mask = maskLayer
}
All you have left to do is make the cell's height bigger by the same value as your desired verticalPadding, and then modify your inner layout so that any views that had spacing to the edges of the cell have that same spacing increased by verticalPadding/2. Minor downside: you get verticalPadding/2 padding on both the top and bottom of the tableView, but you can quickly fix this by setting tableView.contentInset.bottom = -verticalPadding/2 and tableView.contentInset.top = -verticalPadding/2. Hope this helps somebody!
You will have to set frame to your image. Untested code is
cell.imageView.frame = CGRectOffset(cell.frame, 10, 10);
I override this function is subclass of UITableViewCell, and it works OK for me
override func layoutSubviews() {
super.layoutSubviews()
//set the values for top,left,bottom,right margins
let margins = UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8)
contentView.frame = contentView.frame.inset(by: margins)
contentView.layer.cornerRadius = 8
}
I think the most straight forward solution if your just looking for a little space and probably least expensive would be to simply set the cell border color to your tables background color then set the border width to get desired result!
cell.layer.borderColor = blueColor.CGColor
cell.layer.borderWidth = 3
Use sections instead of rows
Each section should return one row
Assign your cell data using indexPath.section, instead of row
Implement UITableView delegate method heightForHeader and return your desired spacing
I solved it like this way in Swift 4.
I create a extension of UITableViewCell and include this code:
override open var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
var frame = newFrame
frame.origin.y += 10
frame.origin.x += 10
frame.size.height -= 15
frame.size.width -= 2 * 10
super.frame = frame
}
}
override open func awakeFromNib() {
super.awakeFromNib()
layer.cornerRadius = 15
layer.masksToBounds = false
}
I hope it helps you.
Change the number of rows in section to 1
You have changed number of sections instead number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
Here you put spacing between rows
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
If you are not using section headers (or footers) already, you can use them to add arbitrary spacing to table cells. Instead of having one section with n rows, create a table with n sections with one row each.
Implement the tableView:heightForHeaderInSection: method to control the spacing.
You may also want to implement tableView:viewForHeaderInSection: to control what the spacing looks like.
Example in swift 3..
Crease a single view application
add tableview in view controller
add a customcell for tablview cell
view controller code is bellow like
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var arraytable = [[String:Any]]()
override func viewDidLoad() {
super.viewDidLoad()
arraytable = [
["title":"About Us","detail":"RA-InfoTech Ltd -A Joint Venture IT Company formed by Bank Asia Ltd"],
["title":"Contact","detail":"Bengal Center (4th & 6th Floor), 28, Topkhana Road, Dhaka - 1000, Bangladesh"]
]
tableView.delegate = self
tableView.dataSource = self
//For Auto Resize Table View Cell;
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableViewAutomaticDimension
//Detault Background clear
tableView.backgroundColor = UIColor.clear
}
func numberOfSections(in tableView: UITableView) -> Int {
return arraytable.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// Set the spacing between sections
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 10
}
// Make the background color show through
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = UIColor.clear
return headerView
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")! as! CustomCell
cell.tv_title.text = arraytable[indexPath.section]["title"] as! String?
cell.tv_details.text = arraytable[indexPath.section]["detail"] as! String?
//label height dynamically increase
cell.tv_details.numberOfLines = 0
//For bottom border to tv_title;
let frame = cell.tv_title.frame
let bottomLayer = CALayer()
bottomLayer.frame = CGRect(x: 0, y: frame.height - 1, width: frame.width, height: 1)
bottomLayer.backgroundColor = UIColor.black.cgColor
cell.tv_title.layer.addSublayer(bottomLayer)
//borderColor,borderWidth, cornerRadius
cell.backgroundColor = UIColor.lightGray
cell.layer.borderColor = UIColor.red.cgColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
cell.clipsToBounds = true
return cell
}
}
Download full source to Github : link
https://github.com/enamul95/CustomSectionTable
Three approaches I can think of:
Create a custom table cell that lays out the view of the entire cell in the manner that you desire
Instead of adding the image to the
image view, clear the subviews of
the image view, create a custom
view that adds an UIImageView for the image and another view, perhaps a simple UIView that provides the desired spacing, and add it as a subview of the
image view.
I want to suggest that you manipulate the UIImageView directly to set a fixed size/padding, but I'm nowhere near Xcode so I can't confirm whether/how this would work.
Does that make sense?
Yes you can increase or decrease the spacing(padding) between two cell by creating one base view on content view in cell.Set clear colour for content view background and you can adjust the height of the base view to create space between cells.
Based on Husam's answer: Using the cell layer instead of content view allows for adding a border around the entire cell and the accessory if need. This method requires careful adjustment of the bottom constraints of the cell as well as those insets otherwise the view will not proper.
#implementation TableViewCell
- (void)awakeFromNib {
...
}
- (void) layoutSubviews {
[super layoutSubviews];
CGRect newFrame = UIEdgeInsetsInsetRect(self.layer.frame, UIEdgeInsetsMake(4, 0, 4, 0));
self.layer.frame = newFrame;
}
#end
Read this after reading other people answers
I'd like to warn everyone who wants to use the solution like adding headers that will serve the purpose of spacing. If you do this, you will not be able to animate cells insertions, deletions, etc.. For example, you may get this kind of error if you use that method
Invalid update: invalid number of sections. The number of sections contained in the table view after the update (6) must be equal to the number of sections contained in the table view before the update (5), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).
In case you need to animate insertions and deletions of rows I would go with adding this space in the cells itself. If you are concern about highlighting, then you can override method
func setHighlighted(_ highlighted: Bool, animated: Bool)
and set the highlighting yourself
I think this is the cleanest solution:
class MyTableViewCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
layoutMargins = UIEdgeInsetsMake(8, 0, 8, 0)
}
}
This article helped, it's pretty much what the other answers said but summarize and concise
https://medium.com/#andersongusmao/left-and-right-margins-on-uitableviewcell-595f0ba5f5e6
In it, he only applies them to left and right sides but the UIEdgeInsetsMake init allows to add padding to all four points.
func UIEdgeInsetsMake(_ top: CGFloat, _ left: CGFloat, _ bottom: CGFloat, _ right: CGFloat) -> UIEdgeInsets
Description
Creates an edge inset for a button or view.
An inset is a margin around a rectangle. Positive values represent margins closer to the center of the rectangle, while negative values represent margins further from the center.
Parameters
top: The inset at the top of an object.
left: The inset on the left of an object
bottom: The inset on the bottom of an object.
right: The inset on the right of an object.
Returns
An inset for a button or view
Note that UIEdgeInsets can also be used to achieve the same.
Xcode 9.3/Swift 4
Using the headers as spacing would work fine I guess if you don't want to use any headers. Otherwise, probably not the best idea. What I'm thinking is create a custom cell view.
Examples:
Using Nib
In code
In the custom cell, make a background view with constraints so that it doesn't fill the entire cell, give it some padding.
Then, make the tableview background invisible and remove the separators:
// Make the background invisible
tableView.backgroundView = UIView()
tableView.backgroundColor = .clear
// Remove the separators
tableview.separatorStyle = .none
If you don't want to change the section and row number of your table view (like I did), here's what you do:
1) Add an ImageView to the bottom of your table cell view.
2) Make it the same colour as the background colour of the table view.
I've done this in my application and it works perfectly. Cheers! :D
Using a bunch of different sections is not needed. The other answers use frame insets and CGRect and layers and... BLAH. Not good; use auto layout and a custom UITableViewCell. In that UITableViewCell, instead of sub viewing your content inside the contentView, make a new containerView (a UIView), subview the container view inside the contentView, then subview all your views inside the container view.
To make the spacing now, simply edit the layout margins of the container view, like so:
class CustomTableViewCell: UITableViewCell {
let containerView = UIView()
let imageView = UIImageView()
required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder)}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
containerView.translatesAutoResizingMaskIntoConstraints = false
imageView.translatesAutoResizingMaskIntoConstraints = false
contentView.addSubview(containerView)
containerView.addSubview(imageView)
contentView.layoutMargins = UIEdgeInsets(top: 15, left: 3, bottom: 15, right: 3)
containerView.layoutMargins = UIEdgeInsets(top: 15, left: 17, bottom: 15, right: 17) // It isn't really necessary unless you've got an extremely complex table view cell. Otherwise, you could just write e.g. containerView.topAnchor
let cg = contentView.layoutMarginsGuide
let lg = containerView.layoutMarginsGuide
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: cg.topAnchor),
containerView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
imageView.topAnchor.constraint(equalTo: lg.topAnchor),
imageView.leadingAnchor.constraint(equalTo: lg.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: lg.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: lg.bottomAnchor)
])
}
}
Try looking into
- (UIEdgeInsets)layoutMargins;
on the cell
My situation was i used custom UIView to viewForHeader in section also heightForHeader in section return constant height say 40, issue was when there is no data all header views were touched to each other. so i wanted to space between the section in absent of data so i fixed by just changing "tableview style" plane to "Group".and it worked for me.
Check out my solution on GitHub with subclassing of UITableView and using runtime features of Objective-C.
It basically uses Apple's private data structure UITableViewRowData that I got searching private runtime header of UITableView:
https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UITableView.h,
and here's desired private class that contains everything you need to layout your cells' spacings however you want without setting it in cells' classes:
https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UITableViewRowData.h
I was having trouble getting this to work alongside background colours and accessory views in the cell. Ended up having to:
1) Set the cells background view property with a UIView set with a background colour.
let view = UIView()
view.backgroundColor = UIColor.white
self.backgroundView = view
2) Re-position this view in layoutSubviews to add the idea of spacing
override func layoutSubviews() {
super.layoutSubviews()
backgroundView?.frame = backgroundView?.frame.inset(by: UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)) ?? CGRect.zero
}
You can simply use constraint in code like this :
class viewCell : UITableViewCell
{
#IBOutlet weak var container: UIView!
func setShape() {
self.container.backgroundColor = .blue
self.container.layer.cornerRadius = 20
container.translatesAutoresizingMaskIntoConstraints = false
self.container.widthAnchor.constraint(equalTo:contentView.widthAnchor , constant: -40).isActive = true
self.container.heightAnchor.constraint(equalTo: contentView.heightAnchor,constant: -20).isActive = true
self.container.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
self.container.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
}
it's important to add subview (container) and put other elements in it.
Just adding to the pool of answers with what worked for me.
I’ve added a view (purple view) within the TableViewCell that I use as my cell’s content view. And constrain the purple view to have padding on top and bottom, or however you’d like, but I think this way creates a little more flexibility.
TableViewCell ->
override open var frame: CGRect {
get {
return super.frame
}
set {
var frame = newValue
frame.size.height -= 2
super.frame = frame
}
}
add a inner view to the cell then add your own views to it.

Resources