0
votes

I'm trying to animate the height constraint of a UITextView inside a UIScrollView. When the user taps the "toggle" button, the text should appear in animation from top to bottom. But somehow UIKit fades in the complete view.

Animation

To ensure the "dynamic" height depending on the intrinsic content-size, I deactivate the height constraint set to zero.

 @IBAction func toggle() {
    layoutIfNeeded()
    UIView.animate(withDuration: 0.6, animations: { [weak self] in

        guard let self = self else {
            return
        }

        if self.expanded {
            NSLayoutConstraint.activate([self.height].compactMap { $0 })
        } else {
            NSLayoutConstraint.deactivate([self.height].compactMap { $0 })
        }
        self.layoutIfNeeded()
    })
    expanded.toggle()
}

The full code of this example is available on my GitHub Repo: ScrollAnimationExample

1
Is there a reason you are using a UITextView instead of a UILabel? And, are you targeting iOS 13+ or earlier?DonMag
Hi @DonMag, thank you for your answer. There's no reason for the UITextView. We could have used a multi line UILabel too. We're currently targeting iOS 11.2+ but thinking about going to iOS 13+.Joshua Brunhuber

1 Answers

2
votes

Reviewing your GitHub repo...

The issue is due to the view that is animated. You want to run .animate() on the "top-most" view in the hierarchy.

To do this, you can either create a new property of your ExpandableView, such as:

var topMostView: UIView?

and then set that property from your view controller, or...

To keep your class encapsulated, let it find the top-most view. Replace your toggle() func with:

@IBAction func toggle() {

    // we need to run .animate() on the "top" superview

    // make sure we have a superview
    guard self.superview != nil else {
        return
    }

    // find the top-most superview
    var mv: UIView = self
    while let s = mv.superview {
        mv = s
    }

    // UITextView has subviews, one of which is a _UITextContainerView,
    //  which also has a _UITextCanvasView subview.
    // If scrolling is disabled, and the TextView's height is animated to Zero,
    //  the CanvasView's height is instantly set to Zero -- so it disappears instead of animating.

    // So, when the view is "expanded" we need to first enable scrolling,
    //  and then animate the height (to Zero)
    // When the view is NOT expanded, we first disable scrolling
    //  and then animate the height (to its intrinsic content height)

    if expanded {
        textView.isScrollEnabled = true
        NSLayoutConstraint.activate([height].compactMap { $0 })
    } else {
        textView.isScrollEnabled = false
        NSLayoutConstraint.deactivate([height].compactMap { $0 })
    }

    UIView.animate(withDuration: 0.6, animations: {
        mv.layoutIfNeeded()
    })

    expanded.toggle()

}