skip to Main Content

I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.

As you can see in the following animation, initially, the add/ expand animation works just fine.

When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!

enter image description here

My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.

Here’s the code which performs add/ expand animation.

Add/ expand animation

@IBAction func add(_ sender: Any) {
    let customView = CustomView.instanceFromNib()
    
    customView.hide()
    stackView.addArrangedSubview(customView)
    // Clear off horizontal swipe in animation caused by addArrangedSubview
    stackView.superview?.layoutIfNeeded()
    
    customView.show()
    // Perform expand animation.
    UIView.animate(withDuration: 1) {
        self.stackView.superview?.layoutIfNeeded()
    }
}

Here’s the constraint setup of the UIScrollView & added custom view item

Constraint setup

enter image description here

Custom view

class CustomView: UIView {

    private var zeroHeightConstraint: NSLayoutConstraint!
    
    @IBOutlet weak var borderView: UIView!
    @IBOutlet weak var stackView: UIStackView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        borderView.layer.cornerRadius = stackView.frame.height / 2
        borderView.layer.masksToBounds = true
        borderView.layer.borderWidth = 1
        
        zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
        zeroHeightConstraint.isActive = false
    }
    
    func hide() {
        zeroHeightConstraint.isActive = true
    }
    
    func show() {
        zeroHeightConstraint.isActive = false
    }
}

Here’s the complete source code

https://github.com/yccheok/add-expand-animation-in-scroll-view

Do you have any idea why such problem occur, and we can fix such? Thanks.

2

Answers


  1. I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.

    In CustomView:

    func hide() {
        alpha = 0.0
        isHidden = true
        zeroHeightConstraint.isActive = true
    }
    
    func show() {
        alpha = 1.0
        isHidden = false
        zeroHeightConstraint.isActive = false
    }
    

    In your view controller:

    @IBAction func add(_ sender: Any) {
        let customView = CustomView.instanceFromNib()
        customView.hide()
        stackView.addArrangedSubview(customView)
        self.stackView.layoutIfNeeded()
    
        UIView.animate(withDuration: 00.5) {
            customView.show()
            self.stackView.layoutIfNeeded()
        }
    }
    

    Also, the constraints in your storyboard aren’t totally correct. You are seeing a red constraint error because autolayout doesn’t know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.

    Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn’t need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.

    Everything else looks pretty good.

    Login or Signup to reply.
  2. Because of the way stack views arrange their subviews, animation can be problematic.

    One approach that you may find works better is to embed the stack view in a "container" view.

    That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:

    enter image description here

    The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):

    func addCustomView(_ animated: Bool) {
        let customView = CustomView.instanceFromNib()
        
        stackView.addArrangedSubview(customView)
        customView.isHidden = true
        
        if animated {
            DispatchQueue.main.async {
                UIView.animate(withDuration: 1) {
                    customView.isHidden = false
                }
            }
        } else {
            customView.isHidden = false
        }
    }
    

    And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:

    class CustomView: UIView {
        
        @IBOutlet weak var borderView: UIView!
        @IBOutlet weak var stackView: UIStackView!
        
        override func awakeFromNib() {
            super.awakeFromNib()
            
            borderView.layer.masksToBounds = true
            borderView.layer.borderWidth = 1
            
        }
        override func layoutSubviews() {
            super.layoutSubviews()
            borderView.layer.cornerRadius = borderView.bounds.height * 0.5
        }
    }
    

    Since it’s a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view


    Edit

    Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).

    One way to get around that is to add an empty view as the first subview.

    So, for this example, in viewDidLoad() before adding an instance of CustomView:

    let v = UIView()
    stackView.addArrangedSubview(v)
    

    This will make the first arranged subview a zero-height view (so it won’t be visible).

    Then, if you’re implementing removing custom views, just make sure you don’t remove that first, empty view.

    If your stack view has .spacing = 0 noting else is needed.

    If your stack view has a non-zero spacing, add another line:

    let v = UIView()
    stackView.addArrangedSubview(v)
    stackView.setCustomSpacing(0, after: v)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search