Autolayout in `setFrame:` UIView subplots
I have a container UIView
. His childish posts are run auto-start. However, I want to put the container itself explicitly .
The container exists in the subclass UIView
that I am calling VideoPreviewView
. The container has a name contentOverlayView
.
Here's my code to add an explicit red child to the container that fills it except for the 1 pixel edge around the edges:
UIView *const redView = [UIView new];
[redView setTranslatesAutoresizingMaskIntoConstraints: NO];
[redView setBackgroundColor: [UIColor redColor]];
[[videoView contentOverlayView] addSubview: redView];
[[videoView contentOverlayView] addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:@"H:|-(1@900)-[redView]-(1@900)-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(redView)]];
[[videoView contentOverlayView] addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:@"V:|-(1@900)-[redView]-(1@900)-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(redView)]];
Setting the frame in layoutSubviews
My original approach to keeping the container view in it was to simply override layoutSubviews
for VideoPreviewView
and explicitly set the container's border there, like this:
-(void) layoutSubviews
{
// Other stuff snipped out.
const CGRect videoBounds = [self videoBounds];
[_contentOverlayView setFrame: videoBounds];
[super layoutSubviews];
}
... But it didn't work. If I use recursiveDescription
on videoView
, I get:
<VideoPreviewView: 0x146a54f0; frame = (0 0; 320 460); layer = <CALayer: 0x146a5640>>
| <UIView: 0x145b6610; frame = (0 0; 2 2); layer = <CALayer: 0x145b6680>>
| | <UIView: 0x145bff60; frame = (1 1; 0 0); layer = <CALayer: 0x145bffd0>>
It has enough width to respect the margins I need, but the position is completely wrong and the frame is not as big as I want.
I used _autoLayoutTrace
in container view to try and resolve this and was told that the layout is ambiguous:
UIWindow:0x145a3a50
| UIView:0x145a6e60
| | HPBezelView:0x1468d580
| | | UIImageView:0x145aab30
| | *VideoPreviewView:0x146a54f0
| | | *UIView:0x145b6610- AMBIGUOUS LAYOUT for UIView:0x145b6610.minX{id: 5}, UIView:0x145b6610.minY{id: 13}, UIView:0x145b6610.Width{id: 8}, UIView:0x145b6610.Height{id: 16}
| | | | *UIView:0x145bff60- AMBIGUOUS LAYOUT for UIView:0x145bff60.minX{id: 4}, UIView:0x145bff60.minY{id: 12}, UIView:0x145bff60.Width{id: 9}, UIView:0x145bff60.Height{id: 17}
This hinted to me that if I use autolayout the frame is ignored, so I need to set the container's frame using autoplay as well.
Adding size constraints
Several other posts and blogs offer this course of action as well. So I added some constraints to control the width and height of the container:
_contentOverlayWidthConstraint =
[NSLayoutConstraint constraintWithItem: contentOverlayView
attribute: NSLayoutAttributeWidth
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 1];
_contentOverlayHeightConstraint =
[NSLayoutConstraint constraintWithItem: contentOverlayView
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 1];
[contentOverlayView addConstraints: @[_contentOverlayWidthConstraint, _contentOverlayHeightConstraint]];
I've updated my method layoutSubviews
to adjust the "constants" on these constraints:
-(void) layoutSubviews
{
const CGRect videoBounds = [self videoBounds];
[_contentOverlayView setFrame: videoBounds];
[_contentOverlayWidthConstraint setConstant: CGRectGetWidth(videoBounds)];
[_contentOverlayHeightConstraint setConstant: CGRectGetHeight(videoBounds)];
[super layoutSubviews];
}
This works a little better - the red view is now visible and the correct size, but it is still in the wrong place. I am still getting ambiguities from the trace:
UIWindow:0x17d76a00
| UIView:0x17d7dfd0
| | HPBezelView:0x17d7d7c0
| | | UIImageView:0x17d82240
| | *VideoPreviewView:0x17d925e0
| | | UIView:0x17d96790
| | | *UIView:0x17d96830- AMBIGUOUS LAYOUT for UIView:0x17d96830.minX{id: 9}, UIView:0x17d96830.minY{id: 16}
| | | | *UIView:0x17d9ae90- AMBIGUOUS LAYOUT for UIView:0x17d9ae90.minX{id: 8}, UIView:0x17d9ae90.minY{id: 15}
... so the ambiguities of width and height are gone, but ambiguities of position remain.
Adding position constraints
So, I added two more layout constraints to set the position of the container:
_contentOverlayLeftConstraint =
[NSLayoutConstraint constraintWithItem: contentOverlayView
attribute: NSLayoutAttributeLeft
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 0];
_contentOverlayTopConstraint =
[NSLayoutConstraint constraintWithItem: contentOverlayView
attribute: NSLayoutAttributeTop
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 0];
[contentOverlayView addConstraints: @[_contentOverlayLeftConstraint, _contentOverlayTopConstraint]];
And updated layoutSubviews
:
-(void) layoutSubviews
{
[[self contentView] setFrame: [self bounds]];
[[self videoPreviewLayer] setFrame: [self bounds]];
const CGRect videoBounds = [self videoBounds];
[_contentOverlayWidthConstraint setConstant: CGRectGetWidth(videoBounds)];
[_contentOverlayHeightConstraint setConstant: CGRectGetHeight(videoBounds)];
[_contentOverlayLeftConstraint setConstant: CGRectGetMinX(videoBounds)];
[_contentOverlayTopConstraint setConstant: CGRectGetMinY(videoBounds)];
[super layoutSubviews];
}
Crash
But when I try to add new constraints, I get an error exception:
* Application terminated due to uncaught exception 'NSInvalidArgumentException', reason: '* + [Restriction NSLayoutConstraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:]: Cannot make a constraint that sets a location equal to a constant. Location attributes must be specified in pairs
I don't get that at all. I can't see how I can fix this.
You can help?
source to share
The problem is that these lines and similar:
[NSLayoutConstraint constraintWithItem: contentOverlayView
attribute: NSLayoutAttributeLeft
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 0];
You cannot set Left / Top equal to element zero. You have to set it to some attribute of another view (it will usually be the left / top of that watch element or whatever you want to align).
Only width and height can be absolute. And even they can't be 0 (or at least they shouldn't be) like you have here; this makes no sense.
source to share
Matt has provided a great answer here. I would like to add a few more points from my implementation that I found useful.
Preventing Initial Constraint Violations
As Matt points out, I create my constraints such that the view has 0 width and height at the time of init
presentation. I do this because when I create constraints I have no reasonable idea of the width or height. Unfortunately, if I add these constraints to the view, they can cause a constraint violation.
To avoid this, init
add no restrictions to the view. I wait until the first time is called updateConstraints
. Here I know the correct values for the width and height, so I set them. Then I add constraints on the views if I haven't already. It looks like this:
-(void) updateConstraints
{
const CGRect videoBounds = [self videoBounds];
[_contentOverlayWidthConstraint setConstant: CGRectGetWidth(videoBounds)];
[_contentOverlayHeightConstraint setConstant: CGRectGetHeight(videoBounds)];
[_contentOverlayLeftConstraint setConstant: CGRectGetMinX(videoBounds)];
[_contentOverlayTopConstraint setConstant: CGRectGetMinY(videoBounds)];
if(!_constraintsAreAdded)
{
[_contentOverlayView addConstraints: @[_contentOverlayWidthConstraint, _contentOverlayHeightConstraint]];
[self addConstraints: @[_contentOverlayLeftConstraint, _contentOverlayTopConstraint]];
_constraintsAreAdded = YES;
}
[super updateConstraints];
}
shorcuts create constraints
I find the method NSLayoutConstraint
for creating individual constraints rather lengthy and obscure. I've added a category to make this easier and to provide symbolic descriptions of the specific types of constraint being created. I'll add more methods to it as they seem useful, but here's what I have so far:
UIView + Constraints.h
@interface UIView (Constraints)
-(NSLayoutConstraint*) widthConstraintWithConstant: (CGFloat) initialWidth;
-(NSLayoutConstraint*) heightConstraintWithConstant: (CGFloat) initialHeight;
-(NSLayoutConstraint*) leftOffsetFromView: (UIView*) view withConstant: (CGFloat) offset;
-(NSLayoutConstraint*) topOffsetFromView: (UIView*) view withConstant: (CGFloat) offset;
@end
UIView + Constraints.m
@implementation UIView (Constraints)
-(NSLayoutConstraint*) widthConstraintWithConstant: (CGFloat) initial
{
return [NSLayoutConstraint
constraintWithItem: self
attribute: NSLayoutAttributeWidth
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: initial];
}
-(NSLayoutConstraint*) heightConstraintWithConstant: (CGFloat) initial
{
return [NSLayoutConstraint
constraintWithItem: self
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: initial];
}
-(NSLayoutConstraint*) leftOffsetFromView: (UIView*) view withConstant: (CGFloat) offset
{
return [NSLayoutConstraint
constraintWithItem: self
attribute: NSLayoutAttributeLeft
relatedBy: NSLayoutRelationEqual
toItem: view
attribute: NSLayoutAttributeLeft
multiplier: 1.0
constant: offset];
}
-(NSLayoutConstraint*) topOffsetFromView: (UIView*) view withConstant: (CGFloat) offset
{
return [NSLayoutConstraint
constraintWithItem: self
attribute: NSLayoutAttributeTop
relatedBy: NSLayoutRelationEqual
toItem: view
attribute: NSLayoutAttributeTop
multiplier: 1.0
constant: offset];
}
@end
source to share