Search code examples
swiftcocoaconstraints

How to update my constraints with animation without losing children constraints on the parent?


I have a blueView as a parent that has for own a custom constraint and this blueView has a subView called redView, the constraint of redView is always same like in the code, but the constraint of blueView is going updated after lunching app in 2 sec.

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        
        let blueView: NSView = NSView()
        blueView.wantsLayer = true
        blueView.layer?.backgroundColor = NSColor.blue.cgColor

        self.view.addSubview(blueView)
        
        
        blueView.translatesAutoresizingMaskIntoConstraints = false
        blueView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 50.0).isActive = true
        blueView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 50.0).isActive = true
        blueView.widthAnchor.constraint(equalToConstant: 200.0).isActive = true
        blueView.heightAnchor.constraint(equalToConstant: 200.0).isActive = true
        

        let redView: NSView = NSView()
        redView.wantsLayer = true
        redView.layer?.backgroundColor = NSColor.red.cgColor
        

        blueView.addSubview(redView)
        redView.translatesAutoresizingMaskIntoConstraints = false
        redView.centerXAnchor.constraint(equalTo: blueView.centerXAnchor).isActive = true
        redView.centerYAnchor.constraint(equalTo: blueView.centerYAnchor).isActive = true
        redView.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
        redView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true


        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(2)) {
            
            blueView.removeConstraints(blueView.constraints)
            
            blueView.translatesAutoresizingMaskIntoConstraints = false
            blueView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            blueView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true

            blueView.widthAnchor.constraint(equalToConstant: 300.0).isActive = true
            blueView.heightAnchor.constraint(equalToConstant: 300.0).isActive = true

            
        }

    }

}

The issue that I got is Xcode complains about Unable to simultaneously satisfy constraints: and i think this complain is about this that I am updating my constraint in wrong way.

The second issue is that redView lose its constraint in process of updating constraint for blueView, which I want redView keeps its constraint to blueView as coded.

The third issue is that it happens without any animation, I want it happens with animation.


Solution

  • You're missing a couple of things. First, you don't attempt to animate anything. Second, you don't need to deactivate any constraints, you simply need to modify them.

    In order to modify a constraint, you must do it on that particular constraint instance. And to do this, you may have to expand its scope by making it an instance property of the view controller, which I have done. The constraint can either be optional and nil to start, which you can set later, or you can simply declare it on initialization and mark it lazy, as I did here, so that it's only read when it's safe to read.

    import PlaygroundSupport
    import AppKit
    
    class ViewController: NSViewController {
        private let blueView = NSView()
        private let redView = NSView()
        
        private lazy var blueViewCenterXAnchor = blueView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 50)
        private lazy var blueViewCenterYAnchor = blueView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 50)
        private lazy var blueViewWidthAnchor = blueView.widthAnchor.constraint(equalToConstant: 200)
        private lazy var blueViewHeightAnchor = blueView.heightAnchor.constraint(equalToConstant: 200)
        
        override func loadView() {
            view = NSView()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            blueView.wantsLayer = true
            blueView.layer?.backgroundColor = NSColor.blue.cgColor
            blueView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(blueView)
            blueViewCenterXAnchor.isActive = true
            blueViewCenterYAnchor.isActive = true
            blueViewWidthAnchor.isActive = true
            blueViewHeightAnchor.isActive = true
            
            redView.wantsLayer = true
            redView.layer?.backgroundColor = NSColor.red.cgColor
            redView.translatesAutoresizingMaskIntoConstraints = false
            blueView.addSubview(redView)
            redView.centerXAnchor.constraint(equalTo: blueView.centerXAnchor).isActive = true
            redView.centerYAnchor.constraint(equalTo: blueView.centerYAnchor).isActive = true
            redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
            redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
            
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(2)) {
                self.blueViewCenterXAnchor.constant = 0
                self.blueViewCenterYAnchor.constant = 0
                self.blueViewWidthAnchor.constant = 300
                self.blueViewHeightAnchor.constant = 300
                
                NSAnimationContext.runAnimationGroup { context in
                    context.allowsImplicitAnimation = true
                    context.duration = 3
                    self.view.layoutSubtreeIfNeeded()
                }
            }
        }
    }
    
    PlaygroundPage.current.liveView = ViewController()