自动布局 `setFrame:` UIView 的子视图

3
我有一个容器 UIView。它的子视图位置是通过自动布局来管理的。然而,我想要明确地定位容器本身。
该容器存在于我称之为 VideoPreviewViewUIView 子类中。该容器的名称为 contentOverlayView
以下是我的代码,用于向容器添加一个明显的红色子视图,该子视图填充容器,除了边缘的 1 像素外。
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)]];

layoutSubviews中设置框架

我最初保持容器视图位置的方法是简单地覆盖VideoPreviewViewlayoutSubviews并在那里明确设置容器的框架,如下所示:

-(void) layoutSubviews
{
  // Other stuff snipped out.
  const CGRect videoBounds = [self videoBounds];
  [_contentOverlayView setFrame: videoBounds];
  [super layoutSubviews];
}

但是这样并不起作用。如果我在videoView上使用recursiveDescription,我会得到:

<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>>

这个框架的宽度刚好符合我要求的边距,但位置完全不对,而且框架也不够大。

我在容器视图上使用了_autoLayoutTrace来尝试解决这个问题,但是被告知布局是有歧义的:

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}

这启示我,如果我正在使用自动布局,那么框架将被忽略,因此我还需要使用自动布局设置容器的框架。

添加大小约束

其他一些帖子、博客等也建议采取这种方法。因此,我添加了一些约束来控制容器的宽度和高度:

_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]];

我更新了我的layoutSubviews方法来调整这些约束的“常量”:
-(void) layoutSubviews
{
  const CGRect videoBounds = [self videoBounds];
  [_contentOverlayView setFrame: videoBounds];
  [_contentOverlayWidthConstraint setConstant: CGRectGetWidth(videoBounds)];
  [_contentOverlayHeightConstraint setConstant: CGRectGetHeight(videoBounds)];

  [super layoutSubviews];
}

这样做效果稍微好一些了——红色视图现在是可见的并且具有正确的尺寸,但它仍然处于错误的位置。我仍然从跟踪中得到歧义:

   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}

...所以,宽度和高度的歧义已经消除了,但位置的歧义仍然存在。

添加位置约束

因此,我添加了另外两个布局约束来设置容器的位置:

_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]];

更新了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];
}

崩溃

但是当我尝试添加新的约束时,我遇到了一个异常,错误信息如下:

* 由于未捕获到的异常 'NSInvalidArgumentException' 而终止应用程序,原因是:*+ [NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]:无法设置将位置设置为常量的约束。位置属性必须成对指定。

我完全不理解这个错误。我看不出我该如何纠正它。

你能帮忙吗?

2个回答

7
问题出在这些行及其相似之处:
[NSLayoutConstraint constraintWithItem: contentOverlayView
                             attribute: NSLayoutAttributeLeft
                             relatedBy: NSLayoutRelationEqual
                                toItem: nil
                             attribute: NSLayoutAttributeNotAnAttribute
                            multiplier: 1.0
                              constant: 0];

您不能将Left / Top设置为nil项。您必须将其设置为另一个视图的某个属性(通常是该项的superview的左侧/顶部,或者您想要将其与之对齐的东西)。

只有宽度和高度可以是绝对值。即使如此,它们也不能为0(或者至少不应该),因为在这里您有0; 那是没有意义的。


你真的想让这个东西的宽度和高度为1吗?那几乎看不见了。 - matt
实际上,你所做的非常明智、简单,并且完全符合自动布局的精神。你可能认为它不简单,但事实上你正在与系统协调工作。约束可能看起来冗长,但它们只是样板文件,具有很强的描述能力。在布局发生之前或在layoutSubviews中更改约束是完全合法的;有一个WWDC视频演示了这种技术。 - matt
1
你可能想阅读我讨论的这一部分:http://www.apeth.com/iOSBook/ch14.html#_autolayout 再加上这个:http://www.apeth.com/iOSBook/ch14.html#_order_of_layout_events 我认为你会发现它非常令人放心。但请忽略中间的部分(关于nib中的约束),因为它已经过时了。 - matt
抱歉 - 我在这里使用了与原问题不同的名称!使用原始名称,视图层次结构为 videoView -> contentOverlayView -> redViewvideoViewUIView 子类 VideoPreviewView 的一个实例。它在其 init 过程中创建了 contentOverlayView。它有约束条件(如讨论所述),将 contentOverlayView 约束到它呈现的视频端口的顶部。redView 是我的调试工具,用于测试约束条件。它与 contentOverlayView 一起添加到其他地方,并在其中添加了它与 contentOverlayView 之间的约束条件,就像问题中所述。 - Benjohn
让我们在聊天中继续这个讨论。点击此处进入聊天室 - matt
显示剩余7条评论

2

Matt在这里提供了一个很好的答案。我想从我的实现中添加一些我发现有用的点。

避免初始约束违规

正如Matt指出的那样,我创建我的约束,以便在视图的init期间宽度和高度为0。我这样做是因为当我创建约束时,我没有任何合理的想法关于宽度或高度。不幸的是,如果我将这些约束添加到视图中,它们很可能会导致约束违规。

为了避免这种情况,init不会将约束添加到视图中。我等到第一次调用updateConstraints时再添加。在这里,我知道宽度和高度的正确值,所以我设置它们。然后,如果我还没有这样做,我就将约束添加到视图上。代码如下:

-(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];
}

约束条件的快捷创建

我觉得使用NSLayoutConstraint方法来创建单个的约束条件很长,也不够清晰明了。因此我添加了一个类别(category)以使这个过程更容易,并提供一些具体约束类型的符号描述。如果有需要,我会继续为该类别添加更多方法。目前,它包含以下内容:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接