0

How To Make Expandable UITableViewCells For Dynamic Text Height With Auto Layout

 3 years ago
source link: https://sweettutos.com/2016/07/31/how-to-make-expandable-uitableviewcells-for-dynamic-text-height-with-auto-layout/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How To Make Expandable UITableViewCells For Dynamic Text Height With Auto Layout

UITableViewCell objects are the core elements for table views and are used to draw the table view visible rows.

Although I have covered a bunch of their concepts in most of our posts here, I decided to write this tutorial to provide a closer look at table view cells. Specifically, how to dynamically expand cells to reveal more content.

Giving the fact Self-Sizing cells provides a way to adjust the cell height depending on the enclosed content, sometimes you may need to expand that cell height ON DEMAND.

I am going to go a little bit beyond what Self-Sizing cells do. Using Auto Layout technology, everything is possible.

The app you are going to develop here will basically query some 2016 US movies from a JSON webservice.

The final result will look like this:

How To Make Expandable UITableViewCells For Dynamic Text Height With Auto Layout

This tutorial is made using the latest Xcode 8 and Swift 3 language.

As usual, let’s do it!

I have prepared a starter project with some pre-needed setup, download it here.

Make sure to open the 2016Movies.xcworkspace and not the .xcodeproj file.

The project has already embedded the SDWebImage library for asynchronous images loading and caching. I chose to use a third party library in order to bypass the images download process code, since this will not be our topic in this tutorial.

However, if you want to know more on how to download and cache images asynchronously without relying on third libraries, I invite you to read my post here.

Most of the work regarding this tutorial will be with the UI and Auto Layout. Select the Main.storyboard file from the Project navigator and drag a UITableView from the Object library to the View.

Add UITableView in Xcode 8

The table view is not layout yet, select the Pin tool and activate the top/bottom/leading and trailing constraints. Also, make sure the margins are set to 0 so that the table view will take the full screen.

Table View Auto Layout in Xcode 8

Next, drag a UITableViewCell from the Object library to the table view and change its height to 170 from the Size inspector.

Add a UITableViewCell to the Table View

Time now to add the cell subviews; Basically, you gonna add a container view as a first child of the content view with a slightly smaller width/height than the cell itself so that you make the illusion of a spaced table view cells.

Later, all the remaining subviews (image view, labels) will be placed as subviews of the container view.

The final cell will look like this (I coloured the views for better distinction):

How to setup a UITableViewCell with Auto Layout in Xcode 8

The view in red is the cell while the view in yellow is the container view which in turn hold all the remaining controls as subviews.

Before you start the layout tweaks, let’s choose one screen size to work with for this tutorial. I picked the iPhone 6S screen. So make sure you are using the same from the menu at the bottom of the storyboard, like below:

To achieve the above cell layout, retrace the following steps:

1- Select the ViewController from the storyboard canvas then choose “Editor\Embed In\Navigation Controller” from the menu.

2- Drag a UIView from the Object library to the cell and make it so that it’s the cell subview.

3- Apply the following frame to the view: x = 10, y = 8, width = 355, height = 153. Next, add the following constraints to it (from the Pin tool): Leading = 10, Trailing = 10, Top = 8, Bottom = 8.

Also, make sure to uncheck the Constrain to margins option.

Add Leading, Trailing, Top, Bottom Constraints from the Pin tool in Xcode 8

4- Add a UIImageView and two UILabels to the newly added container view. Apply the following frames and constraints to them:

UIImageView:

– Frame: x = 6, y = 6, width = 85, height = 100.
– Constraints: Leading space to superview = 6, width = 85, Top space to superview = 6, height = 100

Title UILabel:

– Frame: x = 99, y = 54, width = 248, height = 20
– Constraints: Trailing space to superview = 8, Top space to superview = 54, height = 20, leading space to the UIImageView = 8
– Font: System 15.0

Description UILabel:

– Frame: x = 8, y = 111, width = 339, height = 38
– Constraints: Trailing space to superview = 8, Leading space to superview = 8, Bottom space to superview = 4, Top space to the UIImageView = 5
– Font: System 13.0
– Select the Attributes inspector and make sure the Lines property of the label is set to 0.

Set number of lines of UILabel to 0 to allow multiple lines

Note: The key point to the label expandable feature is to allow it to contain multiple lines by setting its number of lines property to 0, and also by NOT restricting its frames with an explicit height and width. That explains why you didn’t set a height/width constraints but only margins constraints.

5- Select the Table View Cell from the Document Outline, then select the Attributes inspector and change the Identifier to “CellID”. Next, select the Identity inspector view and change the Custom Class to “MovieCustomCell”. Also, double check the Module field, if it’s not already filled in, choose a Module from the list.

Change the cell identifier from the Attributes inspector

Change the cell identifier to “CellID”

Change the view custom class from the Identity inspector

Change the Cell custom class to “MovieCustomCell”

Select a Module for the View Controller

Select a Module for the Custom Cell

“MovieCustomCell” is the custom class that will hold all the cell controls references. You will implement it in a moment.

6- Drag another UIViewController from the Object library to the canvas.

7- Add a UITableView from the Object library to the second view controller. Change its frame to x = 0, y = 0, width = 375 and height = 667. Also, activate the Trailing/Leading/Top/Bottom constraints of the table view and make sure all are set to 0.

8- Ctrl click the first ViewController and drag a line to the second one. Choose the “Show” segue from the list.

Create a segue between two view controllers in storyboard

9- Select the newly added Storyboard Segue, then change its Identifier to “ShowDetails” from the Attributes inspector.

Change the Segue Identifier

10- Make sure the second view controller is selected (not the table view), then change the Custom Class to “DetailsViewController” from the Identity inspector. Also, double check the Module field is selected from the list and NOT set to “None”.

Now time to hook up the UI controls with their relevant classes and outlets.

First, let’s add the custom classes for the cell as well as for the second view controller.

Select the “File\New\File” from the menu, then “iOS\Source\Cocoa Touch Class”. Make sure its a subclass of UITableViewCell and set the class name to “MovieCustomCell” then hit the “Create” button.

Repeat the same step but this time create a UIViewController subclass and call it “DetailsViewController”.

Switch to the Assistant editor and verify the storyboard is side by side with the “MovieCustomCell” class. Ctrl click the UIImageView and drag a line to the class code. Make sure it’s an Outlet connection and name it “moviePhoto”.

Hook up an UIImageView outlet to the code with the Assistant editor

Repeat the same with the remaining two labels. Call the first label (next to the image view) “movieTitle” and the second label “movieDescription”.

Keep the Assistant editor and select the ViewController.swift file to be side by side with the Main.storyboard file. Ctrl click the table view and drag a line to the ViewController.swift code. Call the outlet “tableView” and hit the “Connect” button.

Hook up a UITableView to the code in Storyboard

To finish up with the UI, select the DetailsViewController.swift file in the split window and hook up an outlet from the table view of the DetailsViewController UI to the DetailsViewController.swift code. Call the outlet “tableView” and click the “Connect” button.

Great work so far. You are done with the Auto Layout constraints and UI adjustments. Now time for some code to get all this alive 🙂

Select the ViewController.swift file from the Project navigator view and add the following vars declarations:

    var expandedLabel: UILabel!
    var indexOfCellToExpand: Int!
    var movies: [[String: AnyObject]]!
    var selectedMovie: [String: AnyObject]!
    var selectedMoviePhoto: UIImage!

// 1: The expandedLabel is a reference that will hold the label in its new frame height (after it is about to display the longer text). As you may wonder, the indexOfCellToExpand will store the index of the cell that contain the label to expand.

// 2: The movies variable is an array of dictionaries that will be filled with all the JSON data you will query from the API.

Locate the viewDidLoad function and add the following code inside (right after the super.viewDidLoad() call):

movies = [[String: AnyObject]]()
let url = URL(string: "http://sweettutos.com/movies.json")
let task = URLSession.shared().dataTask(with: url!) { (data:Data?, response: URLResponse?, error: NSError?) in
if error == nil
             let results = try JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as! [String:AnyObject]
             self.movies = results["movies"] as! [[String: AnyObject]]
             DispatchQueue.main.async(execute: {
                   self.tableView.reloadData()
        }catch
             print("An error occurred")
task.resume()

The code above will basically fetch data from an API through an URLSession asynchronous call. After the data is pulled off, the table view will be reloaded to display the list of movies.

Note: You can notice some Swift 3 new changes here. Mainly, the “NS” prefix is removed from many Foundation classes.

Now that the data is fetched, let’s populate it in the table view. Implement the following code before the class closing bracket:

func numberOfSections(in tableView: UITableView) -> Int {
    return 1
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CellID") as! MovieCustomCell
    let movie = self.movies[indexPath.row]
    let photoURL = movie["Photo"] as! String
    let title = movie["Title"] as! String
    let intro = movie["Intro"] as! String
    cell.movieTitle.text = title
    cell.movieDescription.text = intro
    cell.movieDescription.tag = indexPath.row
    let tap = UITapGestureRecognizer(target: self, action: #selector(ViewController.expandCell(sender:)))
    cell.movieDescription.addGestureRecognizer(tap)
    cell.movieDescription.isUserInteractionEnabled = true
    // Download the photo using the SDWebImage library
    cell.moviePhoto.sd_setImage(with: URL(string: photoURL))
    return cell
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.movies.count

It’s important to attach a gesture recognizer to the description label with the row index as a tag, since we need to know when it’s being clicked to launch the expansion code and make the whole label and cell expanded to reveal more content.

Before you launch the project, let’s write the code for the “expandCell” function. Place the following code inside your class:

func expandCell(sender: UITapGestureRecognizer)
        let label = sender.view as! UILabel
        let cell: MovieCustomCell = tableView.cellForRow(at: IndexPath(row: label.tag, section: 0)) as! MovieCustomCell
        let movie = self.movies[label.tag]
        let description = movie["Description"] as! String
        cell.movieDescription.sizeToFit()
        cell.movieDescription.text = description
        expandedLabel = cell.movieDescription
        indexOfCellToExpand = label.tag
        tableView.reloadRows(at: [IndexPath(row: label.tag, section: 0)], with: .fade)
        tableView.scrollToRow(at: IndexPath(row: label.tag, section: 0), at: .top, animated: true)

This function first extracts the gesture attached view (in our case the description label), then it assigns the whole movie description to the label. The call to sizeToFit function is mandatory in order to ask the label to resize its frames to enclose the new text content.

In order to show the new expanded cell, the function reloads the selected row with animation and scroll it to the top so that it’s ready to be read without manually scrolling up or down the table view.

A few things still needed to see your work in action. Finish this up by making the following changes to the class:

// Change the class declaration to the following
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// Place the following at the top of the viewDidLoad function (right after the super.viewDidLoad() call)
indexOfCellToExpand = -1
tableView.dataSource = self
tableView.delegate = self
// Implement the following before the end of the class
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
     if indexPath.row == indexOfCellToExpand
        return 170 + expandedLabel.frame.height - 38
     return 170

The tableView:heightForRowAt: delegate method is responsible of returning the height of each cell the table view should display. Basically, whenever the table view reload, this method will check the index of the row towards the index of the selected row, for which case, the function will ask the expanded cell to redraw with a new height (original height + the expanded label height – 38).

Note: 38 is the height of the label before it gets expanded.

In order to enable HTTP request calls, you need to disable the ATS (App Transport Security) in your project. Select the Info.plist file from the Project navigator and make the following change to it:

Run the app, select the description labels to reveal more content and enjoy your work in action 🙂

Let’s finish up with the details controller. Basically, once you select a row, we want to show the movie details in another screen.

Always inside the ViewController.swift class, add the following code before the closing bracket:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        selectedMovie = movies[indexPath.row]
        let cell = tableView.cellForRow(at: indexPath) as! MovieCustomCell
        selectedMoviePhoto = cell.moviePhoto.image
        self.performSegue(withIdentifier: "ShowDetails", sender: self)
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
        let detailsVC = segue.destinationViewController as! DetailsViewController
        detailsVC.movie = selectedMovie
        detailsVC.photo = selectedMoviePhoto

This will prepare the selected movie data to be transferred while the push segue to the details view controller.

Select the DetailsViewController.swift file from the Project navigator view and make the following changes:

// Change the class declaration to the following
class DetailsViewController: UIViewController, UITableViewDataSource {
// Properties declarations at the top of the class
var photo: UIImage!
var movie: [String: AnyObject]!
// Inside the viewDidLoad function (right after the super.viewDidLoad call)
tableView.tableHeaderView = UIImageView(image: photo)
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CellID")
tableView.estimatedRowHeight = 100
// At the end of the class
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CellID")
    cell?.textLabel?.text = movie["Description"] as? String
    cell?.textLabel?.numberOfLines = 0
    return cell!

That’s it. Run the project and enjoy your work 🙂

As usual, you can download the final project here.

How do you do to expand the table view cell? Feel free to share your approach in the comments section below.

If you enjoy this post, you can subscribe in the form below to get notified of all our upcoming tutorials, ebooks, apps and discounts.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK