【问题标题】:Autolayout the subviews of a `setFrame:` UIView自动布局`setFrame:` UIView 的子视图
【发布时间】:2014-11-17 01:50:25
【问题描述】:

我有一个容器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中设置框架

我最初将容器视图定位在其中的方法是简单地覆盖 layoutSubviewsVideoPreviewView 并在那里显式设置容器的框架,如下所示:

-(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:]: 不能做出这样的约束设置一个等于常数的位置。位置属性必须成对指定'

这个我完全不明白。我不知道如何纠正这个问题。

你能帮忙吗?

【问题讨论】:

    标签: ios uikit autolayout


    【解决方案1】:

    Matt 在这里提供了great answer。我想从我的实现中添加一些我认为有用的点。

    避免违反初始约束

    正如 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 创建单个约束的方法相当长且不清楚。我添加了一个类别以使这更容易,并提供正在创建的特定类型的约束的符号描述。我会添加更多方法,因为它们看起来很有用,但这是我目前所拥有的:

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

      问题是这些行和类似的:

      [NSLayoutConstraint constraintWithItem: contentOverlayView
                                   attribute: NSLayoutAttributeLeft
                                   relatedBy: NSLayoutRelationEqual
                                      toItem: nil
                                   attribute: NSLayoutAttributeNotAnAttribute
                                  multiplier: 1.0
                                    constant: 0];
      

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

      只有宽度和高度可以是绝对的。甚至它们也不能是 0(或者至少不应该是),就像你在这里看到的那样;这没有任何意义。

      【讨论】:

      • 你真的希望这个东西的宽度和高度为1吗?那将是几乎看不见的。
      • 嗨,马特,感谢您的回复。我只是在尝试,你是绝对正确的:它工作得很好。如果我_autoLayoutTrace 现在没有歧义。我最初将常量设置为零,因为我没有任何更合理的值——我还不知道它们。当在layoutSubviews 期间实际确定布局时,我会在调用 [super layoutSubviews] 之前将它们更新为合理的值以允许自动布局发生(我试图在问题中展示这一点——如果不清楚,请告诉我)。
      • 顺便说一句——有没有更简单的方法来实现这一点?我想知道是否可以以某种方式使用自动调整大小的蒙版,但由于我没有合理的容器初始帧,我不确定这是否有效。
      • 在您标记的那些初始 0 常量值上 - 我将红色正方形子项设置为 1 像素填充的优先级为 900(不是默认值 1000)。如果我把它拿出来,当我设置视图时,我会收到违反约束的警告,所以你又是对的 :-) 这有点烦人 - 能够设置一个 I-have-no-idea 会很好-what-this-should-be 初始值,基于我将在调用layoutSublayers 时立即更新它。
      • 你可能想阅读我讨论的这一部分:apeth.com/iOSBook/ch14.html#_autolayout 加上这个:apeth.com/iOSBook/ch14.html#_order_of_layout_events 我想你会觉得这很让人放心。 :) 但是忽略中间部分(关于笔尖中的约束),因为它已经过时了。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-22
      相关资源
      最近更新 更多