Skip to main content

How to Set Self-Sizing Cells with UITableView and Auto Layout - Swift

Set Self-Sizing Cells

For a long time, a big challenge for iOS developers was custom heights and sizes for UITableView and also UICollectionView Cells. There were ways to achieve this such as ‘Auto Layout’, but they were ‘hacky’ and not fully implemented.
Previously to calculate height, developers had two options.


If using Auto Layout, a developer could create an offscreen cell, layout the content in tableView:heightForRowAtIndexPath: and then get the height (using systemLayoutSizeFittingSize:).


If not using Auto Layout, the height had to be calculated manually for each cell and the values cached to avoid slowdown.
With iOS 8, UITableViews and UICollectionViews have embraced Auto Layout. In this post, we will cover what you need to know to support UITableViews in your apps.
We will create a small sample application, that you can also find on Github.
The assets used in the app can be found here.

Project Setup

In this app we will display a series of quotes with a photo of the quoter if available. We’ll write the app in Swift 2.1 on Xcode 7.2, but will target iOS 8.1 and above.
In Xcode, go to File -> New -> Project, and create a new Single View Application. This will generate a new project with a plain UIViewController for you to get started with. Since we won’t be needing it, delete ViewController.swift.
After creating the project, we need to create the view controllers and custom cells. These should be a subclasses of UITableViewController and UITableViewCell.
Inside the project, create two new groups (folders), View Controllers and Cells to hold the above.
Let’s create our View Controller. Right-click the View Controllers group, select New File and choose Cocoa Touch Class in the panel that appears.
Create Class
Then create a sub-class of UITableViewController. Since we’ll be using Storyboards in the app, you can leave the Also create XIB file option unchecked.
UITableViewController Subclass
Under the Cells group, create a sub-class of UITableViewCell.
This time, we will check the XIB option, as we’ll be using it to setup the cell later.
UITableViewCell Subclass
Next we need to use the view controller we created in the app. Open Main.storyboardand delete any existing scenes generated by Xcode. Then click and drag a UITableViewController from the object library onto the storyboard and change it into an instance of TableViewController.
Change UITableViewController Class
Embed this view controller in a UINavigationController by selecting the TableViewController scene and choosing Editor -> Embed In -> Navigation Controller. This is optional for our purposes, but it’ll make the app look better.
Finally, make sure that the navigation controller is the entry point of the app. Select the controller, and in the attributes inspector, enable Is Initial View Controller.
Set Initial View Controller
The storyboard should now look like this:
Storyboard
We’ll configure the cells and view controllers later.
Next, we’ll create:
  • A Model representing Quotes as an NSObject subclass
  • Another NSObject subclass for a ViewModel that acts as a datasource for the Controller. This will simplify our view controllers and let’s us re-use code where possible. For instance, if we need to move from using a tableView to a collectionView, it would be trivial to do so.
Create Quote.swift inside a new Models group and add the following:
import Foundation

class Quote: NSObject {
    var text: String!
    var imageName:String?
    var personName:String?

    convenience init(text:String!, imageName:String?, personName:String?) {
        self.init()
        self.text = text
        self.imageName = imageName
        self.personName = personName
    }
}
Next we’ll create the ViewModel which loads and holds the quotes that will populate the ViewController, acting as a data source for the UITableView. Create ViewModel.swift inside the View Controllers group and add the following:
import UIKit

class ViewModel: NSObject,UITableViewDelegate, UITableViewDataSource {

var quotes:[Quote] = []

    override init() {
        super.init()
        self.loadQuotes()
    }

    private func loadQuotes(){
        quotes = []
        if let path = NSBundle.mainBundle().pathForResource("quotes", ofType: "plist"),let quotesArray = NSArray(contentsOfFile: path){

            for dictionary in quotesArray {
                let dictionary = dictionary as! NSDictionary
                let text = dictionary.valueForKey("text") as? String ?? ""
                let imageName = dictionary.valueForKey("imageName") as? String
                let personName = dictionary.valueForKey("person") as? String

                let quote = Quote(text: text, imageName: imageName, personName: personName)
                quotes.append(quote)
            }
        }
    }
    // TODO: TableView datasource methods
}
Here we’re loading a static set of quotes from a plist file in our bundle.


Configuring UITableViewCell and UITableView

Now that we have our data and models in place, it’s time to setup the views.
For each quote, we’ll display the quote itself, the name of the quoter, and a photo of the person if available.
Note that the image is optional. This means that we may or may not want an imageView in the cell and this affects the space available for text, which in turn affects the height.
There are two approaches to deal with this:
  • Use different re-use identifiers and cell subclasses for each type of quote, or
  • Dynamically change constraints in a single cell subclass and get the layout we want.
The approach taken depends on the complexity of changes to constraints. If you find you need an entirely different set of Auto Layout constraints for each cell class, you’re better off with the first.
In this case, it’s simple enough change to the constraints so we’ll follow the second.
Open TableViewCell.xib and add elements like the below:
TableViewCell
The cell consists of a photoView to display the image, quoteTextLabel to display the quote, and a nameLabel to display the name.

ImageView

We set the size of the photoView to a fixed 64×64, and set the constraints accordingly.
ImageView
Note: Width and Height constraints are set to a priority of 1000 by default. There might be instances where your constraints don’t work. If you check the debug logs, it should include a system-added constraint called UIView-Encapsulated-Layout-Height and other constraints removed by the system. The solution is to set such constraints to a lesser priority, like 999. In this case, we’ve set the imageView height constraint to this priority, hence the dotted line around it.

Quote Text Label

Set the font and number of lines as follows. We set the number of lines to 0 as we want the label to display all the text of the quote.
Label Config
Label Bounds
When setting the constraints of the label, set the Content Hugging priority to a value of 751, i.e high, which will ensure the label will resize to fit its content.
Label Constraints


Name Label

Since the name is unlikely to be long and require multiple lines, we set the number of lines to 1 and the height to a fixed 21 pts.
Name Label
Note: There’s a chance that either the nameLabel or the photoView is the bottom most view in the cell, depending on the length of the quote. Therefore, we set a >=constraint for both their respective bottom spacing constraint.
Note: It’s important that the Auto Layout constraints are unambiguous. They should be solvable by the system
Next, drag and drop from the views to create outlets in the cell:
@IBOutlet weak var photoView: UIImageView!
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var nameLabel: UILabel!
Finally, remember how the imageView is optional? To achieve this, we’ll maintain references to a set of constraints, specifically, the photoView’s width constraint and the spacing constraint between the photoView and quoteTextLabel. By updating just these 2 constraints, we can make it look like the photoView is not even in the cell.
@IBOutlet weak var photoWidthConstraint: NSLayoutConstraint!
@IBOutlet weak var photoRightMarginConstraint: NSLayoutConstraint!
Creating outlets for constraints is the same as for views. Just select the constraint in Interface Builder, and ctrl-click + drag to the assistant editor.
After setting up the cell, it’s time to setup the data source methods in the ViewModel.swift to display content.
...

// TODO: TableView datasource methods

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return quotes.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(tableViewCellIdentifier, forIndexPath: indexPath) as! TableViewCell

    let quote = quotes[indexPath.row]
    cell.quoteTextLabel.text = quote.text
    cell.nameLabel.text = quote.personName

    if let imageName = quote.imageName where !imageName.isEmpty{
        cell.photoView?.image = UIImage(named: imageName)
        cell.photoWidthConstraint.constant = kDefaultPhotoWidth
        cell.photoRightMarginConstraint.constant = kDefaultPhotoRightMargin
    }
    else {
        cell.photoView?.image = nil
        cell.photoWidthConstraint.constant = 0
        cell.photoRightMarginConstraint.constant = 0
    }

    cell.contentView.setNeedsLayout()
    cell.contentView.layoutIfNeeded()

    return cell
}
And add the variable used here into TableViewCell.swift:


import UIKit

let tableViewCellIdentifier = "TableViewCell"
let kDefaultPhotoWidth: CGFloat = 64
let kDefaultPhotoRightMargin: CGFloat = 8

class TableViewCell: UITableViewCell {
  ...
}
The data source populates each cell with the contents. Note how we make use of the constraints previously stored as outlets. Before we return the cell, we can make changes to these constraints. If the image is not present, the photoView is set to a width of 0, and so is its right margin. Effectively, we’re preventing the photoViewfrom being displayed in the cell.
It’s not enough to just use the numeric value of the constraints. We also need to let the cell’s contentView know that its constraints have changed, and it needs to be re-laid out. Hence, the calls to setNeedsLayout: and layoutIfNeeded:
Note: The above seems to be necessary only on iOS 8, and not on iOS 9.
Finally, we come to the tableView setup, where all the magic happens.
In TableViewController.swift:

...

var viewModel:ViewModel!

override func viewDidLoad() {
    super.viewDidLoad()

    //Create the ViewModel, and set it as the delegate + datasource.
    self.viewModel = ViewModel()
    self.tableView.delegate = viewModel
    self.tableView.dataSource = viewModel

    //Register our custom cell subclass.
    self.tableView.registerNib(UINib(nibName: "TableViewCell", bundle: NSBundle.mainBundle()), forCellReuseIdentifier: tableViewCellIdentifier)

    // Self-sizing magic!
    self.tableView.rowHeight = UITableViewAutomaticDimension
    self.tableView.estimatedRowHeight = 50; //Set this to any value that works for you.

}
All that’s required to enable self-sizing cells on UITableView are the last two lines. Because the row height is now a non-explicit value, iOS will first use the estimatedRowHeight to setup the cells and will then calculate the row height before cells appear on screen. All the work we did with Auto Layout setup pays off here.


How does it work? iOS calculates the height, by solving the linear equation represented by Auto Layout constraints, using a known set of variables: The width of the cell’s contentView (which is usually the width of the tableView minus things like indentation, accessoryViews etc), and the constraints themselves. This information is the minimum required to calculate height in any way.
Since rowHeight has already been set, it’s not necessary to implement tableView:heightForRowAtIndexPath: anymore.
A note about estimatedHeight: This is just that, an estimation. If your estimated value varies greatly from the actual height, you might see some jumpiness while scrolling, as iOS expands or collapses the cell to its proper height. If you need granular control over the estimated height, use the UITableView Datasource method tableView:estimatedHeightForRowAtIndexPath: and calculate the estimated height for each row individually.
Now that you’ve done all the hard work, it’s time to run the app. The final app should look like this:
Final App
Please let me know if you have any questions or comments below.

Comments

Popular Posts

How I Reduced the Size of My React Native App by 85%

How and Why You Should Do It I borrowed 25$ from my friend to start a Play Store Developer account to put up my first app. I had already created the app, created the assets and published it in the store. Nobody wants to download a todo list app that costs 25mb of bandwidth and another 25 MB of storage space. So today I am going to share with you how I reduced the size of Tet from 25 MB to around 3.5 MB. Size Matters Like any beginner, I wrote my app using Expo, the awesome React Native platform that makes creating native apps a breeze. There is no native setup, you write javascript and Expo builds the binaries for you. I love everything about Expo except the size of the binaries. Each binary weighs around 25 MB regardless of your app. So the first thing I did was to migrate my existing Expo app to React Native. Migrating to React Native react-native init  a new project with the same name Copy the  source  files over from Expo project Install all de...

How to recover data of your Android KeyStore?

These methods can save you by recovering Key Alias and Key Password and KeyStore Password. This dialog becomes trouble to you? You should always keep the keystore file safe as you will not be able to update your previously uploaded APKs on PlayStore. It always need same keystore file for every version releases. But it’s even worse when you have KeyStore file and you forget any credentials shown in above box. But Good thing is you can recover them with certain tricks [Yes, there are always ways]. So let’s get straight to those ways. 1. Check your log files → For  windows  users, Go to windows file explorer C://Users/your PC name/.AndroidStudio1.4 ( your android studio version )\system\log\idea.log.1 ( or any old log number ) Open your log file in Notepad++ or Any text editor, and search for: android.injected.signing and if you are lucky enough then you will start seeing these. Pandroid.injected.signing.store.file = This is  file path where t...

Video Calling In IOS Objective C

Video Calling Sources Project homepage on GIT — https://github.com/QuickBlox/quickblox-ios-sdk/tree/master/sample-videochat-webrtc Download ZIP - https://github.com/QuickBlox/quickblox-ios-sdk/archive/master.zip Overview The VideoChat code sample allows you to easily add video calling and audio calling features into your iOS app. Enable a video call function similar to FaceTime or Skype using this code sample as a basis. It is built on the top of WebRTC technology.            System requirements The QuickbloxWebRTC.framework supports the next:     * Quickblox.framework v2.7 (pod QuickBlox)     * iPhone 4S+.     * iPad 2+.     * iPod Touch 5+.     * iOS 8+.     * iOS simulator 32/64 bit (audio might not work on simulators).     * Wi-Fi and 4G/LTE connections. Getting Started with Video Calling API Installation with CocoaPods CocoaPods is a dependency manag...