Let's say I have a UIView or UIViewController I want to use in many places throughout the app.
While I don't know how or where it'll ultimately be displayed, I know I always want it to have half the width of its parent, and be pinned to the right.
If I wanted to achieve something like this in HTML/CSS, I would apply the following style:
.floatRight{
position: relative;
width: 50%;
float: right;
}
<div class="floatRight"> foobar </div>
I've tried using NSLayoutConstraints, but as far as I can tell I can only specify constraints relative to other views, which means I have to know where my view will be placed ahead of time.
Is there any way in iOS that I can specify "this view should be this tall and always be as far right as possible" without knowing the parent or surrounding views ahead of time?
While I don't know how or where it'll ultimately be displayed, I know I always want it to have half the width of its parent, and be pinned to the right.
You have to wait until the view has a parent, but that's not difficult. Use a custom UIView subclass. In that subclass, override didMoveToSuperview, and in your override, create and activate the constraints. As you do that, refer to the view's superview as self.superview!; that's all your code needs to know.
Related
There seem to be several different options/terms and people in the iOS community use with respect to layout (e.g. UIEdgeInsets is a type, but sometimes I hear/read "set the insets" or layout margins vs layout guides).
I've always been able to find an option that works. But I'm never sure that I'm using the right tool for the job.
Can someone help provide some clarity between these different aspects of layout and when to use each in the best way?
See this UIKit: Apps for Every Size and Shape before and after reading this. Might also want to see the notes from here as well.
Being the Bounty offerer...I'd say the majority of my confusion came from not properly understanding the UILayoutGuide class. That is key, but also very simple.
Let me first introduce a problem:
In the old days, if you needed to constrain these circles like this:
Then you had to create clear UIViews and add them as your subviews and then add your constraints to them like below:
Today you don't need to add them as your subviews. You could instead just use
Layout Guides
To create a layout guide, you must perform the following steps:
Instantiate a new layout guide.
Add the layout guide to a view by calling the view’s addLayoutGuide(_:) method.
Define the position and size of the layout guide using Auto Layout.
You can use these guides to define the space between elements in your layout. The following example shows layout guides used to define an equal spacing between a series of views.
steps:
let space1 = UILayoutGuide()
view.addLayoutGuide(space1)
let space2 = UILayoutGuide()
view.addLayoutGuide(space2)
space1.widthAnchor.constraintEqualToAnchor(space2.widthAnchor).active = true
saveButton.trailingAnchor.constraintEqualToAnchor(space1.leadingAnchor).active = true
cancelButton.leadingAnchor.constraintEqualToAnchor(space1.trailingAnchor).active = true
cancelButton.trailingAnchor.constraintEqualToAnchor(space2.leadingAnchor).active = true
clearButton.leadingAnchor.constraintEqualToAnchor(space2.trailingAnchor).active = true
Layout guides can also act as a black box, containing a number of other views and controls. This lets you encapsulate part of your view, breaking your layout into modular chunks.
Three interesting notes:
If you are using the 'view debug hierarchy' then you would be seeing more instances of UILayoutGuide
Just like a UIView, a UILayoutGuide instance has all kinds of anchors
As for why not just create dummy UIViews and going through creating UILayoutGuides: "There are a number of costs associated with adding dummy views to your view hierarchy. First, there is the cost of creating and maintaining the view itself. Second, the dummy view is a full member of the view hierarchy, which means that it adds overhead to every task the hierarchy performs. Worst of all, the invisible dummy view can intercept messages that are intended for other views, causing problems that are very difficult to find."
For more see documentation.
topLayoutGuide vs. safeAreaLayoutGuide
topLayoutGuide (deprecated)
It's deprecated for but for learning purposes: A UIViewController has 2 dummy boxes. 1 property at the top named topLayoutGuide and another property at the bottom named bottomLayoutGuide. The viewController itself doesn't have any guides for its left/leading or right/trailing sides. Both of these are an instance of UILayoutGuide
if constrained to view.topAnchor ie:
tableView.topAnchor.constraint(equalTo: view.topAnchor)
tableView doesn't start from the bottom of the navigationBar. Notice the orange behind the navigationBar...
However if you constrained it to topLayoutGuide.bottomAnchor ie:
tableView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor)
then tableView starts from the bottom of the navigationBar
And depending on your layout design you might want your content to be blurred below the navigation bar.
And the idea was that you would display your content edge to edge. And
it would underlap the bars so that you could get these nice colorful
blurs with your content through the bars
For more see this moment from WWDC and this question here. I don't think the solutions are exactly related, just the image in the question.
safeAreaLayoutGuide
since iOS11
Apple has deprecated topLayoutGuide & bottomLayoutGuide. So
instead of having two dummy boxes, you now have one dummy box named
safeAreaLayoutGuide on the UIView instance. UIViewController no longer has any of this...
A visual comparison copied from useyourloaf:
side note: If you use storyboards then aligning your views to the topLayoutGuide or top of safeAreaLayoutGuide would render the same. If you don't use storyboards (do it programmatically) then you would have to dance between iOS11 and and LessThaniOS11 and have 2 different versions of code
For more on safeAreaLayoutGuide, I highly recommend that you set Apple's article on: Positioning Content Relative to the Safe Area
NOTE: safeAreaLayoutGuide is a UIView property. topLayoutGuide is a UIViewController property.
layoutMarginsGuide
UIView has only 1 dummy box. The property is named layoutMarginsGuide . But unlike UIViewController it doesn't sit at the top or bottom. It just sits at the center with 8points padding/inset (from all 4 sides) into the UIView.So where is this useful?: You would use this if you don't want your textView to be constrained to the edges of a UIView instance. This would improve the reading experience. Or instead of constraining a button to the leadingAnchor of its superview and making it look ugly, you add 8 points to the anchor...ie constraint the button to the leadingAnchor and then adding 8 constant points. The striked text, is actually where you would use readableContentGuide, layoutMarginsGuide is useful if for when you don't want your button or label anchored to the edge of its superview
someButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8)
But wait there is an easier way. Just use Apple's recommended margin ie use:
someButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)
Also see the example provided in
documentation.
A good Raywenderlich tutorial can be found
here
readableContentGuide
Is slightly different from layoutMarginGuide. Both are properties of UIView. Sometimes they are identical sometimes they aren't. It's purpose is:
This layout guide defines an area that can easily be read without
forcing users to move their head to track the lines
For more see this moment from WWDC: building Adaptive layout and this awesome useyourloaf tutorial.
On the iPhone 7 Plus in portrait, readable content guides are the same
as the view’s margin guides, but in landscape there is more white
space on either side of the text view. On the iPad in landscape, the
white space is increased significantly.
The margin size depends on the system’s dynamic type. The larger the
font, the wider the guide will be.
From RayWenderlich
In the image below the cyan is anchored to the layoutMarginGuide, but the green is anchored to the readableContentGuide:
UIEdgeInsets
If you want to change your layoutMarginsGuide ie change the desired margin from 8 points to 16 points then you must change the layoutMargins's value and then the layoutMarginsGuide's anchors would get automatically updated.
UIEdgeInsets is just the type of your layoutMargins. layoutMargins is a property name of the UIView class
someview.layoutMargins = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50)
The only place I found this code ☝️ to have its effect is inside viewDidLayoutSubviews.For more see here
Anchors
They're foundational but nothing special to it. They are the farthest edge of any UIView/UILayoutGuide. Both UIView and UILayoutGuide instances have it. EVERYTHING you constrain is eventually constrained using anchors, it's just a matter of to what entity's anchors you are anchoring it to. It could be a safeAreaLayoutGuide's anchor, it could be a layoutMarginGuide's anchor, it could be a topLayoutGuide's anchor it could be a view's anchor. (though you may also just anchor your heightAnchor to 50, so in that case there isn't another anchor)
A great visual comparison between layoutMarginsGuide and Anchorscan be found from this answer. It's done using storyboards so it makes it easier to understand.
contentInsets
While it's important to understand, is a totally different discussion and has nothing to do with the others. It's specific to UIScrollViews. For more see this great article
Conclusion
To be safe and sure everything is inside your view use safeAreaLayoutGuide. If you want to use the system provided margins to have better layout of views or have some of padding then, use layoutMarginGuide, if you want to make things more readable readableContentGuide.
The readableContentGuide's size is always smaller or equal to layoutMarginGuide.
The layoutMarginGuide's size is always smaller or equal to safeAreaLayoutGuide
layoutMargins is very similar to CSS's padding. safeAreaLayoutGuide is similar to CSS margins. I don't know if there is any CSS equivalent for readableContentGuide
Addendum: ContentInset vs. contentOffset
They are for scrollViews, and somewhat unrelated to the rest of the answer. As for what contentInset & contentOffset are, please see this moment from WWDC 2018: UIKit: Apps for Every Size and Shape
. The video is very simple. Also refer to Karthik's answer below. Having that said it's vital that you fully understand how a scrollView works and understand what contentSize is, otherwise it would be complicated. For more on contentSize and scrollView see Vacawama's answer here
I hope you will get the info from the following links/pictures.
You will be able to deduce the required information on layout parameters from below links.
alignment rects.
content inset and offset example 2
margins
Sorry if this is a boring answer, but I feel like the Apple Developer documentation is pretty good at describing how each UIView property is intended to be used.
As for what is the best way of implementing a layout when you have multiple methods/options that work... that's really a question of style/opinion. I'd focus on the following criteria to come to a decision:
Consider eliminating the options that are hardest to maintain/debug/understand
Imagine someone brand new joining your team or someone inheriting your code - which implementation would they have difficulty debugging and why?
Consider eliminating the options that make the layout more difficult to rearrange/expand/edit
Consider eliminating the options that are inconsistent with the rest of the app (this kind of goes back to the first point)
Consider eliminating the options that go against Apple's intent (see the docs/WWDC talks to get a sense of their intentions).
A team could work together through this criteria to come to agreement on "how we layout our UI" in each scenario. I guess what you're asking for is the sum-total product of such discussions: a layout-styleguide of sorts.
In my experience, this has always developed organically on the teams I've worked on, and things weren't really written down. It was a lot more of following point 3 above.
Some excerpts from the Apple documentation for classes that are described in the question:
On UILayoutGuide:
Use layout guides to replace the dummy views you may have created to represent inter-view spaces or encapsulation in your user interface. Traditionally, there were a number of Auto Layout techniques that required dummy views.
...
The UILayoutGuide class is designed to perform all the tasks previously performed by dummy views, but to do it in a safer, more efficient manner.
On layoutMargins:
In iOS 11 and later, use the directionalLayoutMargins property to specify layout margins instead of this property.
On directionalLayoutMargins:
Use this property to specify the desired amount of space (measured in points) between the edges of this view and its subviews. The leading and trailing margins are applied appropriately to the left or right margins based on the current layout direction.
On contentInset:
Use this property to extend the space between your content and the edges of the content view. The unit of size is points. The default value is UIEdgeInsetsZero.
On topAnchor:
Use this anchor to create constraints with the view’s top edge. You can combine this anchor only with other NSLayoutYAxisAnchor anchors. For more information, see NSLayoutAnchor.
On NSLayoutAnchor
Use these constraints to programatically define your layout using Auto Layout. Instead of creating NSLayoutConstraint objects directly, start with a UIView, NSView, or UILayoutGuide object you wish to constrain, and select one of that object’s anchor properties. These properties correspond to the main NSLayoutConstraint.Attribute values used in Auto Layout, and provide an appropriate NSLayoutAnchor subclass for creating constraints to that attribute. Use the anchor’s methods to construct your constraint.
I'm figuring out (again) how to set the margin for a custom UIView instance. From what I recall I had to set the AlignmentRect via the alignmentRectInsets method. But that did not worked with auto layout.
Searching on google I found that there is another property called layoutMargins.
So the question is what does layoutMargins and alignmentRect do ? Do they affect each other? Totally different things ?
layoutMargins determines how things inside of the view are positioned with auto layout. Usually this is used to keep objects a specific distance away from the edges of the view.
alignmentRectInsets is for telling objects outside of your custom view how they should align with it. For example, you might have a view with a wavy or angled top. Aligning other objects with the top of the view may not look quite right, so you might set an inset on the top alignment to compensate.
You probably care about the layoutMargins. I've never actually seen anyone use alignmentRectInsets.
What is the proper way to make a fluid layout in iOS, in the sense that hidden elements do not take up space anymore?
I have a table view with in each cell a customized detail-type of view with title, subtitle and a row with some extra information:
The extra information can be up to three pairs of an icon and a label with a value. The layout of all views inside the cell is done using AutoLayout with no missing or ambiguous constraints.
What I would like to achieve is that when the value is 0, the icon and the label are not displayed and the views on the right are shifted to the left.
If I just use the setHidden: method, the width of the hidden parts are not changed, so that there is just whitespace, but no views are moved. Example:
It should look like this:
The following questions are related but do not seem to fit my case:
Fluid UI layout on iPhone
AutoLayout with hidden UIViews?
I have tried to follow the approach with creating layout constraints for the four frames that need to be set to zero: the width of the heart-shaped icon, the width of the label containing the value, the whitespace in between those and the whitespace between the label and the next icon. This did not work because I could not bind the layout constraints to the outlet in the code, and besides it seems a cumbersome method for something that should be a common scenario.
EDIT: I fixed the problem with the outlets to constraints: to do this it is necessary to create a subclass for the table cell and creating outlets for the constraints there.
With "common scenario" I refer to doing something similar in web design, where setting the display style to none is simple and has the desired effect. I expect that there is something similarly simple for this in iOS.
I have been thinking of using a collection view with reusable cells, but then I need to set up a delegate and a datasource and everything, and before I would go this way I wanted to make sure that that is the way to do it.
There is no need to remove a hidden view. Connect the constraint to an outlet in the code, and when you determine a view is hidden, subtract from the constraint's constant. Then, in the cell's prepareForReuse, remember to return the constraint's constant to the correct value.
Hidden views maintain their frame, so auto layout will have no reason to adjust the view. The correct way to do this would be to remove the views from the superview. The last thing you must do is double check the constraints. Since you will be removing views, you cannot use those views for auto layout. This will require quite a bit of constraint setting on your UI.
I have been struggling with IB and auto layout and figure there must be an easier way to create a simple scrollable form with multiple fields on it. The width can adjust to the device screen width (or in the case of the iPad to the details view of a split view.
The basic layout is simple a label and underneath a field (in some cases the field must have a minimum height) repeated for each attribute.
I think I must be doing something completely stupid or IB's autolayout is hopeless, I suspect the former. In any event whatever I do in IB fields just adopt whatever constraints they feel like it seems. Even when I set a constrain it seem IB just replaces it sometimes.
Currently I am using a view with a fixed height and width that fits the screen and laying out fields on this form, however its a complete nightmare having to pin every field in almost every direction. And then IB complains about a million constraints that it can't satisfy.
Hopefully someone has a better way of doing this they are willing to share - perhaps it might be best just to write some code to set up the constraints rather than relying on IB ?
Thanks
You are right UIScrollView and autolayout is not clear at first look.
You have to understand that when you are adding layout constraints for a view inside scroll view, the superview is content view of scroll (not the view of the UIScrollView) that can be more or less then current view of the ScrollView in IB.
There is one thing that is really making me crazy about auto layout , I'm doing my tests and found that if you subclass a UIView and you put some views with their constraints is impossible to know the computed value.
Let't say that we have a TestViewClass that inherits from UIView, we've got some subviews inside. we want to use it for iPhone and iPad, so we use in two sizes, let's suppose 100x100 and 200x200 we made our constraints to make everything work. We build this view placing those subviews in some positions.
Now we need to build another subviews that contains a number of buttons as subviews (contentView) which number is given at runtime.
The algorithm will pick the size of this content view and put the correct number of buttons calculating the space between them in a way that the will be at the same distance from each other but covering the whole content view width. For example, the contentView is 200 point in width, buttons are 3 and squared with a side of 60. Together they cover 180, so we have 20 points left that should be placed as a space between the buttons->10 points.
That's pretty easy, made thousand of times before auto layout. To do that I need the width of the contentView which has some constraints to its superview that make it resize its width according to superview size, the problem is that I can't find any place inside UIView implementation where I can get the final value of the size of the contentView.
I tried view -layoutSubviews, -updateConstraints, -didMoveToSuperview, value show are alway the original one. When the new frames are calculated?
I clearly didn't get something about auto layout...
I discovered this problem trying to set table view cell height, trying to make them appear all on screen independently by the size of the table view. here is the related question other question
Finally I've get what I was missing about auto layout and it is a fundamental concept.
Autolayout doesn't work like autoresizing masks, the new frames aren't
updated instantaneously but by request.
This makes a huge difference, it seems to be in sync with the CATransaction render cycle, that makes sense, because layout calculation could be an expensive task and is useless to do until you really need it, most of the time during rendering.
Of course there are few exceptions, like this one and how can we force auto layout to make this calculation? we can do setting -setNeedsLayout method and -layoutIfNeeded. the first method marks the view as "dirty" and the second forces an immediate layout. If you set call those two methods in -didMoveToSuperview right after you get correct updated frames. Hope this helps,
Andrea
You can use view controller's viewDidLayoutSubviews, which will notify you when the views are laid out. If all of this is adding of constraints is happening in a custom UIView, you could simply write a method for your view called viewDidLayoutSubviews and have the view controller call that when the view controller receives it. At this point, the containers should have their dimensions properly configured.
But, there are a couple of approaches of even spacing a bunch of buttons without needing to bother knowing the size of the container view:
One approach I've used for even spacing controls in a container is to create what I call "spacer" views (views that are present, but just have a clearColor background, so you can't visually see them). You can then create a series of constraints using, effectively, something like:
H:|[spacer1][button1(60)][spacer2(==spacer1)][button2(60)][spacer3(==spacer1)][button3(60)][spacer4(==spacer1)]|
If your number of buttons is fixed, you can do this in a single constraintsWithVisualFormat, like above. If it's a variable number of buttons, you can iterate through them, building a VFL string for each pair of button and spacer (e.g. the first button would use VFL of
H:|[spacer][button(60)]
all the subsequent ones would create another button and another spacer and use the following VFL:
H:[previousButton][spacer(==originalSpacer)][button(60)]
and then I add one final spacer at the end:
H:[lastButton][spacer(==originalSpacer)]|
This is a little cumbersome, but as you can see, your controls are perfectly laid out, evenly spaced, and you never needed to know the dimensions of the container.
Another approach is to use the NSLayoutFormatAlignAllCenterX, attribute. This bypasses the need to create all of those spacers, but if you use a simple algorithm like I do below, the centers will be evenly distributed amongst themselves. Thus, in addition to the standard vertical constraints and the horizontal width constraint for the button, you can then control the horizontal placement of the buttons with:
[containerView addConstraint:[NSLayoutConstraint constraintWithItem:button[i]
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeCenterX
multiplier:2.0 * (double) (i + 0.5) / (double) n
constant:0.0]];
where i is the zero-based index of which button you're setting this horizontal constraint for, and n is the number of buttons.