Emulating aspect-fit behaviour using AutoLayout constraints in Xcode 6 - ios

I want to use AutoLayout to size and layout a view in a manner that is reminiscent of UIImageView's aspect-fit content mode.
I have a subview inside a container view in Interface Builder. The subview has some inherent aspect ratio which I wish to respect. The container view's size is unknown until runtime.
If the container view's aspect ratio is wider than the subview, then I want the subview's height to equal the parent view's height.
If the container view's aspect ratio is taller than the subview, then I want the subview's width to equal the parent view's width.
In either case I wish the subview to be centered horizontally and vertically within the container view.
Is there a way to achieve this using AutoLayout constraints in Xcode 6 or in previous version? Ideally using Interface Builder, but if not perhaps it is possible to define such constraints programmatically.

You're not describing scale-to-fit; you're describing aspect-fit. (I have edited your question in this regard.) The subview becomes as large as possible while maintaining its aspect ratio and fitting entirely inside its parent.
Anyway, you can do this with auto layout. You can do it entirely in IB as of Xcode 5.1. Let's start with some views:
The light green view has an aspect ratio of 4:1. The dark green view has an aspect ratio of 1:4. I'm going to set up constraints so that the blue view fills the top half of the screen, the pink view fills the bottom half of the screen, and each green view expands as much as possible while maintaining its aspect ratio and fitting in its container.
First, I'll create constraints on all four sides of the blue view. I'll pin it to its nearest neighbor on each edge, with a distance of 0. I make sure to turn off margins:
Note that I don't update the frame yet. I find it easier to leave room between the views when setting up constraints, and just set the constants to 0 (or whatever) by hand.
Next, I pin the left, bottom, and right edges of the pink view to its nearest neighbor. I don't need to set up a top edge constraint because its top edge is already constrained to the bottom edge of the blue view.
I also need an equal-heights constraint between the pink and blue views. This will make them each fill half the screen:
If I tell Xcode to update all the frames now, I get this:
So the constraints I've set up so far are correct. I undo that and start work on the light green view.
Aspect-fitting the light green view requires five constraints:
A required-priority aspect ratio constraint on the light green view. You can create this constraint in a xib or storyboard with Xcode 5.1 or later.
A required-priority constraint limiting the width of the light green view to be less than or equal to the width of its container.
A high-priority constraint setting the width of the light green view to be equal to the width of its container.
A required-priority constraint limiting the height of the light green view to be less than or equal to the height of its container.
A high-priority constraint setting the height of the light green view to be equal to the height of its container.
Let's consider the two width constraints. The less-than-or-equal constraint, by itself, is not sufficient to determine the width of the light green view; many widths will fit the constraint. Since there's ambiguity, autolayout will try to choose a solution that minimizes the error in the other (high-priority but not required) constraint. Minimizing the error means making the width as close as possible to the container's width, while not violating the required less-than-or-equal constraint.
The same thing happens with the height constraint. And since the aspect-ratio constraint is also required, it can only maximize the size of the subview along one axis (unless the container happens to have the same aspect ratio as the subview).
So first I create the aspect ratio constraint:
Then I create equal width and height constraints with the container:
I need to edit these constraints to be less-than-or-equal constraints:
Next I need to create another set of equal width and height constraints with the container:
And I need to make these new constraints less than required priority:
Finally, you asked for the subview to be centered in its container, so I'll set up those constraints:
Now, to test, I'll select the view controller and ask Xcode to update all the frames. This is what I get:
Oops! The subview has expanded to completely fill its container. If I select it, I can see that in fact it's maintained its aspect ratio, but it's doing an aspect-fill instead of an aspect-fit.
The problem is that on a less-than-or-equal constraint, it matters which view is at each end of the constraint, and Xcode has set up the constraint opposite from my expectation. I could select each of the two constraints and reverse its first and second items. Instead, I'll just select the subview and change the constraints to be greater-than-or-equal:
Xcode updates the layout:
Now I do all the same things to the dark green view on the bottom. I need to make sure its aspect ratio is 1:4 (Xcode resized it in a weird way since it didn't have constraints). I won't show the steps again since they're the same. Here's the result:
Now I can run it in the iPhone 4S simulator, which has a different screen size than IB used, and test rotation:
And I can test in in the iPhone 6 simulator:
I've uploaded my final storyboard to this gist for your convenience.

Rob, your answer is awesome!
I also know that this question is specifically about achieving this by using auto-layout. However, just as a reference, I'd like to show how this can be done in code. You set up the top and bottom views (blue and pink) just like Rob showed. Then you create a custom AspectFitView:
AspectFitView.h:
#import <UIKit/UIKit.h>
#interface AspectFitView : UIView
#property (nonatomic, strong) UIView *childView;
#end
AspectFitView.m:
#import "AspectFitView.h"
#implementation AspectFitView
- (void)setChildView:(UIView *)childView
{
if (_childView) {
[_childView removeFromSuperview];
}
_childView = childView;
[self addSubview:childView];
[self setNeedsLayout];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (_childView) {
CGSize childSize = _childView.frame.size;
CGSize parentSize = self.frame.size;
CGFloat aspectRatioForHeight = childSize.width / childSize.height;
CGFloat aspectRatioForWidth = childSize.height / childSize.width;
if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
// whole height, adjust width
CGFloat width = parentSize.width * aspectRatioForWidth;
_childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
} else {
// whole width, adjust height
CGFloat height = parentSize.height * aspectRatioForHeight;
_childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
}
}
}
#end
Next, you change the class of the blue and pink views in the storyboard to be AspectFitViews. Finally you set two outlets to your viewcontroller topAspectFitView and bottomAspectFitView and set their childViews in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
top.backgroundColor = [UIColor lightGrayColor];
UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
bottom.backgroundColor = [UIColor greenColor];
_topAspectFitView.childView = top;
_bottomAspectFitView.childView = bottom;
}
So it's not hard to do this in code and it is still very adaptable and works with variably-sized views and different aspect ratios.
Update July 2015: Find a demo app here: https://github.com/jfahrenkrug/SPWKAspectFitView

This is for macOS.
I have problem to use Rob's way to achieve aspect fit on the OS X application. But I made it with another way -- Instead of using width and height, I used leading, trailing, top and bottom space.
Basically, add two leading spaces where one is >= 0 #1000 required priority and another one is = 0 #250 low priority. Do same settings to trailing, top and bottom space.
Of course, you need to set aspect ratio and centre X and centre Y.
And then job's done!

I needed a solution from the accepted answer, but executed from the code. The most elegant way I've found is using Masonry framework.
#import "Masonry.h"
...
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
make.size.lessThanOrEqualTo(superview);
make.size.equalTo(superview).with.priorityHigh();
make.center.equalTo(superview);
}];

I found myself wanting aspect-fill behavior so that a UIImageView would maintain its own aspect ratio and entirely fill the container view. Confusingly, my UIImageView was breaking BOTH high-priority equal-width and equal-height constraints (described in Rob's answer) and rendering at full resolution.
The solution was simply to set the UIImageView's Content Compression Resistance Priority lower than the priority of the equal-width and equal-height constraints:

This is a port of #rob_mayoff's excellent answer to a code-centric approach, using NSLayoutAnchor objects and ported to Xamarin. For me, NSLayoutAnchor and related classes have made AutoLayout much easier to program:
public class ContentView : UIView
{
public ContentView (UIColor fillColor)
{
BackgroundColor = fillColor;
}
}
public class MyController : UIViewController
{
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
//Starting point:
var view = new ContentView (UIColor.White);
blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
view.AddSubview (blueView);
lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
lightGreenView.Frame = new CGRect (20, 40, 200, 60);
view.AddSubview (lightGreenView);
pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
view.AddSubview (pinkView);
greenView = new ContentView (UIColor.Green);
greenView.Frame = new CGRect (80, 20, 40, 200);
pinkView.AddSubview (greenView);
//Now start doing in code the things that #rob_mayoff did in IB
//Make the blue view size up to its parent, but half the height
blueView.TranslatesAutoresizingMaskIntoConstraints = false;
var blueConstraints = new []
{
blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
};
NSLayoutConstraint.ActivateConstraints (blueConstraints);
//Make the pink view same size as blue view, and linked to bottom of blue view
pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
var pinkConstraints = new []
{
pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
};
NSLayoutConstraint.ActivateConstraints (pinkConstraints);
//From here, address the aspect-fitting challenge:
lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
//These are the must-fulfill constraints:
var lightGreenConstraints = new []
{
//Aspect ratio of 1 : 5
NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
//Cannot be larger than parent's width or height
lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
//Center in parent
lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
};
//Must-fulfill
foreach (var c in lightGreenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);
//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var lightGreenLowPriorityConstraints = new []
{
lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
};
//Lower priority
foreach (var c in lightGreenLowPriorityConstraints)
{
c.Priority = 750;
}
NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);
//Aspect-fit on the green view now
greenView.TranslatesAutoresizingMaskIntoConstraints = false;
var greenConstraints = new []
{
//Aspect ratio of 5:1
NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
//Cannot be larger than parent's width or height
greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
//Center in parent
greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
};
//Must fulfill
foreach (var c in greenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (greenConstraints);
//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var greenLowPriorityConstraints = new []
{
greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
};
//Lower-priority than above
foreach (var c in greenLowPriorityConstraints)
{
c.Priority = 750;
}
NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);
this.View = view;
view.LayoutIfNeeded ();
}
}

Maybe this is the shortest answer, with Masonry, which also supports aspect-fill and stretch.
typedef NS_ENUM(NSInteger, ContentMode) {
ContentMode_aspectFit,
ContentMode_aspectFill,
ContentMode_stretch
}
// ....
[containerView addSubview:subview];
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
if (contentMode == ContentMode_stretch) {
make.edges.equalTo(containerView);
}
else {
make.center.equalTo(containerView);
make.edges.equalTo(containerView).priorityHigh();
make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
if (contentMode == ContentMode_aspectFit) {
make.width.height.lessThanOrEqualTo(containerView);
}
else { // contentMode == ContentMode_aspectFill
make.width.height.greaterThanOrEqualTo(containerView);
}
}
}];

I couldn't find any ready-to-go fully programatical solution, so here is my take in swift 5 for the aspect fill extension to a view:
extension UIView {
public enum FillingMode {
case full(padding:Int = 0)
case aspectFit(ratio:CGFloat)
// case aspectFill ...
}
public func addSubview(_ newView:UIView, withFillingMode fillingMode:FillingMode) {
newView.translatesAutoresizingMaskIntoConstraints = false
addSubview(newView)
switch fillingMode {
case let .full(padding):
let cgPadding = CGFloat(padding)
NSLayoutConstraint.activate([
newView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cgPadding),
newView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cgPadding),
newView.topAnchor.constraint(equalTo: topAnchor, constant: cgPadding),
newView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -cgPadding)
])
case let .aspectFit(ratio):
guard ratio != 0 else { return }
NSLayoutConstraint.activate([
newView.centerXAnchor.constraint(equalTo: centerXAnchor),
newView.centerYAnchor.constraint(equalTo: centerYAnchor),
newView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
newView.leadingAnchor.constraint(equalTo: leadingAnchor).usingPriority(900),
newView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
newView.trailingAnchor.constraint(equalTo: trailingAnchor).usingPriority(900),
newView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
newView.topAnchor.constraint(equalTo: topAnchor).usingPriority(900),
newView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
newView.bottomAnchor.constraint(equalTo: bottomAnchor).usingPriority(900),
newView.heightAnchor.constraint(equalTo: newView.widthAnchor, multiplier: CGFloat(ratio)),
])
}
}
}
And here is the priority extension (comes from another thread but I "secured it" with some pattern matching between 1...1000:
extension NSLayoutConstraint {
/// Returns the constraint sender with the passed priority.
///
/// - Parameter priority: The priority to be set.
/// - Returns: The sended constraint adjusted with the new priority.
func usingPriority(_ priority: Int) -> NSLayoutConstraint {
self.priority = UILayoutPriority( (1...1000 ~= priority) ? Float(priority) : 1000 )
return self
}
}
Hope it helps ~

Related

Self-sizing cells and dynamic size controls for iOS

Problem definition
I am trying to build a custom control which will behave similarly to UILabel. I should be able to place such a control inside of a self-sizing table cell and it should:
Wrap it's content (like UILabel with numberOfLines=0 does)
automatically extend self-sized cell height size
handle a device rotation
don't require any special code in UITableCellView or ViewControll to implement this functionality (UILabel doesn't require any special code for that).
Research
The first thing which I did is very simple. I decided to observe how UILabel works. I did following:
created a table with self-sizing cells
created a custom cell, put UILabel (with numberOfLines=0) in it
created constraints to make sure that UILabel occupies a whole cell
subclasses UILabel and overrode a bunch of methods to see how it behaves
I checked following things
Run it in a portrait (the label is displayed correctly over several lines) and the cell height is correct
Rotate it. The table width and height was updated and they are correct too.
I observed that it behaves well. It doesn't require any special code and I saw the order of (some) calls which system does to render it.
A partial solution
#Wingzero wrote a partial solution below. It creates cells of a correct size.
However, his solution has two problems:
It uses "self.superview.bounds.size.width". This could be used if your control occupies a whole cell. However, if you have anything else in the cell which uses constraints then such code won't calculate a width correctly.
It doesn't handle rotation at all. I am pretty sure it doesn't handle other resizing events (there are bunch of less common resizing events - like a statusbar getting bigger on a phone call etc).
Do you know how to solve these problems for this case?
I found a bunch of articles which talks about building more static custom controls and using pre-built controls in self-sizing cells.
However, I haven't found anything which put together a solution to handle both of these.
I have to use the answer section to post my ideas and moving forward, though it may not be your answer, since I am not fully understand what's blocking you, because I think you already know the intrinsic size and that's it.
based on the comments, I tried to create a view with a text property and override the intrinsic:
header file, later I found maxPreferredWidth is not used totally, so ignore it:
#interface LabelView : UIView
IB_DESIGNABLE
#property (nonatomic, copy) IBInspectable NSString *text;
#property (nonatomic, assign) IBInspectable CGFloat maxPreferredWidth;
#end
.m file:
#import "LabelView.h"
#implementation LabelView
-(void)setText:(NSString *)text {
if (![_text isEqualToString:text]) {
_text = text;
[self invalidateIntrinsicContentSize];
}
}
-(CGSize)intrinsicContentSize {
CGRect boundingRect = [self.text boundingRectWithSize:CGSizeMake(self.superview.bounds.size.width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:16]}
context:nil];
return boundingRect.size;
}
#end
and a UITableViewCell with xib:
header file:
#interface LabelCell : UITableViewCell
#property (weak, nonatomic) IBOutlet LabelView *labelView;
#end
.m file:
#implementation LabelCell
- (void)awakeFromNib {
[super awakeFromNib];
}
#end
xib, it's simple, just top, bottom, leading, trailing constraints:
So running it, based on the text's bounding rect, the cell's height is different, in my case, I have two text to loop: 1. "haha", 2. "asdf"{repeat many times to create a long string}
so the odd cell is 19 height and even cell is 58 height:
Is this what are you looking for?
My ideas:
the UITableView's cell's width is always the same as the table view, so that's the width. UICollectionView may be more issues there, but the point is we will calculate it and just return it is enough.
Demo project: https://github.com/liuxuan30/StackOverflow-DynamicSize
(I changed based on my old project, which has some images, ignore those.)
Here's a solution that meets your requirements and is also IBDesignable so it previews live in Interface Builder. This class will lay out a series of squares (the total number is equal to the IBInspectable count property). By default, it will just lay them all out in one long line. But if you set the wrap IBInspectable property to On, it will wrap the squares and increase its height based on its constrained width (like a UILabel with numberOfLines == 0). In a self-sizing table view cell, this will have the effect of pushing out the top and bottom to accommodate the wrapped intrinsic size of the custom view.
The code:
import Foundation
import UIKit
#IBDesignable class WrappingView : UIView {
private class InnerWrappingView : UIView {
private var lastPoint:CGPoint = CGPointZero
private var wrap = false
private var count:Int = 100
private var size:Int = 8
private var spacing:Int = 3
private func calculatedSize() -> CGSize {
lastPoint = CGPoint(x:-(size + spacing), y: 0)
for _ in 0..<count {
var nextPoint:CGPoint!
if wrap {
nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
} else {
nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
}
lastPoint = nextPoint
}
return CGSize(width: wrap ? bounds.width : lastPoint.x + CGFloat(size), height: lastPoint.y + CGFloat(size))
}
override func layoutSubviews() {
super.layoutSubviews()
guard bounds.size != calculatedSize() || subviews.count == 0 else {
return
}
for subview in subviews {
subview.removeFromSuperview()
}
lastPoint = CGPoint(x:-(size + spacing), y: 0)
for _ in 0..<count {
let square = createSquareView()
var nextPoint:CGPoint!
if wrap {
nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
} else {
nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
}
square.frame = CGRect(origin: nextPoint, size: square.bounds.size)
addSubview(square)
lastPoint = nextPoint
}
let newframe = CGRect(origin: frame.origin, size: calculatedSize())
frame = newframe
invalidateIntrinsicContentSize()
setNeedsLayout()
}
private func createSquareView() -> UIView {
let square = UIView(frame: CGRect(x: 0, y: 0, width: size, height: size))
square.backgroundColor = UIColor.blueColor()
return square
}
override func intrinsicContentSize() -> CGSize {
return calculatedSize()
}
}
#IBInspectable var count:Int = 500 {
didSet {
innerView.count = count
layoutSubviews()
}
}
#IBInspectable var size:Int = 8 {
didSet {
innerView.size = size
layoutSubviews()
}
}
#IBInspectable var spacing:Int = 3 {
didSet {
innerView.spacing = spacing
layoutSubviews()
}
}
#IBInspectable var wrap:Bool = false {
didSet {
innerView.wrap = wrap
layoutSubviews()
}
}
private var _innerView:InnerWrappingView! {
didSet {
clipsToBounds = true
addSubview(_innerView)
_innerView.clipsToBounds = true
_innerView.frame = bounds
_innerView.wrap = wrap
_innerView.translatesAutoresizingMaskIntoConstraints = false
_innerView.leftAnchor.constraintEqualToAnchor(leftAnchor).active = true
_innerView.rightAnchor.constraintEqualToAnchor(rightAnchor).active = true
_innerView.topAnchor.constraintEqualToAnchor(topAnchor).active = true
_innerView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true
_innerView.setContentCompressionResistancePriority(750, forAxis: .Vertical)
_innerView.setContentHuggingPriority(251, forAxis: .Vertical)
}
}
private var innerView:InnerWrappingView! {
if _innerView == nil {
_innerView = InnerWrappingView()
}
return _innerView
}
override func layoutSubviews() {
super.layoutSubviews()
if innerView.bounds.width != bounds.width {
innerView.frame = CGRect(origin: CGPointZero, size: CGSize(width: bounds.width, height: 0))
}
innerView.layoutSubviews()
if innerView.bounds.height != bounds.height {
invalidateIntrinsicContentSize()
superview?.layoutIfNeeded()
}
}
override func intrinsicContentSize() -> CGSize {
return innerView.calculatedSize()
}
}
In my sample application, I set the table view to dequeue a cell containing this custom view for each row, and set the count property of the custom view to 20 * the indexPath's row. The custom view is constrained to 50% of the cell's width, so its width will change automatically when moving between landscape and portrait. Because each successive table cell wraps a longer and longer string of squares, each cell is automatically sized to be taller and taller.
When running, it looks like this (includes demonstration of rotation):
To build on the other answer from #Wingzero, layout is a complex problem...
The maxPreferredWidth mentioned is important, and relates to preferredMaxLayoutWidth of UILabel. The point of this attribute is to tell a label not to just be one long line and to instead prefer to wrap if the width gets to that value. So when calculating the intrinsic size you would use the minimum of the preferredMaxLayoutWidth (if set) or the view width as the max width.
Another key aspect is invalidateIntrinsicContentSize which the view should call on itself whenever something changes and means a new layout is required.
UILabel doesn't handle rotation - it doesn't know about it. It's the responsibility of the view controller to detect and handle rotation, generally by invalidating the layout and updating the view size before triggering a new layout run. The labels (and other views) are just there to handle the resulting layout. As part of the rotation you (i.e. a view controller) may change the preferredMaxLayoutWidth as it makes sense to allow more width in landscape layout for example.
Are you looking for something like this ^^? The cell has dynamic heights to facilitate the content of the UILabel, and there's no code to calculate size/width/height whatsoever - just some constraints.
Essentially, the label at left-hand side has top, bottom and leading margin to the cell, and trailing margin to the right-hand side label, which has trailing margin to the cell. Just need one label? Ignore the right hand side label then, and configure the left hand side label with a trailing constraint to the cell.
And if you need the label to be multi-line, configure that. Set the numberOfLines to 2, 3, or 0, up to you.
You don't need to calculate table view cell's height, the auto layout will calculate for you; but you need to let it know that, by telling it to use auto dimension: self.tableView.rowHeight = UITableViewAutomaticDimension, or return it in tableView:heightForRowAtIndexPath:. And you can also tell table view a "rough" estimation in tableView:estimatedHeightForRowAtIndexPath: for a better performance.
Still not working? Set the Content Compression Resistance Priority - Vertical to 1000 / Required for the UILabel in question, so that the label's content will try its best to "resist the compression", and the numberOfLines configuration will be fully acknowledged.
And it rotates? Try to observe the rotation (there're orientation change notifications) and then update layout (setNeedsLayout).
Still not working? More reads here: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

Scale an image with AutoLayout

So I want to have an image that displays on the bottom half of my screen and then animate it to a smaller scale and have it attached to the top/right. Sorry for the crude images, but this is basically what I want:
I thought I could do this by constraining the top, right, and aspect ratio of my view then just animate the change of my aspect ratio constraint. Unfortunately, when I do this IB screams that I don't have a proper X value for my view, and when I animate the scale I just end up with my view shrunk vertically but still full width.
I hope this makes sense and someone can help me figure out how to make this happen.
Ok so I figured it out. Lets see if I can explain it clearly for others.
Position 1 Constraints:
Left = 0
Right = 0
Bottom = 0
Top = 150
Position 2 Constraints:
Top = 40
Aspect Ratio Height to SuperView 0.25 (I use PureLayout so
not sure what the generic style constraint would look like)
Aspect
Ratio Width to SuperView 0.25
To do my animation:
- (void)animateAgent
{
self.agentTop.constant = 40.0;
[NSLayoutConstraint deactivateConstraints:#[ self.agentBottom, self.agentLeft ]];
self.agentHeightRatio = [self.agentContainer autoMatchDimension:ALDimensionHeight toDimension:ALDimensionHeight ofView:self.animationContainer withMultiplier:0.25];
self.agentWidthRatio = [self.agentContainer autoMatchDimension:ALDimensionWidth toDimension:ALDimensionHeight ofView:self.animationContainer withMultiplier:0.25];
[self.view setNeedsLayout];
[UIView animateWithDuration:0.3 animations:^{
[self.view layoutIfNeeded];
}];
}

Horizontally center multiple UIViews

I want to horizontally center a number of UIViews (they happen to be circles) in the master UIView. It will end up basically looking like the dots on the standard Page Control.
I have all the code written to create the circle UIViews I just have no idea how to arrange them horizontally and dynamically at run time.
Essentially I need some kind of horizontal container where I can do this
-(void)addCircle{
[self addSubView:[CircleView init]];
}
And it will auto arrange however many children it has in the center.
I get confused with auto-layout as well from time to time but here is a way how you can do it programmatically: (I assume that you add your circle views to a containerView property of your view controller and you do not add any other views to it.)
Add these two properties to your view controller:
#property (nonatomic) CGRect circleViewFrame;
#property (nonatomic) CGFloat delta;
Initiate those properties with the desired values in your view controller's viewDidLoad method:
// the size (frame) of your circle views
self.circleViewFrame = CGRectMake(0, 0, 10, 10);
// the horizontal distance between your circle views
self.delta = 10.0;
Now we add your "automatic addCircle method":
- (void)addCircleView {
UIView *newCircleView = [self createCircleView];
[self.containerView addSubview:newCircleView];
[self alignCircleViews];
}
Of course we need to implement the createCircleView method...
- (UIView*)createCircleView {
// Create your circle view here - I use a simple square view as an example
UIView *circleView = [[UIView alloc] initWithFrame:self.circleViewFrame];
// Set the backgroundColor to some solid color so you can see the view :)
circleView.backgroundColor = [UIColor redColor];
return circleView;
}
... and the alignCircleViews method:
- (void)alignCircleViews {
int numberOfSubviews = [self.containerView.subviews count];
CGFloat totalWidth = (numberOfSubviews * self.circleViewFrame.size.width) + (numberOfSubviews - 1) * self.delta;
CGFloat x = (self.containerView.frame.size.width / 2) - (totalWidth / 2);
for (int i = 0; i < numberOfSubviews; i++) {
UIView *circleView = self.containerView.subviews[i];
circleView.frame = CGRectMake(x,
self.circleViewFrame.origin.y,
self.circleViewFrame.size.width,
self.circleViewFrame.size.height);
x += self.circleViewFrame.size.width + self.delta;
}
}
This is the most important method which will automatically realign all your subviews each time a new circleView is added. The result will look like this:
Simple steps: append circle to container view, resize container view, center align container view
-(void)addToContanerView:(CircleView*)circle{
circle.rect.frame = CGrectMake(containers_end,container_y,no_change,no_change);
[containerView addSubview:circle];
[containerView sizeToFit];
containerView.center = self.view.center;
}
Assumptions:
containers_end & containers_y you can get from CGRectMax function,
for UIView SizeToFit method check here
To take care of rotation use make sure your Autoresizing subviews are set for left, right bottom and top margin.
You can try using this library. I have used it on several of my projects and so far, it worked really well.
https://github.com/davamale/DMHorizontalView

When moving a UILabel vertically with a UIPanGestureRecognizer, how do I stop them from going too far in either direction?

I'm trying to make the user be able to move the UILabel up and down across the view by attaching a UIPanGestureRecognizer to the UILabel and subsequently altering the constant of a constraint from the UILabel to the top of its view. So basically if the gesture recognizer detects them moving down 12pts, move the constant of the constraint 12pts to move the UILabel.
However, I want them to be stopped from moving further when they hit a certain vertical point (both too high or too low). I could just check the translation of the pan gesture, but my UILabel can be any number of lines, so if it's five lines instead of one, obviously it can't be panned down quite as far, so I can't rely on the translation of the pan gesture, I have to take into account the size of the label.
So I started monitoring its frame, and it works well, but in my implementation there's an annoying result where if they pan completely to the bottom limit, they have to pan really far back up before the UILabel "catches up" and comes with it (though no such problem exists when they hit the top boundary). Basically, they pan down to the bottom limit, and when they pan back up (this is all in the same gesture) it "sticks" momentarily until they pan far enough up, then it jumps up with their finger.
Here's the code I'm using to accomplish this:
- (void)textLabelPanned:(UIPanGestureRecognizer *)panGestureRecognizer {
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
_textDistanceFromTopBeforeMove = self.textToReadLabelPositionFromTopConstraint.constant;
}
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
NSNumber *textDistanceFromTop = #(self.textToReadLabelPositionFromTopConstraint.constant);
[[NSUserDefaults standardUserDefaults] setObject:textDistanceFromTop forKey:#"TextDistanceFromTop"];
}
else {
if (CGRectGetMinY(self.textToReadLabel.frame) >= [UIScreen mainScreen].bounds.origin.y + CLOSEST_TEXT_DISTANCE_TO_TOP && CGRectGetMaxY(self.textToReadLabel.frame) <= [UIScreen mainScreen].bounds.size.height - CLOSEST_TEXT_DISTANCE_TO_BOTTOM) {
self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
}
else if ([panGestureRecognizer translationInView:self.mainView].y > 0) {
if (CGRectGetMaxY(self.textToReadLabel.frame) + _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y < [UIScreen mainScreen].bounds.size.height - CLOSEST_TEXT_DISTANCE_TO_BOTTOM) {
self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
}
}
else if ([panGestureRecognizer translationInView:self.mainView].y < 0) {
if (CGRectGetMinY(self.textToReadLabel.frame) + _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y > [UIScreen mainScreen].bounds.origin.y + CLOSEST_TEXT_DISTANCE_TO_TOP) {
self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
}
}
// If one of the options views are present and the user pans really low, hide the options as to allow the user to see where they're panning
if (_inSpeedChangingMode) {
if (CGRectGetMaxY(self.textToReadLabel.frame) > CGRectGetMinY(self.progressBar.frame) - 10) {
[self showWordOptions:nil];
}
}
else if (_inTextChangingMode) {
if (CGRectGetMaxY(self.textToReadLabel.frame) > CGRectGetMinY(self.progressBar.frame) - 10) {
[self showTextOptions:nil];
}
}
}
}
What exactly am I doing wrong that would be causing it to "stick"? And is there perhaps a better way to be doing this?
You can accomplish this entirely with constraints, defined either in Interface Builder, or in code. The trick is to define constraints that prevent the label from moving out of bounds that have higher priority than the constraints that set the desired position.
In my test project I set up a view hierarchy entirely in a storyboard having a 1) view controller view 2) "container view" which defines the bounds 3) multi-line UILabel. There are 6 constraints acting on the label from its container:
4 'space to' constraints (leading, trailing, top, bottom) prevent the label from ever being positioned outside the bounds of its parent container. The priority on these is set to the default '1000' value in Interface Builder. The relation for these constraints is '>=', and the constant value is '0'.
2 'space to' constraints (leading, top) drive the label's actual position. The priority on these is set lower; I chose '500'. These constraints have outlets in the view controller so they can be adjusted in code. The relation for these constraints is '=', and the initial value is whatever you want to position the label.
The label itself has a width constraint to force it to display with multiple lines.
Here's what this looks like in IB:
The selected constraint has a lower priority and is used to drive the x position of the label. This constraint is tied to an ivar in the view controller so it can be adjusted at runtime.
The selected constraint has a higher priority and is used to corral the label within its parent view.
And here is the code in the view controller:
#interface TSViewController ()
#end
#implementation TSViewController
{
IBOutlet NSLayoutConstraint* _xLayoutConstraint;
IBOutlet NSLayoutConstraint* _yLayoutConstraint;
}
- (IBAction) pan: (UIGestureRecognizer*) pgr
{
CGPoint p = [pgr locationInView: self.view];
p.x -= pgr.view.frame.size.width / 2.0;
p.y -= pgr.view.frame.size.height / 2.0;
_xLayoutConstraint.constant = p.x;
_yLayoutConstraint.constant = p.y;
}
#end
The UIPanGestureRecognizer is associated with the UILabel and has its callback set to the pan: method in the view controller.
If your app has minimum SDK iOS7, you can use UIKit Dynamics instead of those UIGestureRecognizers. Your problem could be easily solved with a UICollisionBehavoir combined with an UIAttachmentBehavior
You might want to look into it. Here's the apple sample project on UIKit Dynamics:
https://developer.apple.com/library/IOS/samplecode/DynamicsCatalog/Introduction/Intro.html
Play around with it and you'll be amazed what you can do with so little code.
WWDC 2013 sessions:
- Getting Started with UIKit Dynamics
- Advanced Techniques with UIKit Dynamics

iOS: Programatically align button x pixels from bottom of screen

I'm creating compatibility for iOS6 for an app made by someone else. I'm used to using making buttons/UI elements with autoresizing masks but I don't really know how they work when you're creating the button programatically.
For example:
- (UIButton*) createSampleButton {
UIButton* b = createSampleViewButton(CGRectMake(67, 270, 191, 45),
#"btn_shuffle",
#"btn_shuffle_active",
self,
#selector(sampleAction));
b.autoresizingMask = UIViewAutoresizingFlexibleTopMargin;
[self attachButton:b];
return b;
}
How can I change these buttons such that they'll be placed according to some scale/margin instead of arbitrarily choosing points until everything "looks right" ?
I was thinking of something like:
- (UIButton*) createSampleButton {
CGFloat height = self.bounds.size.height;
CGFloat bottomBound = 80;
UIButton* b = createSampleViewButton(CGRectMake(67, height-bottomBound, 191, 45),
#"btn_shuffle",
#"btn_shuffle_active",
self,
#selector(sampleAction));
[self attachButton:b];
return b;
}
This would guarantee me that the button is placed 80 points from the bottom of the screen every time right? Is there a more graceful or purposeful way of doing this?
The masks are the same as when created in IB or code. The thing you want to make sure to do in code though is make sure the frames are set properly proportioned once. In your case, yes you do want UIViewAutoResizingFlexibleTopMargin, and setting the correct y-value on the origin in terms of y = parentView.bounds.size.height - (x points as you described), is all you need to do.
EDIT:
According to your updated question, maybe this will help you. If the button has a constant size, set the frame to that size with CGPointZero as the origin when you create the button. If a UIView owns the button, then put this code in layoutSubviews. If a UIViewController owns the button, replace self.bounds with self.view.bounds and put this in view(Will/Did)LayoutSubviews (Assuming iOS5+).
// Aligning the button at it's current x value, current size, with its bottom border margin pizels from the bottom of the parent view.
CGFloat margin = 10;
CGRect buttonFrame = button.frame;
buttonFrame.origin.y = self.bounds.size.height - buttonFrame.size.height - margin;
button.frame = buttonFrame;
Also, define constant values at the top of the implementation file. Feel free to create convenience methods for readability (if you find this more readable and not doing too much on one line) such as
CGRect CGSetYInRect(CGFloat y, CGRect rect)
...
button.frame = CGSetYInRect(self.bounds.size.height - button.frame.size.height - margin, button.frame);
Use AutoResizing when appropriate to avoid explicit logic in layoutSubviews.
When you move to iOS 6 + only, use AutoLayout.

Resources