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?

+3


source to share


2 answers


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.

+7


source


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

      

+2


source







All Articles