Search code examples
iosswiftanimationuiviewuikit

Animating constraints changes weird result


I have this class:

   class Cell {
        
        /// The view that contains everything.
        ///
        /// Use this view to present this `Cell`.
        var view: UIView
        
        /// The view that contains the content of the cell without `openedView`.
        var cellView: UIView
        
        var openedView: UIView
        
        var topOpenedView: UIView
        
        var button: UIButton! = nil
        
        private var isOpenedProtected: Bool = false
        
        /// The visibility of ```openedView```.
        ///
        /// Changing this variable is the same as ```changeOpened(to:animated:)``` with animation.
        var isOpened: Bool {
            get {
                return isOpenedProtected
            }
            set {
                changeOpened(to: newValue, animated: true)
            }
        }
        
        private var openedViewHeightConstraint: NSLayoutConstraint! = nil
        private var openedViewBottomConstraint: NSLayoutConstraint! = nil
        
        lazy private var buttonHandler: UIActionHandler = { _ in
            self.isOpened.toggle()
        }
        
        init(tint color: UIColor, openedView givenOpenedView: UIView) {
            
            self.openedView = givenOpenedView
            openedView.translatesAutoresizingMaskIntoConstraints = false
            
            // VIEW
            self.view = UIView(frame: .zero)
            view.translatesAutoresizingMaskIntoConstraints = false
            view.clipsToBounds = true
            
            self.topOpenedView = UIView(frame: .zero)
            topOpenedView.clipsToBounds = true
            topOpenedView.translatesAutoresizingMaskIntoConstraints = false
            topOpenedView.addSubview(openedView)
            openedView.clipsToBounds = true

            topOpenedView.topAnchor.constraint(equalTo: openedView.topAnchor).isActive = true
            topOpenedView.leadingAnchor.constraint(equalTo: openedView.leadingAnchor).isActive = true
            topOpenedView.trailingAnchor.constraint(equalTo: openedView.trailingAnchor).isActive = true
            self.openedViewBottomConstraint = topOpenedView.bottomAnchor.constraint(equalTo: openedView.bottomAnchor)
            openedViewBottomConstraint.isActive = false
            self.openedViewHeightConstraint = topOpenedView.heightAnchor.constraint(equalToConstant: 0)
            openedViewHeightConstraint.isActive = true
            
            for constraint in openedView.constraints {
                constraint.priority -= 1
            }
            // ---
            
            self.cellView = UIView(frame: .zero)
            cellView.translatesAutoresizingMaskIntoConstraints = false
            cellView.clipsToBounds = true
            
            // Button
            self.button = UIButton(frame: .zero, primaryAction: UIAction(handler: buttonHandler))
            button.translatesAutoresizingMaskIntoConstraints = false
            
            // Add subviews
            cellView.addSubview(button)
            cellView.heightAnchor.constraint(equalToConstant: 45).isActive = true
            
            cellView.leadingAnchor.constraint(equalTo: button.leadingAnchor).isActive = true
            cellView.trailingAnchor.constraint(equalTo: button.trailingAnchor).isActive = true
            cellView.topAnchor.constraint(equalTo: button.topAnchor).isActive = true
            cellView.bottomAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
            
            cellView.backgroundColor = color
            
            cellView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
            
            // View
            view.layer.cornerRadius = 10
            view.backgroundColor = .clear
            
            view.addSubview(cellView)
            view.addSubview(topOpenedView)
            
            view.topAnchor.constraint(equalTo: cellView.topAnchor).isActive = true
            view.leadingAnchor.constraint(equalTo: cellView.leadingAnchor).isActive = true
            view.leadingAnchor.constraint(equalTo: topOpenedView.leadingAnchor).isActive = true
            view.trailingAnchor.constraint(equalTo: cellView.trailingAnchor).isActive = true
            view.trailingAnchor.constraint(equalTo: topOpenedView.trailingAnchor).isActive = true
            view.bottomAnchor.constraint(equalTo: topOpenedView.bottomAnchor).isActive = true
            topOpenedView.topAnchor.constraint(equalTo: cellView.bottomAnchor).isActive = true
            
            cellView.bringSubviewToFront(button)
        }
        
        func changeOpened(to open: Bool, animated: Bool) {
            if isOpenedProtected == open { return }
            isOpenedProtected = open
            
            let duration: Double = animated ? 0.5 : 0.0
            
            self.view.layoutIfNeeded()
            
            print(open ? "Opening" : "Closing")
            
            self.openedViewHeightConstraint.isActive = !open
            self.openedViewBottomConstraint.isActive = open
            
            UIView.animate(withDuration: duration/*, delay: 0, options: .transitionFlipFromTop*/) {
                self.view.layoutIfNeeded()
            }
        }
    }

This class basically creates a View that when pressing it another View opens below it

This is my code calling the class: (in view did load):

        let myView = UIView(frame: .zero)
        myView.translatesAutoresizingMaskIntoConstraints = false
        myView.backgroundColor = .systemPink
        myView.heightAnchor.constraint(equalToConstant: 120).isActive = true
        
        let cell = Cell(tint: .blue, openedView: myView)
        
        view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(cell.view)
        cell.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        cell.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        cell.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

But the animation result is not what I expected:

as you can see, the animation is jumpy because its starting from the middle instead of the top. I want the blue view to stay in its place when the pink view opens and closes.

Thanks in advance


Solution

  • That seems like a rather convoluted way to create a custom view, but...

    First, do NOT set .translatesAutoresizingMaskIntoConstraints = false on a view controller's view. So, in your viewDidLoad() func:

        let cell = Cell(tint: .blue, openedView: myView)
    
        // DO NOT DO THIS       
        //view.translatesAutoresizingMaskIntoConstraints = false
    
        view.addSubview(cell.view)
    

    The weird animation is because you're animating the wrong view.

    Change the end of your changeOpened(...) func to this:

        guard let sv = self.view.superview else {
            return
        }
    
        UIView.animate(withDuration: duration/*, delay: 0, options: .transitionFlipFromTop*/) {
            //self.view.layoutIfNeeded()
            sv.layoutIfNeeded()
        }