AutoLayoutConvenience

AutoLayout 的便捷助手

简介

这是一个辅助库,包含用于常见 AutoLayout 操作的辅助函数,使 AutoLayout 的使用更具表现力。 您无需创建多个约束,只需“简单地”调用其中一个辅助函数即可

之前

subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
NSLayoutConstraint.activate([
	view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: subview.topAnchor, constant: -8),
	view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: subview.leadingAnchor, constant: -8),
	view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: subview.bottomAchor, constant: -8),
	view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: -8),
])

之后

view.addSubview(subview, filling: .safeArea, insets: .all(8))

它是什么?

有 7 个主要的 AutoLayout 操作

您可以基于一组简单的条件有条件地 (conditionally) 应用创建的约束:例如,当垂直方向为常规尺寸类别时,将视图固定到顶部;当垂直方向为紧凑尺寸类别时,将其固定到中心。

还有一些 UIStackView 的辅助功能:

还有 UIScrollView 助手,可使内容仅在需要时 (only when needed) 溢出。

示例

给定以下视图:

let titleLabel = UILabel(text: "Title Label", textStyle: .largeTitle, alignment: .center)
let subLabel = UILabel(text: String(repeating: "Sub label with a lot of text. ", count: 10), textStyle: .body, alignment: .center)
let closeButton = UIButton(type: .close)
let backgroundView = UIView(backgroundColor: .systemGroupedBackground)
let actionButton = UIButton.platter(title: "Add More Text", titleColor: .white)
let cancelButton = UIButton.platter(title: "Revert", backgroundColor: .white)
let buttonSize = CGSize(width: 32, height: 32)
let smallButtonSize = CGSize(width: 24, height: 24)

以下 8 行代码创建一个视图,其中 titleLabelsubLabel 在 backgroundView 的剩余空间中居中,并遵循可读内容指南;按钮垂直或水平附加到底部,具体取决于设备的垂直尺寸类别;关闭按钮位于 backgroundView 的左上角或右上角,具体取决于垂直尺寸类别。在垂直紧凑的环境中,它也会更小。 标签会在需要时自动变为可滚动。

let content = UIView.verticallyStacked(
	UIView.verticallyStacked(titleLabel, subLabel, spacing: 4).verticallyCentered().verticallyScrollable(),
	UIView.autoAdjustingVerticallyStacked(actionButton, cancelButton, spacing: 8)
)

backgroundView.addSubview(content, filling: .readableContent)
addSubview(backgroundView, filling: .safeArea, insets: .all(32))
UIView.if(.verticallyCompact) {
	backgroundView.addSubview(closeButton.constrain(size: smallButtonSize), pinning: .center, to: .topTrailing)
} else: {
	backgroundView.addSubview(closeButton.constrain(size: buttonSize), pinning: .center, to: .topLeading)
}

Layout Buttons Automatically Adjust Automatically scrollable

用法

基础

该库的基础是所谓的 anchorable layouts,它定义了要使用的锚点和布局指南。 以下可锚定对象和布局可用:

如你所见,有一些可锚定对象接受特定的 UIView,而另一些则不接受。 不接受的那些总是应用于相关视图,通常是被添加的子视图。

还有一些锚点用于处理键盘:

这些锚点是使用自定义 UILayoutGuide 子类实现的,这些子类响应键盘事件。 它们最适合静态视图,因为它们不跟踪视图更改位置。

还有一些锚点用于处理可排除的区域

这些锚点是使用自定义 UILayoutGuides 实现的,它们表示在 4 个侧面之一被排除的区域,要么是因为:

这些实现在 UIView.unsafeAreaLayoutGuides.[top|bottom|leading|trailing], UIView.unreadableContentGuides.[top|bottom|leading|trailing]UIView.excludedByLayoutMarginGuides.[top|bottom|leading|trailing].

内边距

库中的大多数辅助函数都采用内边距。 在 NSDirectionalEdgeInsets 上定义了便捷的辅助程序,使它们更具语义且更短:

填充

Example of Filling

通过指定要约束到的 4 个边缘 (BoxLayout) 来完成填充,并可选择设置内边距

// Fills the insetted by 8pts safeArea of its superview
addSubview(subview, filling: .safeArea, insets: .all(8))

// Fills the layoutMargins of another view that is in the same hierarchy
addSubview(subview, filling: .layoutMargins(anotherView))

// Fills the superview horizontally, safeArea vertically
addSubview(subview, filling: .horizontally(.superview, vertically: .safeArea))

// Fills the superview horizontally, and attached to the safeArea on top, the superview on the bottom
addSubview(subview, filling: .horizontally(.superview, vertically: .top(.safeArea, .bottom: .superview)))

// Fills the view by constraining to the specified edges
addSubview(subview, filling: .top(.safeArea, leading: .safeArea, bottom: .layoutMargins, trailing: .readableContent))

你也可以一步完成填充和定位,使子视图不大于给定的框

// pins subview at the center while being as large as possible, but never extending the safe area (insetted by 8 pts)
addSubview(subview, fillingAtMost: .safeArea, insets: .all(8), pinnedTo: .center)

// pins the topLeading point of the subview to the center of the subview while being as large as possible,
//  but never extending the safe area (insetted by 8 pts)
addSubview(subview, fillingAtMost: .safeArea, insets: .all(8), pinning: .topLeading, to: .center)

居中

Example of Centering

通过指定要居中的 x,y 位置 (PointLayout) 来完成居中,并可选择设置偏移量

// Centers the subview in the layoutMargins of its superview, ofsetted by 4pt horizontally
addSubview(subview, centeredIn: .layoutMargins, offset: CGPoint(x: 4, y: 0))

// Centers the subview horizontally in its superview, vertically in the safeArea of another view
addSubview(subview, centeredIn: .x(.superview, y: .safeAreaOf(anotherView)))

固定位置

Example of Pinning To Positions Example of Pinning Positions

通过指定要固定到的位置来完成固定

相对于 x,y 位置 (PointLayout)。 pinnedTo: 将两个视图中的相同位置固定,pinning:to: 固定两个不同的位置。

//  Pins the top center of subview to the top center of its superview, offsetted by 4pts horizontally
addSubview(subview, pinnedTo: .topCenter, of: .superview, offset: CGPoint(x: 4, 0))

// Pins bottom leading point of subview to the bottom leading point of another view
addSubview(subview, pinnedTo: .bottomLeading, of: .relative(anotherView))

// Pins the center of subview to the top leading of its superview
adSubview(subview, pinning: .center, to: .topLeading, of: .superview)

固定到常量 Rects / Points

Example of Pinning To Rects and Points

你也可以使用以下方法将视图固定到常量矩形或点:

/// pins the subview to the given rect
addSubview(subview, pinnedAt: CGRect(x: 10, y: 20, width: 100, height: 40))

/// pins the subview to the given rect in another view
addSubview(subview, pinnedAt: CGRect(x: 10, y: 20, width: 100, height: 40), in: .relative(anotherView)

/// pins the subview to the given point - the view needs to have a defined width & height or an intrinsic size
addSubview(subview, pinnedAt: CGPoint(x: 20, y: 10))

/// pins the subview to the given point in the safeArea - the view needs to have a defined width & height or an intrinsic size
addSubview(subview, pinnedAt: CGPoint(x: 20, y: 10), in: .safeArea)

固定边缘

固定边缘分为垂直 (vertical)水平 (horizontal) 变体,它们几乎相互镜像。

垂直方向,我们可以固定:

Example of Pinning Vertical Edges

水平方向,我们可以固定:

Example of Pinning Horizontal Edges

通过指定边缘 (YAxisLayoutXAxisLayout) 来完成固定边缘,并采用可选的间距 (spacing)内边距 (insets) 参数。 此外,您可以指定如何约束相反的轴

Example of Aligning Edges

例子

// Pins the top edge of subview to the top edge of its superview with 4pts spacing 
// between them. Horizontally, we center in the superview
addSubview(pinnedTo: .top, of: .superview, horizontally: .center,spacing: 4)

// Pins the leading edge of subview to the leading edge of its superview's safeArea
// and insetting the view by 10pts. Vertically, we align to the top of the superview
addSubview(pinnedTo: .leading, of: .superview, vertically: .top, insets: .all(10))

// Pins subview so that it is below the sibblingView, while horizontally centering
// to the sibblingView.
addSubview(subview, pinningBelow: sibblingView, horizontally: .attached(.center))

// Makes subview fill the remaing space below sibblingView
addSubview(subview, fillingRemainingSpaceBelow: sibblingView)

还有一个助手可以将一堆视图固定到父视图并相互固定,很像 UIStackView,除了没有它的开销

// this stacks viewA, viewB, viewC and viewD along side the vertical axis,
// pinning:
//	- viewA 40pts to the top edge of the superview
// 	- viewB to viewA
//	- viewC to viewB 
//	- and viewD 40pts from the bottom edge of the superview
//
// The spacing between the views will be 8pts and on the horizontal axis,
// the views will be centered
addSubviewsVertically(viewA, viewB, viewC, viewD, horizontally: .center, insets: .all(40), spacing: 8)

// same, but the views are now pinned to the safeArea instead of the superview and no insets or spacing
addSubviewsVertically(viewA, viewB, viewC, viewD, in: .safeArea)

// same, but the views are stacked horizontally
addSubviewsHorizontally(viewA, viewB, viewC, viewD, vertically: .center, insets: .all(40), spacing: 8)

对齐边缘

对齐就像固定,不同之处在于它将确保视图尊重其父视图的边界。 如果你想要一个视图尽可能大,但永远不要超过其父视图的边界,这将为你完成它。

对齐时,您需要指定垂直水平边缘的约束方式

水平方向

垂直方向

例子

// This makes subview align to the top of its superview.
// subview will never grow past the bottom edge of its superview,
// but if its smaller it will not fill until the bottom edge:
//
// You can think of this as: bottom edge < superviews.bottom edge  
//
// Horizontally, the view will fill its superview
addSubview(subview, aligningVerticallyTo: .top)


// this makes subview align to the horizontal center of its superviews 
// layoutMargins, while never growing past the layout margins if it needs to be bigger/
//
// Vertically, the view will fill its superview
addSubview(subview, aligningHorizontallyTo: .center(in: .layoutMargins))

//this will make the subview align vertically to its superview and horizontally to the bottom.
// subview will not extend past the edges of its superview insetted by 10 pts.
addSubview(subview, aligningVerticallyTo: .center, horizontally: .bottom, insets: .all(10))

约束

约束宽度/高度的示例

view.constrain(width: 100)
view.constrain(height: 20)
view.constrain(width: 100, height: 20)
view.constrain(size: (CGSize(width: 100, height: 20))

view.constrain(width: .atMost(100)) // not wider than 100
view.constrain(width: .atLeast(50)) // not smaller than 50
view.constrain(width: .exactly(100)) // exactly 100 wide

// not bigger than 100x30, but with defaultLow priority
view.constrain(size: .atMost(CGSize(width: 100, height: 30), priority: .defaultLow))

// the width should be at least 10 and smaller than 20
view.constrain(widthBetween: 10..<20)

// the height should be at least 10 and at most 20
view.constrain(heightBetween: 10...20)

/// removing existing width constraints and setting a new width constraint
view.removeWidthConstraints().constrain(width: 100)

// removing existing height constraints and setting a new height constraint
view.removeHeightConstraints().constrain(height: 100)

/// removing existing size constraints and setting a new size constraint
view.removeSizeConstraints().constrain(size: CGSize(width: 100, height: 100))

动态约束宽度/高度

还有一些便捷方法可以动态地将视图约束为固定的宽度/高度,因此您可以轻松更新它,而无需跟踪 NSLayoutConstraints。

如果宽度/高度需要更改为另一个硬编码值,请使用这些方法。

view.constrainedFixedWidth = 100 // view will be constrained to 100 width
view.constrainedFixedWidth = 200 // view will now be constrained to 200 width

view.constrainedFixedHeight = 100 // view will be constrained to 100 width
view.constrainedFixedSize = CGSize(100, 150) // view will be constrained to 100-width, 150 height

约束到其他布局

约束宽度/高度的示例:addSubview(otherView, centeredIn: .superview) addSubview(view, pinnedTo: .topCenter)

// Note that this must be called after the view has been added to the hierarchy already,
// since it creates cross-view constraints.
view.constrain(width: .exactly(.relative(otherView)), height: .atLeast(.safeAreaOf(otherView))

// short hand for constraining to views directly
view.constrain(width: .exactly(as: otherView), height: .atLeast(halfOf: otherView))

// 20% of our superview
view.constrain(width: .exactly(.superview, times: 0.2))

// 20% of `otherView`
view.constrain(width: .exactly(sameAs: otherView, times: 0.2))

约束宽高比的示例

// the width will be twice the height
view.constrainAspectRatio(2.0)

// the width will have the same aspect ratio as the given size
view.constrainAspectRatio(for: CGSize(width: 200, height: 100))

(不允许)增长/收缩

有一些可链式使用的辅助方法用于 setContentCompressionResistancePriority()setContentHuggingPriority()

收缩

增长

收缩和增长

条件约束

有时,您希望根据某些条件使用不同的约束。例如,您可能希望在屏幕垂直规则时视图的高度为 100 点,但在屏幕垂直紧凑时仅为 20 点,并根据垂直尺寸类更改位置。 可以通过重写 traitCollectionDidChange(_:) 并删除和设置约束来手动执行此操作,但这需要大量的簿记,并且您的布局代码将不再位于同一位置。

条件约束 可以轻松地实现这一点

UIView.if(.isVerticallyRegular) {
	button.constrain(height: 100)
	view.addSubview(button, pinningTo: .top)
} else: {
	button.constrain(height: 20)
	view.addSubview(button, pinningTo: .bottom)
}

创建条件约束

您可以通过调用 UIView.if(_:then:else:) 来创建条件约束。 如果条件成立,则 then 闭包中创建的约束将处于活动状态,否则 else 闭包中的约束将处于活动状态。 重要的是要注意,这并非实际的 if-else 块,并且 thenelse 闭包中的代码仅会运行一次,并且条件仅适用于创建的约束。

简而言之,对于 thenelse 块中的代码

条件

可以使用几种不同类型的条件来激活约束。 所有这些约束都适用于相关的视图。 如果未指定自定义视图,则相关视图是我们为其添加约束的视图。

尺寸

可见性

Traits

回调:使用回调时,请小心不要强引用视图,以免创建保留周期。

组合

实例辅助方法

特定视图

所有这些约束都适用于特定视图,而不是为其创建约束的视图。

视图特定条件

默认情况下,条件适用于获取约束的视图。 如果您要检查另一个视图,您有两种选择

这两者之间没有区别,只是偏好的问题。

合并更新

当一个条件“改变”并可能发生变化时,条件约束将被重新评估,并且正确的约束将被激活。 这是通过监视视图的边界和/或 traitCollection 的更改来实现的。 对于简单条件,约束将在条件更改时直接应用。 对于更复杂的条件,对活动约束的更新将被合并,并在运行循环结束时执行。 这样做是为了避免在视图更改仍在进行中时不断地重新评估条件。

简单条件是指那些依赖于单个视图的边界或 trait 集合之一的条件,但不能同时依赖于两者。 任何其他情况(多个视图或同时依赖于边界或 traits)都是合并更新的复杂条件。

启用直接更新

您可以通过两种方式禁用合并(从而实现直接更新)

强制更新

如果您使用自定义回调作为条件,您可能需要自己强制更新。 你可以使用

动画

作为条件约束的结果的布局更改可以被动画化。 你可以 - 再次 - 通过两种方式做到这一点:- 在 UIView.if() {} else: {} 调用上调用 animateChanges()UIView.if(.verticalCompact){} else: {}.animateChanges() - 在相关的 UIView 上调用 enableAnimationsForConditionalUpdates()myLabel.enableAnimationsForConditionalUpdates()

命名配置

也可以按名称切换配置。 默认情况下,任何使用条件约束的视图都具有 .main 的配置名称。 可以根据此名称使用 .name(is:) 创建条件。

可以使用 view.activeConditionalConstraintsConfigurationName = ... 更改名称,这反过来会更新相关的条件配置。 还有一个添加命名条件配置的快捷方式:UIView.addNamedConditionalConfiguration(_:configuration:),它等于 UIView.if(.name(...), then: configuration)

有四个预定义的配置名称

您还可以使用UIView.Condition.ConfigurationName(rawValue: ...)定义自己的名称。

例如:

// create two configurations
UIView.addNamedConditionalConfiguration(.main) { button.constrain(widthAndHeight: 44) }
UIView.addNamedConditionalConfiguration(.alternative) { button.constrain(widthAndHeight: 24) }.animateChanges()

DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
	button.activeConditionalConstraintsConfigurationName = .alternative
}

底层工作原理

条件约束带来一些小魔法

我们使用自己的系统来表达条件,并且只执行一次thenelse代码块的原因是,我们不希望创建循环引用:如果我们在每次更改时都执行闭包,我们需要存储闭包,并且任何使用的视图都将被强引用,从而导致循环引用。相反,我们使用自己的系统来定义条件并记录创建的约束。

存储的约束

跟踪约束有时可能很烦人:您必须捕获并存储ConstrainstList,停用它,设置新的约束才能进行小的更改,因为某些NSLayoutConstraint选项需要重新配置。 为了简化此操作,您可以使用Stored Constraints。 如果您使用addStoredConstraints()/replaceStoredConstraints(),系统会自动将您在其闭包中创建的约束保存在identifier下。 下次您使用相同的标识符调用此方法时,旧的约束将自动停用,并且您在闭包中创建的新约束将被安装。

请注意,如果尚未设置任何约束,replaceStoredConstraints也将起作用,但为了提高可读性,有一个别名addStoredConstraints执行完全相同的操作。

标识符的类型为ConstraintsList.Identifier。 有几个标准标识符(mainwidthheight),您也可以使用.custom("MyName")命名自己的标识符。 所有存储约束方法默认使用.main,因此您可以在不提供标识符的情况下使用它。

示例

let topView = UIView().constrain(height: 50)
view.addSubview(topView, pinnedTo: .topCenter, of: .layoutMargins)

topView.addStoredConstraints(for: .width) {
	// half of the superview
	topView.constrain(width: .exactly(.superview, times: 0.5))
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
	UIView.animate(duration: 0.25) {
		topView.replaceStoredConstraints(for: .width) {
			// 70% of the superview
			topView.constrain(width: .exactly(.superview, times: 0.7))
		}
	}
}

批量激活约束

有时您有两个相互依赖的约束,但您想按特定顺序添加它们。 这可能会导致异常,因为相互约束的视图需要在同一个层级结构中。 为了解决这个问题,您可以批量激活约束。

let bottomView = UIView(backgroundColor: .red).constrain(widthAndHeight: 50)
let topView = UIView(backgroundColor: .green).constrain(widthAndHeight: 100)

UIView.batchConstraints {
	addSubview(bottomView, pinning: .center, to: .topLeading, of: .relative(topView))
	addSubview(topView, centeredIn: .safeArea)
}

如果没有UIView.batchConstraints(),第一次addSubview()调用会崩溃,因为topView尚未在视图层次结构中。 将这两个调用包装在一个UIView.batchConstraints {}中可以解决这个问题,因为在其闭包中创建的所有约束只有在该闭包运行完毕并且所有视图都已添加到层次结构后才会被激活。

UIStackView

有几个用于处理(包装器)UIStackView的辅助方法

工厂

所有这些方法都采用可选的spacinginsets参数。

堆叠
对齐

所有这些函数也都有**静态**变体,便于组合。

居中

所有这些函数也都有**静态**变体,便于组合。

插入

此函数也有一个**静态**变体,便于组合。

自动调整

有两个UIStackView子类,它们根据相对轴的紧凑性自动切换其轴

辅助工厂

ScrollView

有两个UIScrollView子类参与自动布局,并在需要时变为可滚动

键盘回避

VerticalOverflowScrollView可以通过设置isAdjustingForKeyboard = true来避免键盘。

工厂

这些函数都有用于相对轴的参数,并且都有**静态**变体,便于组合。

FixedFrameLayoutGuide

一个辅助UILayoutGuide,在其拥有的视图中具有固定框架。 适用于组合自动布局和手动计算。

示例

let layoutGuide = FixedFrameLayoutGuide()
view.addLayoutGuide(layoutGuide)

otherView.addSubview(label, pinnedTo: .center, of: .guide(layoutGuide))

layoutGuide.frame = CGRect(x: 100, y: 50, width: 100, height: 30)

AutoSizingTableHeaderFooterView

UITableView的tableHeaderViewtableFooterView不能很好地与自动布局配合使用:您需要在分配这些视图之前对其进行预先调整大小,然后跟踪更改并重新分配视图以更新其在表视图中的大小。 另一个问题是,当表视图更改大小时(例如,在旋转时),您需要手动调整它们的大小。

您可以使用一个辅助类来拥有具有UITableView的自动调整大小的tableHeaderViewtableFooterView。 将AutoSizingTableHeaderFooterView(view:)用作header或footer,它会在固有内容大小更改时自动更新视图,并带有动画(可以禁用)。

在UITableView上也有一些辅助函数可以轻松地进行设置。

例子

// the tableHeaderView will automatically be updated to the correct size when myAutoLayoutHeaderView
// changes its size, with animation. 
tableView.tableHeaderView = AutoSizingTableHeaderFooterView(view: myAutoLayoutHeaderView)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { myAutoLayoutHeaderView.somethingThatUpdatesTheContentHeightOfThisView()  }

// this is a shortcut for  tableHeaderView = AutoSizingTableHeaderFooterView(view: view)
tableView.selfSizingTableHeaderView = myAutoLayoutHeaderView

// you can also disable animations on size updates
let headerView = AutoSizingTableHeaderFooterView(view: myAutoLayoutHeaderView)
headerView.automaticallyAnimateChanges = false
tableView.tableHeaderView = headerView

// And of course, all of these methods have a footer view equivalent:
tableView.selfSizingTableFooterView = myAutoLayoutFooterView


// if you have a view that uses manual layout, you can use 
// `manualLayoutAutoSizingTableHeaderView` to have it size automatically.
// You need to call the update() method or invalidate the intrinsic content size
// to update changes.
let manualLayoutView = MyManualLayoutViewImplementingSizeThatFits()
tableView.manualLayoutAutoSizingTableHeaderView = manualLayoutView
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
	manualLayoutAutoSizingTableHeaderView.somethingThatUpdatesTheContentHeightOfThisView()
	tableView.updateAutoSizingTableHeader()
}

}