Reputation: 543
Here is the situation. A title label, a "read more" button and a content label in a stackView. And the stackView is in a cell, set Auto-Layout. The height of tableView's cell is set to AutoDimension. When I tap button, content label will show or hide.
The button's action method is
@IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
self.contentLabel.isHidden.toggle()
}, completion: nil)
}
Here is the result in slow animations:
As you can see, when the content is going to show, line 2 is presented firstly, i.e. content is presented from the center. When the content is going to hide, the content label is hidden instantly, and button is stretched to the frame that content label had before hiding. This animation is strange.
Furthermore, if I set the stackView's spacing to 10, the case becomes worse. The title label was also affected:
I adjusted everything I can,stackView's distribution, three subViews' content mode and content hugging/compression priority. I can't find an appropriate way to fix it.
Here is the ideal result:
I achieved it by a little tricky way:
@IBAction func readMore(_ sender: Any) {
tableView.performBatchUpdates({
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.contentLabel.isHidden.toggle()
}
}, completion: nil)
}
I'm not sure this is the most appropriate way to fix it. So I want to know why this weird animation happens and if there is a more appropriate way to fix it. Thanks!
Upvotes: 1
Views: 2885
Reputation: 77568
Animating to hide/reveal multi-line labels can be problematic, particularly when used in a stack view.
If you give it a try, you'll find that even outside of a table view cell - just the stack view in a view - you will see the same issue when toggling the .isHidden
property of the label. This is due to the fact that UILabel
vertically centers its text.
Here is another approach, which doesn't use a stack view (background colors for clarity):
Top Label is set to 1 line; Read More is a normal button, Bottom Label is set to 0 lines.
You will notice the pink rectangle. That is a UIView
which I've named ShimView
- more about that shortly.
The Top Label is constrained Top: 4, Leading: 8, Trailing: 8
The Button is constrained Top: 0 (to topLabel), Leading: 8, Trailing: 8
The Bottom Label is constrained Top: 0 (to button), Leading: 8, Trailing: 8
The "shim view" is constrained Trailing: 8, Top: 0 (*to the top of bottom label*), Bottom: 4 (to the contentView)
The "shim view" is also given a Height constraint of 21
, with Priority: 999
-- and that Height constraint is connected to an IBOutlet
in the cell class.
The key is that we will adjust the shim's Height constraint's .constant
to expand/collapse the cell.
On init, we set the .constant
to 0
- this will leave the Bottom Label at its content-determined height, but won't be visible because it will be clipped by the cell's contentView.
When we want to "reveal/conceal" the label, we'll animate the height .constant
of the shim.
Result:
And, the result after clearing the background colors:
Here is the code:
//
// ExpandCollapseTableViewController.swift
//
// Created by Don Mag on 6/19/18.
//
import UIKit
class ExpandCollapseCell: UITableViewCell {
@IBOutlet var topLabel: UILabel!
@IBOutlet var theButton: UIButton!
@IBOutlet var bottomLabel: UILabel!
@IBOutlet var theShim: UIView!
@IBOutlet var shimHeightConstraint: NSLayoutConstraint!
var myCallBack: (() -> ())?
@IBAction func didTap(_ sender: Any) {
myCallBack?()
}
}
class ExpandCollapseTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandCollapseCell", for: indexPath) as! ExpandCollapseCell
cell.topLabel.text = "Index Path - \(indexPath)"
cell.bottomLabel.text = "Line 1\nLine 2\nLine 3\nLine 4"
// init to "collapsed"
// in actual use, this would be tracked so the row would remain expanded or collapsed
// on reuse (when the table is scrolled)
cell.shimHeightConstraint.constant = 0
if true {
cell.topLabel.backgroundColor = .clear
cell.theButton.backgroundColor = .clear
cell.bottomLabel.backgroundColor = .clear
cell.theShim.backgroundColor = .clear
}
cell.myCallBack = {
UIView.animate(withDuration: 0.3) { // It must be 0.3
self.tableView.beginUpdates()
cell.shimHeightConstraint.constant = (cell.shimHeightConstraint.constant == 0) ? cell.bottomLabel.frame.size.height : 0
self.tableView.layoutIfNeeded()
self.tableView.endUpdates()
}
}
return cell
}
}
Upvotes: 2
Reputation: 7510
I have had to do a similar animation several times before. The way to solve this is to define the height of your stack view and independently your cell's content view. Then when you want the cell's height to change, you only update the content view's height constraint.
A good way to determine a views height is to use the intrinsicContentSize
property. Override this if you need a different value from the inherited one.
Another way to be notified of a views size change is to create a subclass with a delegate or a closure which is called from the subclassed views frame
property and passes the new size to whoever is listening for it.
Upvotes: 1