Fred White
Fred White

Reputation: 319

How to change layout of views between landscape and portrait mode?

In portrait mode I have four views on top of one another from the top to the bottom of the view controller (see image).

portrait view

I then want to change the position of the views relative to one another when the device transitions to landscape (see image two).landscape view

I want view 4 to move alongside views 2 and 3 and them all the sit below view 1.

Some of the layout conditions:

What is the best method to achieve the different layouts?

Would the most elegant solution be to make a reference to the constraints in the view controllers code and activate and deactivate them in viewWillTransition? Or is there a way to use vary for traits to achieve this (I can imagine views 2, 3 & 4 being centered horizontally would make this hard to achieve, as well as adding the new constraints for view 4 in landscape mode)?

Upvotes: 11

Views: 23470

Answers (3)

P. Stern
P. Stern

Reputation: 351

Xcode 13

Xcode 13 removed "Vary for Traits" and replaced it with the ability to install/uninstall constraints for different size classes. Here's an update to @Rob's answer, using the new process.

I'm going to use the term orientation, for ease of explanation, but it's really size class. For example portrait orientation on an iPhone is generally regular height/compact width. Landscape on an iPhone is generally compact height/regular width.

First, set all constraints for one orientation. In this case the red and blue views are constrained to the edges of the safe area or each other. I gave the blue view a 1:1 ratio. enter image description here

Then go through each constraint that needs to change with orientation and uninstall the constraint for the other orientation (size class). I'll demonstrate with the blue view's leading alignment constraint which changes from the safe area's leading edge in portrait to the red view's trailing edge in landscape.

On the blue view's leading alignment constraint, select the plus sign next to "Installed" and chose Width: Any, Height: Compact (ie. landscape). enter image description here

Select "Add Variation" and uncheck the new variation (next to "hC"). You're basically telling Xcode not to use this leading alignment constraint for compact height (landscape). enter image description here

Now, change orientation in Interface Builder, and add a constraint between blue's leading edge and red's trailing edge.

Note: If the views disappear off screen, you can add new constraints by control-dragging between the views in the outline on the left. Eventually, all views should appear as you expect in both orientations. enter image description here

Select the new constraint and add a new variation for Width: Any, Height: Regular (ie. portrait). enter image description here

Uncheck the new variation (next to "hR"), telling Xcode not to use this leading alignment constraint for regular height (portrait). enter image description here

If a constraint doesn't change with orientation, then there will only be one "Installed" constraint for both orientations. For example, the blue view's trailing and bottom edges are constrained to the safe area's respective edges in both orientations.

Also note, the icons next to the constraints in the outline on the left are blue if they apply to the current orientation in Interface Builder, and gray if they don't apply to the current orientation.

enter image description here

Upvotes: 0

Rob
Rob

Reputation: 437482

We used to set up different sets of constraints and activate/deactivate them based upon orientation change. But nowadays can use size classes and "vary for traits".

For example, I start with a simple view and choose a compact width size class and then choose "Vary for traits":

enter image description here

I then add the appropriate constraints and click on "Done varying":

enter image description here

I then choose a "regular width size class" and repeat the process ("Vary for traits", add the constraints, click on "Done varying":

enter image description here

You then end up with a scene that will have a completely different set of constraints active for compact width size classes and regular width size classes. I.e. when I run the app, if the device rotates, the new constraints are activated:

enter image description here

For more information, see WWDC 2016 videos on adaptive layouts:

Upvotes: 26

user7014451
user7014451

Reputation:

I use arrays of constraints and activate/deactivate according to orientation.

var p = [NSLayoutConstraint]()
var l = [NSLayoutConstraint]()
var initialOrientation = true
var isInPortrait = false

override func viewDidLoad() {
    super.viewDidLoad()
    // add any subviews here
    view.turnOffAutoResizing()        
    // add constraints here....
    //    common constraints you can set their isActive = true
    //    otherwise, add in to portrait(p) and landscape(l) arrays
}

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)
        }
    }
}
extension UIView {
    public func turnOffAutoResizing() {
        self.translatesAutoresizingMaskIntoConstraints = false
        for view in self.subviews as [UIView] {
           view.translatesAutoresizingMaskIntoConstraints = false
        }
    }
    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)
        }
    }
}

My needs to distinguish portrait or landscape require using something other than size classes, as my app is universal and iPad (unless using split or slide out view) is always normal size. Also, you may get use viewWillTransistion(toSize:) or viewDidLoadSubviews() instead of 'viewWillLoadSubviews()` - but always test, as these may be executed more than once on an orientation change!

Upvotes: 1

Related Questions