Autolayout top left based on super view ratio năm 2024
I have an Autolayout challenge for you: I have a Square view which contains some images, labels, etc. We need that view to be full width on all iPhone screen sizes & resolutions. Show The view is built at 320 x 320 (or @1x) and it is expected to scale proportionally for each and every other resolution and screen size. Basically, the view and all its elements should scale together, in unison, as if it was an image. Thanks, Octavio, for a great question/challenge! My first reaction was: “this is a problem I had solved previously!” But to make it a bit more interesting this time I am doing it with auto layout and Swift. We might learn something in the process. When building the iCatalog framework – which was long before auto layout – we had the same problem with interactive zones on catalog pages where we would not know the final resolution. If you position a view in a superview, normally you have to specify origin and size relative to the superview’s coordinate system. The solution then was to save origin and size in percent of the superview’s coordinates. This way you could easily multiply the percentage with the final width/height of the superview to arrive at the runtime coordinates. Proportional LayoutThe basic formula for any auto layout constraint is: [view1] [attribute1] IS [relation] TO [view2] [attribute2] MULTIPLIED BY [multiplicator] PLUS [constant]. To model the same behavior as outlined above, for each view we need 4 constraints:
Each of the four values is always a percentage of the width or height of the superview. For example if the superview is {0,0, 100,100} and the view is {10,20,30,40} then the percentages would be {10%, 20%, 30%, 40%}. Interview builder doesn’t have the ability to let us specify positions as a multiple of another view’s attribute. You can only specify it in points. This means we have to do this setup in code. We’ll want to retain the ability to design the view in Interface Builder, but we need some code that will calculate the percentages and add the corresponding constraints to the view at runtime. Enumerating SubviewsWe need to apply the constraints to all subviews of a given view to make them scale proportionally to this outermost view. For that we need a method that will enumerate all views. Wouldn’t it be nice if UIView had a method to do that where we could have a block be executed for each of the subviews in the entire tree? Let’s add it. extension UIView { func enumerateSubviews(block: (view: UIView) -> ()) { }
}This short function contains a few interesting lessons on Swift:
The innermost code recurses the function for the subview’s subviews and then calls the block. This will allow us to add the size and origin constraints to each of the subviews. So let’s get to the code for those now. Removing Prototyping ConstraintsDo you remember when Interface Builder would require you to always add all constraints? Later Xcode became smarter and would happily accept any IB layout. The difference came from IB tacitly adding as many constraints as would be required to nail down all positions and sizes. Those prototyping constraints would clash with our proportional constraints, so we need to get rid of them – and only them. extension UIView { func removePrototypingConstraints() { }
}We are again using the as! [Type] pattern to make sure we get a typed array of NSLayoutConstraint objects so that removeConstraint won’t complain. We convert the dynamicType to a string and if this class name has a prefix of NSIBPrototyping we remove it from the view. Creating Size Constraints in SwiftLet’s first tackle the constraints for the size. As you will see its quite straightforward. The biggest annoyance for me when creating it was that Xcode does not have the ability to colon-align parameters. extension UIView { func addProportionalSizeConstraints() { }
}The first line explicitly unwraps the view’s superview. This can – of course – be nil, which is why Apple made it an optional value. But since it does not make sense to call this method with no superview set we can leave it as that. We actually want this to throw an exception if we ever call this method while superview is still nil. This is our replacement for NSAssert which we would have used in Objective-C. We calculate percentages for width and height based on the superview’s size and then add two constraints which set the view’s width and height as corresponding fractions. Next we’ll take care of the view’s origin. Constraining Origin in SwiftAutolayout refuses to constraint Left or LeftMargin to be a fraction of another view’s Width. The workaround I found was to instead use the Right value of the superview. The other challenge is that Left cannot be ZERO times Right. In that case – if the percent value is 0 – we instead pin Left to the superview’s Left. The same is true for the vertical dimension. extension UIView { func addProportionalOriginConstraints() { // constrain left as percent of superview
if (percent_x > 0)
{ }
else
{ }
// constrain top as percent of superview
if (percent_y > 0 )
{ }
else
{ }
}Update: In my first published sample I didn’t yet disable the translation of autoresizing masks. But later I found that apparently some views – when created by IB – have autoresizing masks set which interferes with the proportional layout. Next, let’s tie these methods together. A Simple TestThe sample project – which you can find in my GitHub Swift Examples repo – has a view subviews put together in interface builder. The root view has a simulated size of 600 x 600 and all subviews are laid out relative to that. The effect we want to achieve is that all subviews should size and position proportionally if we rotate the view between portrait and landscape. I couldn’t find an easy way to get the root view’s size after UIViewController had loaded it from the NIB. It always comes out at the correct resolution for the device, e.g. {0, 0, 375, 667} for iPhone 6. This would destroy our reference frame, so we need to restore it before enumerating through the subviews adding our proportional constraints. The next auto layout pass will restore the correct root view frame and then also our proportional constraints will adjust the frames of all subviews. class ViewController: UIViewController { override func viewDidLoad() { }
}Note the syntax of the block we are passing into our subview enumeration function. In Objective-C we would specify the parameter list outside of the curly braces, like ^(UIView *view). In Swift the parameters are inside the closure and so Apple needed a way to separate the parameter header from the block code. The “in” keyword serves this function. Inside the block we first remove any left over prototyping constraints, then we add the layout constraints for origin and size. Build and run and you will see: All squares we had in interface builder now get proportionally resized based on the dimensions of the outermost view. The one rectangle at the upper left side also resizes as expected. Next we add an extra square view in IB so that we always have a square. Layout SquaredWe want the view to be a square and always be toughing the outside of the screen. This is what they call “aspect fit” if the touching sides are the ones that are closer together, or “aspect fill” if its touching the sides which are wider apart. The tricky part here is that you need to work with two different priority levels. If auto layout cannot fulfil a lesser-priority constraint then it will ignore it. Mike Wolemer from AtomicObject wrote the article that explained this this me. The Required priority constraints are:
The constraints with a lesser priority are:
Required constraints have a priority level of 1000, for the lesser priority you can use any lower value. The only constraint on the square centering view is the one for the aspect ratio. All others are attached to the root view. To add the constraints referencing the root view you CMD+Click on the root view and then the centering view. Click on the add constraints button and choose Equal Widths and Equal Heights. For the greater-than constraints you do the same, but change the relationship in the constraint inspector. Make sure that you got the order of First Item and Second Item correct. If Superview is the first item, then the relation needs to be Greater Then or Equal. Finally we need an outlet in ViewController.swift so that we can access the squareView. Then we change the enumeration to be only this view’s subviews. Et voilá! Bonus: Automatic Font-AdjustingThere remains one item that we didn’t touch upon: the font size in UILabels. The presented approach only adjusts the frames of views, but not their contents. For a UIImageView this is no problem if you set the View Mode to “Scale to Fill”. But the font size of a label would not automatically adjust to changes in the label’s frame. There is no facility in auto layout which would allow us to tie the font size together with any length. But we are free to create a UILabel subclass that does exactly that. Our FontAdjustingLabel stores its initial size and font when it awakes from NIB. This enables it to calculate an adjusted font size whenever its bounds change. class FontAdjustingLabel: UILabel { var initialSize : CGSize! var initialFont : UIFont! override func awakeFromNib() { }
override func layoutSubviews()
{ }
}I needed to add the ! at the end of the instance variable definitions so that the compiler won’t complain that there is no init method which initializes it. Contrary to Objective-C, Swift does not automatically nil all ivars. Adding a ! or ? makes it an optional value and those do get initialized with nil. Note the use of a font descriptor. This preserves all font attributes except the point size which we calculated based on the view’s current height relative to the initial height. ConclusionBy answering Octavio’s question, we learned a few things about auto layout and how to set up constraints in Swift code. We created an extension to UIView, so that we could call these methods on any subclass of UIView. We found that you cannot set up proportional constraints in Interface Builder. And the opposite is true for square layouts: first we thought that “aspect fit” wouldn’t be possible in Interface Builder, but it actually is, using constraints of different priority levels. Finally, adjusting the font size is not possible with auto layout. But we can subclass UILabel so that it adjusts its font automatically. This way we have the auto layout system work together with manual adjustments in code to achieve the desired effect. |