Creating a percentage based iOS layout
Asked Answered
E

6

53

I'm trying to replicate a layout that I currently have in an Android application, but I don't know how to go about it in iOS especially because of the tallness of the iPhone 5.

I know how to explain this in "Android" terms but I've been trying for the past few days to do this in iOS but I can't quite get it to work.

Best way to explain it:

  1. I want two layouts. The top layout must take up 40% and the bottom must take up 60%.
  2. In the top layout, they must be three buttons that fill up all space possible (Essentially 1/3 of the space)
  3. In the bottom layout, I want an imageView, and then a textView on top of that.

This is a paint mockup. Is this possible to do in iOS? I feel that layouts are much harder to create than android.

enter image description here

Erect answered 11/8, 2013 at 18:1 Comment(7)
Are you using Auto-Layout in IB? Or do you perhaps know about Springs and Struts?Carper
I haven't heard about "Springs and Struts". Could you implement that using the interface builder?Erect
disanji.net/iOS_Doc/#documentation/DeveloperTools/Conceptual/…Carper
Note that springs and struts is the old way to do it; Apple wants you to use Auto Layout from here on in, so if you're going to invest your time it's best to do so learning Auto Layout. And yes, it's horrible to use, but it should get significantly easier with the next major release of Xcode.Dehydrogenate
What is each "container" called in iOS? (the 40% and 60% containers?) Just empty views? I'm used to calling them layouts in Android.Erect
You'd usually refer to them as subviews of the main view. In implementation terms they'd be UIViews, or a subclass thereof (such as UIScrollView), which would be added to the root (parent) view, either through interface builder or programatically via the addSubview: method of the root view.Dehydrogenate
Alright, I think I'm getting the hang of it. Is there a way to split the subviews up 60/40?Erect
B
75

Using Xcode 6.0, you can now specify proportional width or height in Interface Builder. Steps for percentage height from superview:

While both the child view and its superview are selected, add an "equal height" constraint (or "equal width" if you wish to have a proportional width).

enter image description here

Then change the "multiplier" of the constraint you just added to the proportion you need. For example, for 50%, change it to 2.

If you like to specify the inner view as percentage of the superview, you can reverse the items in the constraint:

enter image description here

Now you can use a multiplier of 0.5 (or any other proportion you need):

enter image description here

In your specific case, you can define an "equal height" constraint between the 2 child views, and change the multiplier to 1.5 (the bottom being 1.5 the size of the top) or 0.6667 if the items are reversed.

See also How to make constant auto-resize in Storyboard?

Beanpole answered 12/10, 2014 at 9:36 Comment(7)
Hi, I found that if I select both child view and superview, all of the items in 'Pin' would be unselected, how could you select the "equal height" constraint and added? Maybe I miss something and would you please figure me out? thanks.Squeamish
how did you equal to super view ??Haematopoiesis
@Haematopoiesis To create a constraint that crosses multiple views in the view hierarchy, it is generally easier to Control-drag from the view to the container view in the Interface Builder outline view. In the constraint overlay that appears, set the required constraints for the view. developer.apple.com/library/ios/documentation/UserExperience/…Warm
How can I use this method to specify proportional position for an item? For instance: an element positioned always at 20% of the device height from the top.Shermanshermie
Powerful, I always avoid setting those settings as I really don't know what it is. Never thought we selected multiple items, then it makes relationship for us to create things like this one.Duello
Why there is not option for "Proportional Width/Height"? Setting "Equal widths" is not intuitive and messes up the views that you try to position.Flickertail
To position a view certain percent from the top, I would put a "vertical constraint in container" and set its multiplier to some value that is less than 1 (or more than 1 if positioning is from the bottom).Beanpole
U
37

Contrary to the other answers, I think you should at least consider the auto layout system. It was created to make it easier to build predictable layouts like the one you've described. Autolayout is ruled by constraints that you put on the views in the layout. You can create those constraints visually or programmatically. Visually, your first view would look something like this:

visual constraints

The blue lines you see there are a number of constraints that specify the inter-button spacing, the space around the buttons, and the heights and widths of the buttons. You can see a couple constraints that have a = on them -- I selected all three buttons and gave them an "equal height" constraint.

There's a nice visual format syntax that lets you describe constraints as strings. Take a minute to look at the linked document -- it won't take much longer than that to learn enough that you can read the strings. For example, your top layout can be specified like this:

V:[button1]-[button2(==button1)]-[button3(==button1)]

The parenthetical ==button1 tells the layout system to make button2 and button3 the same height as button1. The - indicates that the standard spacing should be used between the buttons; you can specify a different spacing if you want. For 10 point spacing, do this:

V:|-10-[button1]-10-[button2(==button1)]-10-[button3(==button1)]-10-|

Once you have such a string, you can turn it into a constraint using the method: +[NSLayoutConstraint constraintsWithVisualFormat:options:metrics:views:]

Some constraints can't be specified either visually or with the strings described above. Chief among these are those where you want to set a constant (but unequal) relationship between two views, as with your top and bottom layouts. You want one of those to take up 40% of the available vertical space, and the other to take 60%. Do this using the method: +[NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]. For example:

NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:bottomView
                                                     attribute:NSLayoutAttributeHeight
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:topView
                                                    multiplier:1.5
                                                      constant:0];

That gives you a constraint that sets the height of bottomView to 1.5 times the height of topView (which is what you want, since 60/40 = 1.5).

If you create constraints programmatically, you can add them to the appropriate view when you create (or load) the view hierarchy. Your view controller's -viewDidLoad method is a fine place to do that.

Uninterrupted answered 22/8, 2013 at 4:47 Comment(13)
I want to use autolayout though. Am I saying that I don't want to use it anywhere? I'm confused? Please let me know if I say that somewhere. My entire point, is that I want to use auto layout, and not use code to accomplish thisErect
Also, the buttons you explained to use in autolayout is good and well, but I'm still having trouble on doing 40/60 in interface builder.Erect
Additionally, the buttons should take up 1/3 of the view, setting them equal to each other still doesn't have them fill the view that they are nested in.Erect
@Erect Auto layout doesn't mean no code, it means setting up constraints that specify how the views may be laid out. (What's the rationale behind your no code requirement?) Some of that you can do visually, but some constraints (like your 40/60 split) can only be done with code. As for buttons: if you have 3 buttons and zero space between or around them, setting them to all be of equal height means that they'll each take up 1/3 of the space. So just set the heights equal and the space constraints to 0.Uninterrupted
Okay, this is just difficult to understand coming from android that something like 40/60 can only be done in code, but I was looking for a solid answer on that. Some people were giving me answers that you could seemingly accomplish 40/60 with auto layout.Erect
@Erect You can accomplish a 40/60 split with auto layout, but again, auto layout doesn't mean no code. Auto layout means that the framework can lay out your views automatically at run time according to the constraints you specify. Although many constraints can be specified visually, some cannot, and yet they're all part of the auto layout system. Try not to get hung up on comparisons with Android -- Android is practically forced to do things proportionally due to the wide variety of screen sizes it has to deal with. Absolute measurement is more feasible in iOS due to fewer screen sizes.Uninterrupted
I'm still trying to do the 1/3 buttons spacing, but I cannot get it to work. They take up 1/3 of the space, but I want them to take up 1/3 of the available space in the view. Is that possible?Erect
@Erect Not sure I understand that last comment completely, but it sounds like you've got a constraint on one of the buttons that sets it's height. Check each of the buttons for a constraint like 'height equals 42' and change it to 'height greater than 5' (or some small minimum value). I was able to do that entirely in the visual editor.Uninterrupted
Okay, I will try that again.Erect
Yeah, I tried setting a small min value, and I select all three buttons and click "heights equally", but they just stay the same size, and won't fill the view.Erect
let us continue this discussion in chatUninterrupted
+1 Can you specify a constraint such as your c in Xcode 5.1.1 Interface Builder? I would like to constrain a UIButton's trailing to 10% of its superview's width.Lichi
@Lichi No, you need to create constraints like that one in code as I described in the answer. Don't let that stop you though -- it's not at all difficult.Uninterrupted
D
2

I don't know about using autolayout as I dont use it, but in code without it, you could create two UIViews and set their frames to:

CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height * 0.4f);
CGRectMake(0, self.view.frame.size.height * 0.4f, self.view.frame.size.width, self.view.frame.size.width * 0.6f);

And then in the top view, you can add the buttons with frames (assuming the view is called view1):

CGRectmake(0, 0, self.view.frame.size.width, view1.frame.size.height * (1.0f/3.0f));
CGRectmake(0, view1.frame.size.height * (1.0f/3.0f), self.view.frame.size.width, view1.frame.size.height * (1.0f/3.0f));
CGRectmake(0, view1.frame.size.height * (2.0f/3.0f), self.view.frame.size.width, view1.frame.size.height * (1.0f/3.0f));
Detergent answered 11/8, 2013 at 18:6 Comment(0)
L
2

You need to add a couple of constraints to make it work. So here's a brief description of what you need:

  1. You will need horizontal spacing constraints. One if for the top view, because it has zero distance to the top. Another one for the bottom view, because its distance to the bottom is zero. One more constraint between the two views, because they have also zero spacing between each other.

  2. The same principle will be applied to the buttons inside the top view. Put your buttons with the correct size inside the top view and add similar constraints, but now you have three elements (instead of two). So constraints for zero spacing to the top, bottom and between buttons.

When you add components to your view, it will create a few constraints for you. Adding one one top and the other on bottom, it will create both spacing to the top and spacing between each other. To edit them, just go to inspector, fifth tab (ruler) and you will see the list of constraints. Lastly, you may want to try to constraints menu (I don't know if there's a well-known name for this). It's on the bottom-right corner of the Interface Builder canvas. See the image:

enter image description here

Please, let me know if you need further assistance.

Labiodental answered 22/8, 2013 at 1:42 Comment(10)
First click on the XIB or story board file and uncheck auto-layout check box so you can see this options :)Snob
That's correct, @MahmoudFayez . I forgot to mention that. Thank you.Labiodental
I thought auto sizing wasn't available in auto layout?Erect
As per the bounty "I would like to know how to do this for iOS 6+ (No springs and struts. Just auto-layout), and I would like to know how to do this in the interface builder."Erect
You can disable AutoLayout just like Mahmoud said. I think it's the easier way for who is coming from another SDK like Android. But to do that with AutoLayout, basically, for each UIView/UIButton, you're gonna need the following constraints: 1. Horizontal spacing for both UIViews set to 0 (to super view). One more set to zero to the distance between each other (top space to view instead of super view). 2. Same principle for buttons about spacing to top, bottom and to each other.Labiodental
Can you elaborate on this in your answer? "1. Horizontal spacing for both UIViews set to 0 (to super view). One more set to zero to the distance between each other (top space to view instead of super view). 2. Same principle for buttons about spacing to top, bottom and to each other" I get lost after you say "One more set to zero..."Erect
Gimme a couple of minutes and I'll post it with more detailed info.Labiodental
Where would I do that in interface builder?Erect
@EGHDK, I have added more details to my answer. Please, let me know if you need more detailed information.Labiodental
let us continue this discussion in chatErect
W
1

Refer to GK100's answer but let's extend that with below Q/A(s).

How to make constant auto-resize in Storyboard?

Percent based constant is not supported yet, and people try different workarounds, like:

  • Creating invisible UIView(s) as spacers with width/height and specifying a multiplier against the Safe Area size.
  • People even manually override Storyboard constraints in code:
    NSLayoutConstraint(item: myView, attribute: .top,
        relatedBy: .equal, toItem: myOtherView, attribute: .bottom,
        multiplier: 1,
        constant: UIScreen.main.bounds.width * 0.05)
    

While both of above are a headache, my solution's downside is that you need to hardcode at least shortDimensionInDesignTime to your preferred Interface Builder's preview-size (but far better than my PercentConstraint class).

Usage:

  1. Add below ResponsiveConstraint class's code to your project.
  2. Select your constraint in Storyboard editor.
  3. In Identity Inspector, use ResponsiveConstraint as class.
import Foundation

/// Auto-Resize based Layout-Constraint.
/// Later at run-time, resizes initial constant to screen's short dimension.
///
/// For Mac's Catalyst. remember to set size-restrictions to something like:
/// ```
/// let portrait: CGSize = ResponsiveConstraint.maxSize;
/// scene.sizeRestrictions?.maximumSize = portrait;
/// scene.sizeRestrictions?.minimumSize = portrait;
/// ```
///
/// WARNING: supports only Storyboard (by use as class in Identity Inspector),
/// but there is no harm in using static provided field and methods.
///
public class ResponsiveConstraint: NSLayoutConstraint {
    var actualConstant: CGFloat = 0;
    public var isValid: Bool { actualConstant > 0 }

    /// Preset shorter dimension of screen, within Design-Time only
    /// (i.e. it's your Interface-Builder's preview size).
    public static let shortDimensionInDesignTime: CGFloat = 414;
    public static let longDimensionInDesignTime: CGFloat = 896;

    /// Normally same as "UIScreen.main.bounds.size" variable (i.e. screen size).
    /// But for Computers, we limit to a portrait-size (to look same as Mobile).
    public static var maxSize: CGSize {
        #if targetEnvironment(macCatalyst)
        return .init(
            width: Self.shortDimensionInDesignTime,
            height: Self.longDimensionInDesignTime);
        #else
        return UIScreen.main.bounds.size;
        #endif
    }

    public static var shortDimension: CGFloat {
        let screenSize: CGSize = Self.maxSize;
        return screenSize.width < screenSize.height
            ? screenSize.width : screenSize.height;
    }

    public static var longDimension: CGFloat {
        let screenSize: CGSize = Self.maxSize;
        return screenSize.height > screenSize.width
              ? screenSize.height : screenSize.width;
    }

    /// Does same as "init()" would, but for Storyboard.
    override public func awakeFromNib() {
        super.awakeFromNib()
        // Backup real constant (and continue if it's valid).
        self.actualConstant = self.constant
        guard isValid else { return }
        // Listens to screen rotate.
        NotificationCenter.default.addObserver(self,
            selector: #selector(onLayoutChange),
            name: UIDevice.orientationDidChangeNotification,
            object: nil)
        // Apply for current screen size.
        self.onLayoutChange();
    }

    deinit {
        guard isValid else { return }
        NotificationCenter.default.removeObserver(self)
    }

    /// Recalculates constant.
    @objc public func onLayoutChange() {
        guard isValid else { return }
        self.constant = Self.resize(self.actualConstant)
    }

    /// Converts constant from DesignTime to fit current max view-size.
    public static func resize(_ constant: CGFloat) -> CGFloat {
        return constant / Self.shortDimensionInDesignTime * Self.shortDimension;
    }
}

How to achieve percent based constant in Storyboard?

Below solution's downside is need to run App before seeing result (which even that may not be required in future Xcode versions).

Usage:

  1. Add below PercentConstraint class's code to your project.
  2. Select your constraint in Storyboard editor.
  3. In Identity Inspector, use PercentConstraint as class.
  4. In Attribute Inspector, set Percent of Screen field.
  5. Optionally, Run App to see result.
import Foundation

/// Percent based Layout-Constraint.
/// Later at run-time, replaces constant with percentage of screen.
///
/// WARNING: supports only Storyboard (used as class in Identity Inspector).
///
class PercentConstraint: NSLayoutConstraint {

    @IBInspectable public var percentOfScreen: CGFloat = 0

    var isValid: Bool { percentOfScreen > 0 }
    var screenSize: CGSize { UIScreen.main.bounds.size }

    override func awakeFromNib() {
        super.awakeFromNib()

        guard isValid else { return }
        NotificationCenter.default.addObserver(self,
            selector: #selector(onLayoutChange),
            name: UIDevice.orientationDidChangeNotification,
            object: nil)
        self.onLayoutChange();
    }

    deinit {
        guard isValid else { return }
        NotificationCenter.default.removeObserver(self)
    }

    /// Recalculates constant.
    @objc func onLayoutChange() {
        guard isValid else { return }

        switch self.firstAttribute {
        case .height, .top, .topMargin, .bottom, .bottomMargin,
             .centerY, .centerYWithinMargins,
             .firstBaseline, .lastBaseline:
            self.constant = screenSize.height * percentOfScreen / 100.0;
        case .width, .leading, .leadingMargin, .trailing, .trailingMargin,
             .left, .leftMargin, .right, .rightMargin,
             .centerX, .centerXWithinMargins:
            self.constant = screenSize.width * percentOfScreen / 100.0;
        default:
            break;
        }
    }
}

Note that the constant is overwritten at runtime, but you may want to set it anyway (to get an idea how it looks like before running).

Willner answered 6/9, 2021 at 22:3 Comment(0)
G
0

This can be done automatically when using Storyboards (you might have to change a setting or two here and there). When you create your GUI, you can toggle between the screen sizes (3.5 and 4 inch) to make sure that it will look good on both. See the answer to this question.

You could also see this tutorial. That should give you an idea on how to work with the GUI layouts.

Gneiss answered 11/8, 2013 at 18:10 Comment(2)
This answer is not specific enough to be useful, for this question, IMHO. In particular, the requested "40%/60%" ratio is NOT easily done using Storyboards. Nor do the two links in the answer shed light on how to accomplish that (AFAIK, based on a brief look).Moye
Hm, true @ToolmakerSteve. I don't know what I was thinking when I posted this... 4 years ago.Gneiss

© 2022 - 2024 — McMap. All rights reserved.