聊聊 Apple 自身的Auto Layout Constraints

1,961 阅读5分钟

「这是我参与11月更文挑战的1天,活动详情查看:2021最后一次更文挑战」。

像我们常用的Masonry和SnapKit 都是基于Apple 自身的Auto Layout就行封装的,不过好像Masonry没有使用到NSLayoutAnchor这个新类。

Apple Auto Layout Constraints是在 iOS 6 中引入的,它改变了我们对 UI 元素布局的看法。但是在当时它并最优雅或最漂亮的API,反而有点繁琐。这就是为什么会有很多优秀的自动布局库不断的涌现。但从那时起,Apple 围绕 Auto Layout 的 API 进行了改进并且优化了不少。

iOS 6时期的自动布局

当自动布局在 iOS 6 中首次出现时,可以通过三种方式在用户界面布局中创建约束:

1. Interface Builder,我们不会在本文中讨论它。

2. 视觉格式语言 (VFL)。这是一种使用类似 ASCII 艺术的视觉格式字符串创建约束的方法。它看起来像这样|-[find]-[findNext]-[findField(>=20)]-|

下面就是是VFL约束写法的一个简单的示例。

    UIView *view1 = [[UIView alloc]init];
    view1.backgroundColor = [UIColor yellowColor];
    view1.translatesAutoresizingMaskIntoConstraints = NO;
    [superView addSubview:view1];
    
    UIView *view2 = [[UIView alloc]init];
    view2.backgroundColor = [UIColor yellowColor];
    view2.translatesAutoresizingMaskIntoConstraints = NO;
    [superView addSubview:view2];
    
    UIView *view3 = [[UIView alloc]init];
    view3.backgroundColor = [UIColor yellowColor];
    view3.translatesAutoresizingMaskIntoConstraints = NO;
    [superView addSubview:view3];
    
    NSString *hVFL = @"H:|-space-[view1(==100)]-space1-[view2]-space-|";
    NSString *hVFL1 = @"H:|-space-[view3]-space-|";
    NSString *vVFL = @"V:|-space-[view1(==view3)]-space-[view3]-space-|";
    NSString *vVFL1 = @"V:|-space-[view2(==view3)]-space-[view3]-space-|";
    
    NSDictionary *metircs = @{@"space":@20,@"space1":@30};
    NSDictionary *views = NSDictionaryOfVariableBindings(view1,view2,view3);
    
    NSArray *hconstraint = [NSLayoutConstraint constraintsWithVisualFormat:hVFL options:NSLayoutFormatDirectionLeadingToTrailing metrics:metircs views:views];
    NSArray *hconstraint1 = [NSLayoutConstraint constraintsWithVisualFormat:hVFL1 options:NSLayoutFormatDirectionLeadingToTrailing metrics:metircs views:views];
    NSArray *vconstraint = [NSLayoutConstraint constraintsWithVisualFormat:vVFL options:NSLayoutFormatDirectionLeadingToTrailing metrics:metircs views:views];
    NSArray *vconstraint1 = [NSLayoutConstraint constraintsWithVisualFormat:vVFL1 options:NSLayoutFormatDirectionLeadingToTrailing metrics:metircs views:views];
    
    //添加约束
    [superView addConstraints:hconstraint];
    [superView addConstraints:hconstraint1];
    [superView addConstraints:vconstraint];
    [superView addConstraints:vconstraint1];

对于简单的布局,这可能看起来是创建布局的好方法,但对于复杂的布局,情况可能并非如此,但它缺乏对高度/宽度百分比和居中等许多布局属性的支持,而且对于我们来说,这样字符串的一个写法很容易出错,而且不好检验。

3.NSLayoutConstrint API

它的缺点也很明显太冗长。下面就是一个简单的示例。

let leadingConstraint = NSLayoutConstraint(
    item: contentView,
    attribute: .leading,
    relatedBy: .equal,
    toItem: view,
    attribute: .leading,
    multiplier: 1,
    constant: 20)

let bottomConstraint = NSLayoutConstraint(
    item: contentView,
    attribute: .bottom,
    relatedBy: .equal,
    toItem: view,
    attribute: .bottom,
    multiplier: 1,
    constant: -20)

let topConstraint = NSLayoutConstraint(
    item: contentView,
    attribute: .top,
    relatedBy: .equal,
    toItem: view,
    attribute: .top,
    multiplier: 1,
    constant: 20)

let trailingConstraint = NSLayoutConstraint(
    item: contentView,
    attribute: .trailing,
    relatedBy: .equal,
    toItem: view,
    attribute: .trailing,
    multiplier: 1,
    constant: -20)

view.addConstraints([
    leadingConstraint,
    trailingConstraint,
    topConstraint,
    bottomConstraint
])

iOS 8时期 的改进

在 iOS 8 中,Apple 向 NSLayoutConstraint 引入了活动状态的概念。只有在活动状态中约束影响布局的计算。您可以启用/禁用任何约束,而无需将其删除并将其重新添加到视图中。

Activate/Deactivate a collection of constraints

添加约束的旧方式仍然与活动状态兼容;传入的所有约束都addConstraints(_:)将标记为活动的。但是它再也不是推荐的做法。所有约束必须只涉及接收视图范围内的视图。具体来说,任何涉及的视图必须是接收视图本身,或者接收视图的子视图。对于有经验的开发人员来说,这可能不是问题。但可能会让新手混淆他们应该向哪个视图添加约束。如果您尝试向错误的超类()添加约束,您可能会收到此错误。

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Impossible to set up layout with view hierarchy unprepared for constraint.'

在 iOS 8 中,Apple 引入了一种新方法activate(_:)。该activate(_:)方法会自动将约束添加到正确的视图中。也有deactivate(_:)作为removeConstraint(_:)

NSLayoutConstraint.activate([    leadingConstraint,    trailingConstraint,    topConstraint,    bottomConstraint])

NSLayoutConstraint.deactivate([    leadingConstraint,    trailingConstraint,    topConstraint,    bottomConstraint])

而且你还可以通过更改isActive属性来激活/停用单个约束。

let leadingConstraint = NSLayoutConstraint(
    item: contentView,
    attribute: .leading,
    relatedBy: .equal,
    toItem: view,
    attribute: .leading,
    multiplier: 1,
    constant: 20)

leadingConstraint.isActive = true
leadingConstraint.isActive = false

iOS 9 时期的改进 NSLayoutAnchor

Apple 终于在 iOS 9 中解决了约束创建语法冗长的问题。将一个新的NSLayoutAnchor. 将其视为视图中的特定点,您可以在创建约束时将其用作参考点。每个anchor只能与同轴的anchor形成约束。NSLayoutAnchor带有一组方法,包括您会在NSLayoutConstraint的操作中看到,例如等于 (=)、大于或等于 (>=)、小于或等于 (<=)。所有这些都带有各种参数,包括乘数和常数。即使有很多变化,您也可以通过键入 访问所有这些constraint。要探索所有这些变化,请查看Apple 文档

下面表格数据来及Apple 文档

PropertyLayout AnchorDescription
bottomAnchorNSLayoutYAxisAnchorA layout anchor representing the bottom edge of the view’s frame.
centerXAnchorNSLayoutXAxisAnchorA layout anchor representing the horizontal center of the view’s frame.
centerYAnchorNSLayoutYAxisAnchorA layout anchor representing the vertical center of the view’s frame.
firstBaselineAnchorNSLayoutYAxisAnchorA layout anchor representing the baseline for the topmost line of the view’s frame.
heightAnchorNSLayoutDimensionA layout anchor representing the height of the view’s frame.
lastBaselineAnchorNSLayoutYAxisAnchorA layout anchor representing the baseline for the bottommost line of the view’s frame.
leadingAnchorNSLayoutXAxisAnchorA layout anchor representing the leading edge of the view’s frame.
leftAnchorNSLayoutXAxisAnchorA layout anchor representing the left edge of the view’s frame.
rightAnchorNSLayoutXAxisAnchorA layout anchor representing the right edge of the view’s frame.
topAnchorNSLayoutYAxisAnchorA layout anchor representing the top edge of the view’s frame.
trailingAnchorNSLayoutXAxisAnchorA layout anchor representing the trailing edge of the view’s frame.
widthAnchorNSLayoutDimensionA layout anchor representing the width of the view’s frame.

简单的示例:

let constraints = [
    contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
    contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
    contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
    contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20)
]
NSLayoutConstraint.activate(constraints)

关于storyboard的Auto Layout布局,大家可以谷歌一下,这里我推荐一篇文章,同时下一篇文章将根据Apple Auto Layout的特性写一个简单的布局组件。

参考文献:

  1. addconstraints
  2. VFL
  3. activate
  4. NSLayoutAnchor
  5. iOS 自动布局:提示和技巧
  6. Masonry解析
  7. Constraints