Search code examples
iosswiftautolayout

Animate Autolayout constraints in seperate class throwing error even when needforlayout() is called


I've created a minimum working example of the issue on https://github.com/rDivaDuck/TestAppError

My HomeController is a UICollectioviewController and I have a settings launcher class that I'm trying to animate up from the bottom on button press from the navigation bar, the code I have achieves this by deactivating a down constraint and activating an up constraint and then calling window.layoutifneeded() to animate the change. However, I'm still thrown a typical constraint conflict error that the down constraint is still active.

I can achieve the effect fine using .frame and animating a CGRect change but I want to get the same effect using autolayout.

class SettingsLauncher {

    var homeController: HomeController?
    let shadowView = UIView()

    let MenuView: UIView = {
        let View = UIView(frame: .zero)
        View.backgroundColor = UIColor.white
        View.translatesAutoresizingMaskIntoConstraints = false
        return View
    }()

    var downConstraint: NSLayoutConstraint?
    var upConstraint: NSLayoutConstraint?

    func showSettings() {
        if let window = UIApplication.shared.keyWindow{
            shadowView.backgroundColor = UIColor(white: 0,
                                                alpha: 0.4)
            shadowView.addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                                  action: #selector(settingsDown)))
            window.addSubview(shadowView)
            shadowView.frame = window.frame
            shadowView.alpha = 0

            window.addSubview(MenuView)

            downConstraint = MenuView.topAnchor.constraint(equalTo: window.bottomAnchor)
            downConstraint?.isActive = true
            upConstraint = MenuView.topAnchor.constraint(equalTo: window.bottomAnchor, constant: -300)
            MenuView.heightAnchor.constraint(equalToConstant: 300).isActive = true
            MenuView.leadingAnchor.constraint(equalTo: window.leadingAnchor).isActive = true
            MenuView.trailingAnchor.constraint(equalTo: window.trailingAnchor).isActive = true
            MenuView.layoutIfNeeded()
            window.layoutIfNeeded()
            settingsUp()
        }
    }

    func settingsUp() {
        downConstraint?.isActive = false
        upConstraint?.isActive = true
        UIView.animate(withDuration: 0.5,
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 1,
                       options: .curveEaseOut,
                       animations: {
                        self.shadowView.alpha = 1
                        if let window = UIApplication.shared.keyWindow {
                            window.layoutIfNeeded()
                        }
        }, completion: nil)
    }

    @objc func settingsDown() {
        downConstraint?.isActive = true
        upConstraint?.isActive = false
        UIView.animate(withDuration: 0.5,
                       animations: {
                        self.shadowView.alpha = 0
                        if let window = UIApplication.shared.keyWindow {
                            window.layoutIfNeeded()
                        }
        }, completion: nil )
    }

}
(
    "<NSLayoutConstraint:0x600001b5e440 V:[UIWindow:0x7fde18514030]-(-300)-[UIView:0x7fde18525ea0]   (active)>",
    "<NSLayoutConstraint:0x600001b5f250 V:[UIWindow:0x7fde18514030]-(0)-[UIView:0x7fde18525ea0]   (active)>"
)

How can I fix this?


Solution

  • Hello and welcome to StackOverflow, there are multiple errors in your project. The main problem is that every time you are tapping the button and calling showSettings() your adding new constraints to MenuView that are in conflict with the previous ones so to fix this problem you should execute the constraint part of the code only once:

    var viewInitialized = false
    
    func showSettings() {
        if let window = UIApplication.shared.keyWindow {
            if !viewInitialized {
                viewInitialized = true
    
                shadowView.backgroundColor = UIColor(white: 0, alpha: 0.4)
                shadowView.addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                                       action: #selector(settingsDown)))
                window.addSubview(shadowView)
                shadowView.frame = window.frame
                shadowView.alpha = 0
    
                window.addSubview(MenuView)
    
                downConstraint = MenuView.topAnchor.constraint(equalTo: window.bottomAnchor)
                downConstraint?.isActive = true
                upConstraint = MenuView.topAnchor.constraint(equalTo: window.bottomAnchor, constant: -300)
                MenuView.heightAnchor.constraint(equalToConstant: 300).isActive = true
                MenuView.leadingAnchor.constraint(equalTo: window.leadingAnchor).isActive = true
                MenuView.trailingAnchor.constraint(equalTo: window.trailingAnchor).isActive = true
                MenuView.layoutIfNeeded()
                window.layoutIfNeeded()
            }
            settingsUp()
        }
    }
    

    Also in settingsDown() switch the activations statement so you deactivate the constraint before activating the other, sometimes this causes a warning.

    @objc func settingsDown() {
        upConstraint?.isActive = false
        downConstraint?.isActive = true
        ...
    

    The other problems of the project that I found:

    • In AppDelegate.swift why are you creating a new window? There is already one and you can use it. Just delete this line of code that is useless window = UIWindow(frame: UIScreen.main.bounds)
    • Why are you creating the navigation controller in AppDelegate.swift? add it to the storyboard as the first view controller. Same thing for the HomeController add a CollectionViewController to the storyboard and change its class to HomeController
    • It's not a good practice to add views directly to the main window. You should present the settings view as a modal view controller with the presentation style stetted to over current context
    • In SettingsLauncher.swift shadowView is missing the constraints