April 01, 2016

Swift: How to Resize UITextView + TableViewCell Correctly After JSON Fetch

Here’s the issue I had. I have a UITextView of variable length that is populated via JSON. It is part of a XIB file that is inserted as a TableViewCell in a UITableView.

EDIT: In the end I figure that using UITextView in a TableViewCell is more trouble than it’s worth. So I changed all my UITextViews to UILabels even if they are multi-lines + multi-paragraphs. Unless you need scrolling, I highly recommended switching as well.


Problem?

While the table is populated correctly after fetching the data and doing a tableView.reloadData(), the height of the cells are not adjusting. But if I navigate away and come back, somehow they will readjust into the right size this time.

From what I see, the height for each cell is extracted based on the placeholder content I have in my XIB file and not the JSON data… which is not what I want.

My Simplified Codes:

import UIKit
import Alamofire
import SwiftyJSON

class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var listing: JSON = []

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Table
        tableView.delegate = self
        tableView.dataSource = self

        // Table View Cells
        tableView.registerNib(UINib(nibName: "FirstTableViewCell", bundle: nil), forCellReuseIdentifier: "firstCell")
        tableView.registerNib(UINib(nibName: "SecondTableViewCell", bundle: nil), forCellReuseIdentifier: "secondCell")
        tableView.registerNib(UINib(nibName: "ThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "thirdCell")
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)

        let url = "http://dev.local/api/listing"

        Alamofire.request(.GET, url).validate().responseJSON { response in
            switch response.result {
            case .Success:
                let jsonData = JSON(response.result.value!)
                self.listing = jsonData["listing"]

                dispatch_async(dispatch_get_main_queue(), {
                    self.tableView.reloadData()
                })

            case .Failure(let error):
                print(error)
            }
        }
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Each cell is a predefined view section
        return 3
    }

    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }

    func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        switch indexPath.row {
        case 0:
            let cell = tableView.dequeueReusableCellWithIdentifier("firstCell", forIndexPath: indexPath) as! FirstTableViewCell
            cell.delegate = self
            cell.contentView.userInteractionEnabled = false
            return cell
        case 1:
            let cell = tableView.dequeueReusableCellWithIdentifier("secondCell", forIndexPath: indexPath) as! SecondTableViewCell
            cell.delegate = self
            return cell
        default:
            let cell = tableView.dequeueReusableCellWithIdentifier("thirdCell", forIndexPath: indexPath) as! ThirdTableViewCell
            return cell
        }
    }

    func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        switch indexPath.row {
        case 0:
            let firstCell = cell as! GalleryTableViewCell
            firstCell.images = listing["images"].arrayValue
        case 1:
            let secondCell = cell as! ListingDetailsTableViewCell
            secondCell.details = listing["details"].arrayValue
        case 2:
            let thirdCell = cell as! ListingDetailsTableViewCell
            thirdCell.shipping = listing["shipping"].arrayValue
        default: break
        }
    }
}

Solution:

The solution is simple (but not elegant). That is, we will have to do tableView.reloadData() twice.

Turns out that when tableView first started laying out the cells at cellForRowAtIndexPath, it doesn’t yet know the content size. So it turned to heightForRowAtIndexPath and estimatedHeightForRowAtIndexPath to get the height for each cell. Since at this point, the data is not being populated yet, the heights returned are based on my placeholder content in the Interface Builder.

After all of that are done, then willDisplayCell will be called to populate the content. Even though now that the TableView knows its content size, the cells have already been set up. So the JSON data are merely being fed into each outlet without height adjustment.

Finally when TableView is reloaded, the new data is displayed correctly… Just not in the right heights.

Reload Twice

In order to fix that, here’s what we have to do:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    let url = "http://dev.local/api/listing"

    Alamofire.request(.GET, url).validate().responseJSON { response in
        switch response.result {
        case .Success:
            let jsonData = JSON(response.result.value!)
            self.listing = jsonData["listing"]

            dispatch_async(dispatch_get_main_queue(), {
                self.tableView.reloadData()

                // 3 new lines of codes to force size adjustment
                self.tableView.setNeedsLayout()
                self.tableView.layoutIfNeeded()
                self.tableView.reloadData()
            })

        case .Failure(let error):
            print(error)
        }
    }
}

Hope this helps.

***