2

I have an dynamic-width UILabel (colored lime-green for visibility) which has a fixed leading and trailing constraint to its neighbours (the green and red views). I want this table to adapt its width when one of its neighbours is moved (the rounded red/green view). However, I am having an issue with the animation of this change since the width of my label "jumps" to its new value without animating. For my right-aligned label this means that the text itself jumps too.

enter image description here

I am animating the movement of the red/green view like this:

- (void)show {
    self.edgeConstraint.constant = 0;
    [UIView animateWithDuration:0.25 animations:^{
        [self.contentView layoutIfNeeded];
    }];
}

- (void)hide {
    self.edgeConstraint.constant = -100;
    [UIView animateWithDuration:0.25 animations:^{
        [self.contentView layoutIfNeeded];
    }];
}
pajevic
  • 4,607
  • 4
  • 39
  • 73

1 Answers1

3

The problem is with the UILabel content mode. You see a similar problem here. Because you are shortening the label where the text is drawn, the label does a drawing pass, but it uses the final frame of the animation block for the update. The easiest way create a smooth animation is to use a greaterThanOrEqualTo leading constraint and set the textAlignment to .natural.

When I do that I get the result on top as apposed to setting textAlignment .right on bottom.

Screencast

Source code for demo:

import UIKit
import PlaygroundSupport

final class ViewController: UIViewController {

    var constraint: NSLayoutConstraint?
    var otherConstraint: NSLayoutConstraint?

    lazy private var sideView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        self.constraint = view.leadingAnchor.constraint(equalTo: self.view.trailingAnchor)
        self.constraint?.isActive = true
        view.widthAnchor.constraint(equalToConstant: 100).isActive = true
        view.heightAnchor.constraint(equalToConstant: 44).isActive = true
        view.backgroundColor = .yellow
        return view
    }()

    lazy private var secondSideView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 44.0).isActive = true
        self.otherConstraint = view.leadingAnchor.constraint(equalTo: self.view.trailingAnchor)
        self.otherConstraint?.isActive = true
        view.widthAnchor.constraint(equalToConstant: 100).isActive = true
        view.heightAnchor.constraint(equalToConstant: 44).isActive = true
        view.backgroundColor = .yellow
        return view
    }()

    lazy private var label: UILabel = {
        let label = UILabel()
        label.text = "Label"
        label.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(label)
        label.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        label.leadingAnchor.constraint(greaterThanOrEqualTo: self.view.leadingAnchor, constant: 8.0).isActive = true
        label.trailingAnchor.constraint(equalTo: self.sideView.leadingAnchor).isActive = true
        return label
    }()

    lazy private var secondLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .right
        label.text = "Label"
        label.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(label)
        label.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 44.0).isActive = true
        label.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
        label.trailingAnchor.constraint(equalTo: self.sideView.leadingAnchor).isActive = true
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        self.secondSideView.isHidden = false
        self.secondLabel.isHidden = false
        self.sideView.isHidden = false
        self.label.isHidden = false
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.perform(#selector(self.showSideView), with: nil, afterDelay: 5.0)
    }

    @objc private func showSideView() {
        self.view.layoutIfNeeded()
        UIView.animate(withDuration: 1.5, delay: 0.0, options: [.autoreverse, .repeat], animations: {
            self.otherConstraint?.constant = -100
            self.constraint?.constant = -100
            self.view.layoutIfNeeded()
        })
    }
}

PlaygroundPage.current.liveView = ViewController()
beyowulf
  • 15,101
  • 2
  • 34
  • 40
  • You were absolutely right that the issue is with the text redrawing. As soon as I started reading your answer I remembered that I had this issue once before where I ended up using a scale transform (in that case the UILabel was being shrunk in both width and height). Your suggestion with using a "greater then or equal to" leading/trailing constraint did the trick perfectly. Thanks! – pajevic Dec 30 '17 at 08:44