-1

I am practicing auto-layout programmatically. I want to put a UIView centered in the controller whose width will be 4/5 in portrait mode but when it will go to the landscape mode, I need the height to be of 4/5 of the super view's height, rather than the width.

Something like -

enter image description here

So, I am deactivating and then activating the constrains required depending on the orientation but when I change rotation, it gives me conflict as if it didn't deactivated the ones, I specified to be deactivated. Here is my full code. As It is storyboard independent, one can just assign the view controller class to a view controlller and see the effect.


class MyViewController: UIViewController {
    
    var widthSizeClass = UIUserInterfaceSizeClass.unspecified
    
    var centeredView : UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.systemGreen
        return view
    }()
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.view.addSubview(centeredView)
        centeredView.translatesAutoresizingMaskIntoConstraints = false
    }
    
    override func viewWillLayoutSubviews(){
        super.viewWillLayoutSubviews()
        widthSizeClass = self.traitCollection.horizontalSizeClass
        addConstrainsToCenterView()
    }
    
    func addConstrainsToCenterView() {
        
        centeredView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        centeredView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
       
        let compactWidthAnchor = centeredView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 4/5)
        let compactHeightAnchor = centeredView.heightAnchor.constraint(equalTo: centeredView.widthAnchor)
       
        
        let regularHeightAnchor = centeredView.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor, multiplier: 4/5)
        let regularWidthAnchor = centeredView.widthAnchor.constraint(equalTo: centeredView.heightAnchor)
        
        if widthSizeClass == .compact{
            NSLayoutConstraint.deactivate([regularWidthAnchor, regularHeightAnchor])
            NSLayoutConstraint.activate([compactWidthAnchor, compactHeightAnchor])
        }
        else{
            NSLayoutConstraint.deactivate([compactWidthAnchor, compactHeightAnchor])
            NSLayoutConstraint.activate([regularWidthAnchor, regularHeightAnchor])
        }
    }
}

Can anyone please help me detect my flaw.

3
  • For the most part, this is how I do this successfully. (I actually create arrays instead of distinctly named constraints. I don't see this as an issue.) But the only thing that jumps out at me if the use of size classes. They are more closely tied to a different override - and if you are using an iPad, the won't change unless the iPad is put into split screen. A suggestion, one the may require a small rewrite? Check the screen width/height on viewWillLayoutSubviews (or better viewDidLayoutSubviews) and activate/deactivate like you already are. Commented Apr 5, 2021 at 16:28
  • the size class is not the issue here. If I just run it on iPhone 12 pro max, it should work fine without giving me any conflict. However, it is giving me conflict. You can also check by copying the whole code into a view controller as it is the whole code for a viewcontroller. Commented Apr 5, 2021 at 16:32
  • Posting some code that works for me. Maybe it'll help? Commented Apr 5, 2021 at 16:35

3 Answers 3

1

Couple issues...

1 - many iPhone models only have wC hR (portrait) and wC hC (landscape) size classes. So, if you're checking for the .horizontalSizeClass on those devices it will always be .compact. You likely want to be checking the .verticalSizeClass

2 - the way you have your code, you are creating NEW constraints every time you call addConstrainsToCenterView(). You're not activating / deactivating existing constraints.

Take a look at this:

class MyViewController: UIViewController {
    
    var heightSizeClass = UIUserInterfaceSizeClass.unspecified

    var centeredView : UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.systemGreen
        return view
    }()

    // constraints to activate/deactivate
    var compactAnchor: NSLayoutConstraint!
    var regularAnchor: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addSubview(centeredView)
        centeredView.translatesAutoresizingMaskIntoConstraints = false

        // centeredView is Always centerX and centerY
        centeredView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        centeredView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
        
        // for a square (1:1 ratio) view, it doesn't matter whether we set
        //  height == width
        // or
        //  width == height
        // so we can set this Active all the time
        centeredView.heightAnchor.constraint(equalTo: centeredView.widthAnchor).isActive = true
        
        // create constraints to activate / deactivate
        
        // for regular height, set the width to 4/5ths the width of the view
        regularAnchor = centeredView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 4/5)

        // for compact height, set the height to 4/5ths the height of the view
        compactAnchor = centeredView.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor, multiplier: 4/5)
        
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        // use .verticalSizeClass
        heightSizeClass = self.traitCollection.verticalSizeClass

        updateCenterViewConstraints()
    }
    
    func updateCenterViewConstraints() {
        
        if heightSizeClass == .compact {
            // if height is compact
            regularAnchor.isActive = false
            compactAnchor.isActive = true
        }
        else{
            // height is regular
            compactAnchor.isActive = false
            regularAnchor.isActive = true
        }
    }
}

With that approach, we create two vars for the constraints we want to activate/deactivate:

    // constraints to activate/deactivate
    var compactAnchor: NSLayoutConstraint!
    var regularAnchor: NSLayoutConstraint!

Then, in viewDidLoad(), we add centeredView to the view, set its "non-changing" constraints - centerX, centerY, aspect-ratio - and create the two activate/deactivate constraints.

When we change the size class, we only have to deal with the two var constraints.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for pointing out the exact reason of my error.
1

Possibly not an answer, but to go along with my comment, here's code I use successfully:

var p = [NSLayoutConstraint]()
var l = [NSLayoutConstraint]()

Note, p and l are arrays and stand for portrait and landscape respectively.

override func viewDidLoad() {
    super.viewDidLoad()
    setupConstraints()
}

Nothing much here, just showing that constraints can be set up when loading the views.

func setupConstraints() {        

    // for constraints that do not change, set `isActive = true`
    // for constants that do change, use `p.append` and `l.append`
    // for instance:

    btnLibrary.widthAnchor.constraint(equalToConstant: 100.0).isActive = true         

    p.append(btnLibrary.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 10))
    l.append(btnLibrary.bottomAnchor.constraint(equalTo: btnCamera.topAnchor, constant: -10))

Again, nothing much here - it looks like you are doing this. Here's the difference I'm seeing in your view controller overrides:

var initialOrientation = true
var isInPortrait = false

override func viewWillLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if initialOrientation {
        initialOrientation = false
        if view.frame.width > view.frame.height {
            isInPortrait = false
        } else {
            isInPortrait = true
        }
        view.setOrientation(p, l)
    } else {
        if view.orientationHasChanged(&isInPortrait) {
            view.setOrientation(p, l)
        }
    }
}

public func orientationHasChanged(_ isInPortrait:inout Bool) -> Bool {
    if self.frame.width > self.frame.height {
        if isInPortrait {
            isInPortrait = false
            return true
        }
    } else {
        if !isInPortrait {
            isInPortrait = true
            return true
        }
    }
    return false
}
public func setOrientation(_ p:[NSLayoutConstraint], _ l:[NSLayoutConstraint]) {
    NSLayoutConstraint.deactivate(l)
    NSLayoutConstraint.deactivate(p)
    if self.bounds.width > self.bounds.height {
        NSLayoutConstraint.activate(l)
    } else {
        NSLayoutConstraint.activate(p)
    }
}

Some of this may be overkill for your use. But instead of size classes, I actually check the bounds, along with detecting the initial orientation. For my use? I'm actually setting either a sidebar or a bottom bar. Works in all iPhones and iPads.

Again, I'm not seeing anything major - activating/deactivating a named(?) array of constraints instead of creating the arrays, the order of doing this, the override you are using... the one thing that jumps out (for me) is looking at size classes. (Possibly finding out what the initial size class is?)

I'm currently working through documenting how a UISplitViewController decided to show either the Secondary or Compact VC. Turns out that it behaves differently in at least five groups - iPad (always Secondary), iPad split screen (Compact in all iPads except iPad Pro 12.9 in landscape when half screen), iPhone portrait (always Compact), and finally, iPhone Landscape (compact for most, but Secondary for (iPhone 8 Plus, iPhone 11, iPhone 11 Pro Max, and iPhone 12 Pro Max.)

NOTE: It's Compact for iPhone 11 Pro, iPhone 12, and iPhone 12 Pro! I was surprised at this. (Next up for me is directly testing the size classes.)

My point? Maybe you need to go at the screen bounds to determine what layout you want instead of size classes. That is more in your control than size classes. Either way, good luck!

Comments

1

It’s quite simple. Each time layout happens and each time you say eg:

let regularWidthAnchor = centeredView.widthAnchor.constraint(equalTo: centeredView.heightAnchor)
NSLayoutConstraint.deactivate([regularWidthAnchor, regularHeightAnchor])

you are not deactivating the existing active constraint in the interface. You are creating a completely new constraint and then deactivating it (which is pointless as it was never active) and then throwing it away.

You need to create these constraints just once and keep references to them.

2 Comments

Thank you for pointing out the root cause of my issue.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.