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:
- Do a first pass on
tableView.reloadData()
so that the tableView knows the content size -
tableView.setNeedsLayout()
will invalidate the subViews -
tableView.layoutIfNeeded()
will force a redo the layout of the subViews immediately - Now we do another
tableView.reloadData()
to refresh the table to show the JSON data and the right size
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.