Using Auto Layout to Orientate Stack Views Vertically in Portrait and Horizontally in Landscape

Using auto layout to orientate stack views vertically in portrait and horizontally in landscape

You would need to programmatically update the axis property of UIStackView when rotation happens. Docs say:

The exact layout varies depending on the stack view’s axis,
distribution, alignment, spacing, and other properties.

There is no way to achieve this purely using a storyboard.

Working solution

I created a demo project and verified it works. I consists of a single view controller.

class ViewController: UIViewController {

@IBOutlet weak var stackView: UIStackView!

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator:UIViewControllerTransitionCoordinator) {

coordinator.animateAlongsideTransition({ (context) -> Void in

let orientation = UIApplication.sharedApplication().statusBarOrientation

if orientation.isPortrait {
self.stackView.axis = .Horizontal
} else {
self.stackView.axis = .Vertical
}

}, completion: nil)
}
}

stackView is a UIStackView created in a storyboard with initial axis set to Horizontal.

Sample Image

How to use Auto Layout to stack views vertically in portrait mode and horizontally in landscape mode?

Written in a UIViewController's -viewDidLoad method:

[super viewDidLoad];

// Assuming viewA and viewB have already been setup and have their
// translatesAutoresizingMaskIntoConstraints property set to NO

NSDictionary *views = NSDictionaryOfVariableBindings(viewA,viewB);

NSArray *a1, *a2, *a3, *a4, *a5, *a6, *a7, *a8;

// Construct all of the constraints
a1 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[viewA]|" options:0 metrics:nil views:views];
a2 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[viewB]|" options:0 metrics:nil views:views];
a3 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[viewA][viewB]|" options:0 metrics:nil views:views];
a4 = @[[NSLayoutConstraint constraintWithItem:viewB attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:viewA attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0]];
self.portraitConstraints = @[a1,a2,a3,a4];

a5 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[viewA][viewB]|" options:0 metrics:nil views:views];
a6 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[viewA]|" options:0 metrics:nil views:views];
a7 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[viewB]|" options:0 metrics:nil views:views];
a8 = @[[NSLayoutConstraint constraintWithItem:viewB attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:viewA attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];

self.landscapeConstraints = @[a5,a6,a7,a8];

// Add all of the constraints
for (NSArray *constraints in self.portraitConstraints) {
[self.view addConstraints:constraints];
}
for (NSArray *constraints in self.landscapeConstraints) {
[self.view addConstraints:constraints];
}

// Activate the constraints based on the orientation
void(^constraintBlock)() = ^{
// I'm using 'active' property instead of adding and removing constraints. I haven't
// checked to see which way is "better" but it works well so I won't bother!
if (UIDeviceOrientationIsPortrait([[UIApplication sharedApplication]statusBarOrientation])) {
for (NSArray *constraints in self.landscapeConstraints) {
for (NSLayoutConstraint *c in constraints) {
c.active = NO;
}
}
for (NSArray *constraints in self.portraitConstraints) {
for (NSLayoutConstraint *c in constraints) {
c.active = YES;
}
}
} else {
for (NSArray *constraints in self.portraitConstraints) {
for (NSLayoutConstraint *c in constraints) {
c.active = NO;
}
}
for (NSArray *constraints in self.landscapeConstraints) {
for (NSLayoutConstraint *c in constraints) {
c.active = YES;
}
}

}
};

// Call the block immediately to setup the constraints
constraintBlock();

// self.observer is a property I would then use in the viewController's -dealloc
// method to remove it as an observer of the default NSNotificationCenter
self.observer = [[NSNotificationCenter defaultCenter]addObserverForName:UIApplicationDidChangeStatusBarOrientationNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// Update the constraints whenever the status bar orientation changes
constraintBlock();
}];

There are many other ways you could do this as well!

Different layouts in portrait and landscape mode

Note - the answers here are good and do address the problem, but on older versions of iOS.

For iOS11 (Xcode 9) you should consider Adaptive Layouts as referenced here:
https://www.raywenderlich.com/162311/adaptive-layout-tutorial-ios-11-getting-started

Layout change for landscape/portrait

You can vary by traits. Here is a long walkthrough of one way to do it. Hope this all makes sense but there are several ways with Autolayout to do this.

Step 1. Drag a view onto the storyboard and pin to top layout guided, leading,trailing and bottom at 0. We will need this later to center everything.

Step1

Step 2. Drag Another view that will hold all of our content. And pin it to the edges at say 20 points for top, bottom, leading, and trailing. Note: We will be adding some more constraints and changing this later so just take note.
Step2

Step 3. Drag another view on to the screen and add 3 uilabels to it. Click the top label and add the constraints in the image.Step3

Step 4. Select the second label down and add bottom-10, leading and trailing 0. No need for top because it was added in step 3.

Step 5. Repeat for bottom label the same as step 4 add the 3 constraints.

Step 6. Click this entire view and go to the top menu and select Embed in StackView. See Image
EmbedInStackView

Step 7. Add a UILabel above it to hold the Lap Text and pin it to the top 20, leading,trailing at 0, and bottom which is the stackview at 20. See Image
PinLabel

Step 8. Now pin the stackview to leading 0,trailing 0, bottom- 0 or 20.

Step 9. Duplicate the view with the labels 2 Times and drag them using the viewhierchy editor on the left into the stackview so now your stackview has 3 views. It should now look like the image.
StackAlmost

Step 10. Now change every label in the entire view to have a content hugging vertical of 1000. Also change the font on each to desired font and also set so that the font can shrink to a factor of say 0.5. See Image for content hugging setting.
ContentHugging

Step 11. Now go back to the original container that is holding the UILabel and the stackview and change all the constraints of 20 to >= to in the editor and add and drag to the view holding the container(gray in the images) and choose center vertically and center horizontal. I combined image showing the different menus is attached below.

AttachedMenus

Step 12.
Add variations for the stackview and the correct setup. Click the plus beside axis and add this.

[![TraitAxis][9]][9]

Step 13. Now add another variation for Alignment and your final stackview setup should look like this.
Final

The payoff will look like this. Provided you have set the fonts you want and allow them to scale. I deleted some of the extra labels to match your image and when doing so I had to pin the remain bottom label to the labels container. You probably will have to add another alignment variation for Plus devices but that would be just adding the variations like in the final steps. You might also have to resolve some lower priority on labels but I think clicking and saying ok will get that done. Just do whatever it tell you to do. I hope this helps. You could also nest stack views and the layout might be easier. Like I said there are probably many more ways to accomplish this but this should get you on the right path.

Payoff

Working with AutoLayout in Portrait and Landscape

There are several ways to approach this. One way is to enclose your 3 image views in a UIView. Give the top and bottom image views constraints to the top and bottom respectively, and give the middle one a centerY constraint. Give this enclosing view a fixed height and width, and a constraint to the top of the controller's view. Make an IBOutlet to the height constraint for this enclosing view, and change it on rotation. In the example below, I gave it a height of 350 in portrait:

-(void)updateViewConstraints {
[super updateViewConstraints];
if (self.view.bounds.size.height < self.view.bounds.size.width) {
self.heightCon.constant = self.view.bounds.size.height;
}else{
self.heightCon.constant = 350;
}
}

As for the label, the easiest way is to remove the constraints you have (bottom and centerX), and add trailing and centerY on rotation.

Two views, one below another in portrait and side by side in landscape using layout constraints

Here's how you might go about it in code.

Basically you need to:

a) configure the appropriate NSLayoutConstraints for the given orientation in updateViewConstraints in your UIViewController.

b) call [self.view setNeedsUpdateConstraints] when the interface rotates.

Below is a ViewController implementation and a category on UIView with helper methods.

@interface ConstraintsViewController ()

@property (nonatomic, weak) IBOutlet UIView *upperOrLeftView, *lowerOrRightView;

@end

@implementation ConstraintsViewController

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.view setNeedsUpdateConstraints];
}

-(void)updateViewConstraints {
[super updateViewConstraints];

[self.view removeConstraintsRelatingToItems:@[self.upperOrLeftView,self.lowerOrRightView]];

if(UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
[self.view constrainSubview:self.upperOrLeftView usingEdgeInsets:UIEdgeInsetsMake(0, 0, -1, 0)];
[self.view constrainSubview:self.lowerOrRightView usingEdgeInsets:UIEdgeInsetsMake(-1, 0, 0, 0)];
[self.view constrainSubviewsTopToBottom:@[self.upperOrLeftView, self.lowerOrRightView]];
}
else {
[self.view constrainSubview:self.upperOrLeftView usingEdgeInsets:UIEdgeInsetsMake(0, 0, 0, -1)];
[self.view constrainSubview:self.lowerOrRightView usingEdgeInsets:UIEdgeInsetsMake(0, -1, 0, 0)];
[self.view constrainSubviewsLeftToRight:@[self.upperOrLeftView, self.lowerOrRightView]];
}
}

@end

Put this in UIView+Constraints.h

@interface UIView (Constraints)

-(void)removeConstraintsRelatingToItems:(NSArray*)items;

-(void)constrainSubview:(UIView*)subview usingEdgeInsets:(UIEdgeInsets)insets;

-(void)constrainSubviewsLeftToRight:(NSArray*)subviews;

-(void)constrainSubviewsTopToBottom:(NSArray*)subviews;

@end

This is UIView+Constraints.m

@implementation UIView (Constraints)

-(void)removeConstraintsRelatingToItems:(NSArray *)items {
for(NSLayoutConstraint *constraint in self.constraints) {
if([items containsObject:constraint.firstItem] || [items containsObject:constraint.secondItem]) {
[self removeConstraint:constraint];
}
}
}

/** Set up constraints to flow the subviews from top to bottom and with equal heights */
-(void)constrainSubviewsTopToBottom:(NSArray*)subviews {
if(subviews.count > 1) {
UIView *anchorView = subviews[0];
for(int i = 1; i < subviews.count; i++) {
UIView *view = subviews[i];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:anchorView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0];
NSLayoutConstraint *edgesConstraint = [NSLayoutConstraint constraintWithItem:anchorView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0];
[self addConstraints:@[heightConstraint, edgesConstraint]];
anchorView = view;
}
}
}

/** Set up constraints to flow the subviews from left to right and with equal widths */
-(void)constrainSubviewsLeftToRight:(NSArray*)subviews {
if(subviews.count > 1) {
UIView *anchorView = subviews[0];
for(int i = 1; i < subviews.count; i++) {
UIView *view = subviews[i];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:anchorView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0];
NSLayoutConstraint *edgesConstraint = [NSLayoutConstraint constraintWithItem:anchorView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0];
[self addConstraints:@[widthConstraint, edgesConstraint]];
anchorView = view;
}
}
}

/**
Set up constraints to anchor the various edges of the subview to it's superview (this view) using the provided insets.
Any inset set to < 0.0 means that edge is ignored;
*/
-(void)constrainSubview:(UIView*)subview usingEdgeInsets:(UIEdgeInsets)insets {
if(insets.top >= 0.0) {
[self addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:insets.top]];
}

if(insets.right >= 0.0) {
[self addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:-insets.right]];
}

if(insets.bottom >= 0.0) {
[self addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-insets.bottom]];
}

if(insets.left >= 0.0) {
[self addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:insets.left]];
}
}

@end

StackView change to match device orientation

Use Auto Layout to design UI, orientation will be taken care of automatically.

See a tutorial here, it should answer your question in detail.

XCode: How to change layout of views between landscape and portrait mode

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":

Sample Image

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

Sample Image

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

Sample Image

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:

Sample Image

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

  • https://developer.apple.com/videos/play/wwdc2016/222
  • https://developer.apple.com/videos/play/wwdc2016/233

iOS: how to have a different layout depending on app orientation?

OK - step-by-step...

Add 4 buttons to your view controller:

Sample Image

Embed Buttons 1 & 2 in a stack view, and embed Buttons 3 & 4 in a stack view:

Sample Image

Give each stack view these settings:

Sample Image

Now, embed the two stack views in another "outer" stack view:

Sample Image

Same stack view settings:

Sample Image

and constrain this "outer" stack view Leading / Trailing / Bottom at 40-pts.

Next, select the Buttons 1 & 2 stack view, and in the Attributes Inspector pane click the + to the left of Axis and change Width to Any and Height to Compact:

Sample Image

Click Add Variation and it will look like this:

Sample Image

Change the hC: Vertical to hC: Horizontal:

Sample Image

Do the same thing with the 3 & 4 stack view.

Now when you rotate the device, it will look like this:

Sample Image

Adjust the constraints and your stack view Spacing from 40 to your desired values.

No code needed - everything is handled by auto-layout!



Related Topics



Leave a reply



Submit