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 行代码创建一个视图,其中 titleLabel
和 subLabel
在 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)
}
该库的基础是所谓的 anchorable layouts
,它定义了要使用的锚点和布局指南。 以下可锚定对象和布局可用:
none
:不执行布局default
:使用默认布局(稍后详细介绍)superview
:锚定到相关视图的父视图relative(UIView)
:锚定到特定的 UIView
guide(UILayoutGuide)
:锚定到特定的 UILayoutGuide
safeArea
:锚定到相关视图的 safeAreaLayoutGuide
safeAreaOf(UIView)
:锚定到特定视图的 safeAreaLayoutGuide
layoutMargins
:锚定到相关视图的 layoutMarginsGuide
layoutMarginsOf(UIView)
:锚定到特定视图的 layoutMarginsGuide
readableContent
:锚定到相关视图的 readableContentGuide
readableContentOf(UIView)
:锚定到特定视图的 readableContentGuide
scrollContent
:如果相关视图是 UIScrollView
,则锚定到其 contentLayoutGuide
,否则仅锚定到视图本身scrollContentOf(UIView)
:锚定到特定 UIScrollView
的 contentLayoutGuide
scrollFrame
:如果相关视图是 UIScrollView
,则锚定到其 frameLayoutGuide
,否则仅锚定到视图本身scrollFrameOf(UIView)
:锚定到特定 UIScrollView
的 frameLayoutGuide
keyboardSafeArea
:锚定到相关视图的 keyboardSafeAreaLayoutGuide
keyboardSafeAreaOf(UIView)
:锚定到特定视图的 keyboardSafeAreaLayoutGuide
keyboardFrame
:锚定到相关视图的 keyboardFrameLayoutGuide
keyboardFrameOf(UIView)
:锚定到特定视图的 keyboardFrameLayoutGuide
excludedLeadingSideOf(ExcludableArea)
:锚定到可排除区域(safeArea、layoutMargins 或 readableContent)的前导边excludedTrailingSideOf(ExcludableArea)
:锚定到可排除区域(safeArea、layoutMargins 或 readableContent)的尾随边excludedBottomSideOf(ExcludableArea)
:锚定到可排除区域(safeArea、layoutMargins 或 readableContent)的底部excludedTopSideOf(ExcludableArea)
:锚定到可排除区域(safeArea、layoutMargins 或 readableContent)的顶部如你所见,有一些可锚定对象接受特定的 UIView
,而另一些则不接受。 不接受的那些总是应用于相关视图,通常是被添加的子视图。
还有一些锚点用于处理键盘:
keyboardSafeArea[Of]
:这是安全区域减去键盘。 简而言之,它是键盘或安全区域插图未覆盖的区域。keyboardFrame[Of]
:这是键盘的框架。 如果键盘隐藏,则其高度为 0。这些锚点是使用自定义 UILayoutGuide
子类实现的,这些子类响应键盘事件。 它们最适合静态视图,因为它们不跟踪视图更改位置。
还有一些锚点用于处理可排除的区域
excluded[Leading|Trailing|Top|Bottom]SideOf()
这些锚点是使用自定义 UILayoutGuides 实现的,它们表示在 4 个侧面之一被排除的区域,要么是因为:
.safeArea
,所以它是 "不安全" 的区域.layoutMargins
,所以它是实际的边距.readableContent
,所以它是你不应该放置文本的区域。这些实现在 UIView.unsafeAreaLayoutGuides.[top|bottom|leading|trailing]
, UIView.unreadableContentGuides.[top|bottom|leading|trailing]
和 UIView.excludedByLayoutMarginGuides.[top|bottom|leading|trailing]
.
库中的大多数辅助函数都采用内边距。 在 NSDirectionalEdgeInsets
上定义了便捷的辅助程序,使它们更具语义且更短:
.all(value)
:所有边缘都设置为 value
.top(value)
:仅顶部内边距,其他为 0
.leading(value)
:仅前导内边距,其他为 0
.trailing(value)
:仅尾随内边距,其他为 0
.bottom(value)
:仅底部内边距,其他为 0
.vertical(value)
:仅顶部和底部内边距,其他为 0
.horizontal(value)
:仅前导和尾随内边距,其他为 0
.insets(horizontal: value, vertical: otherValue)
:顶部和底部设置为 value,前导和尾随设置为 otherValue
with(top:)
、with(leading:)
、with(bottom:)
、with(trailing:)
:更改某些现有内边距的指定边缘
with(horizontal:)
、with(vertical:)
:更改某些现有内边距的指定边缘
.with(insets1, insets2, insets3)
:将所有内边距加在一起
adding(NSDirectionalEdgeInsets)
:将其他内边距添加到给定的内边距
multiply(value)
:将所有边缘乘以给定的 value
horizontallySwapped
:交换水平边缘的内边距
verticallySwapped
:交换垂直边缘的内边距
通过指定要约束到的 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)
通过指定要居中的 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)))
通过指定要固定到的位置来完成固定
相对于 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)
你也可以使用以下方法将视图固定到常量矩形或点:
/// 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) 变体,它们几乎相互镜像。
垂直方向,我们可以固定:
水平方向,我们可以固定:
通过指定边缘 (YAxisLayout
或 XAxisLayout
) 来完成固定边缘,并采用可选的间距 (spacing) 和内边距 (insets) 参数。 此外,您可以指定如何约束相反的轴
.fill
(默认):使视图填充其父视图的相对边缘filling(Other)
:使视图填充另一个布局,例如 filling(.safeArea)
center
:使视图在其父视图中居中centered(in: Other)
:使视图在另一个布局中居中,例如 filling(.layoutMargins)
centered(in: Other, between: Other)
:使视图在另一个布局中居中,同时约束到另一个布局,例如 centered(in: .superview, between: .safeArea)
overflow(Other)
:使视图不受约束:如果它不适合,它可以溢出其父视图,例如 .overflow(.center)
attach
:使视图约束到我们固定到的视图,而不是其父视图attached(Other)
:使视图约束到我们固定到的视图中的另一个布局,而不是其父视图例子
// 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)
对齐就像固定,不同之处在于它将确保视图尊重其父视图的边界。 如果你想要一个视图尽可能大,但永远不要超过其父视图的边界,这将为你完成它。
对齐时,您需要指定垂直和水平边缘的约束方式
.fill
(默认),使视图水平填充其父视图filling(Other)
:使视图填充另一个布局,例如 filling(.safeArea)
center
使视图在其父视图中水平居中centered(in: Other)
:使视图在另一个布局中居中,例如 filling(.layoutMargins)
centered(in: Other, between: Other)
:使视图在另一个布局中居中,同时约束到另一个布局,例如 centered(in: .superview, between: .safeArea)
overflow(Other)
:使视图不受约束:如果它不适合,它可以溢出其父视图,例如 .overflow(.center)
.leading
使视图水平方向与前导边缘对齐,并根据需要占用尽可能多的空间,但不能超过尾随边缘.trailing
使视图水平方向与尾随边缘对齐,并根据需要占用尽可能多的空间,但不能超过前导边缘.fill
(默认),使视图垂直填充其父视图filling(Other)
:使视图填充另一个布局,例如 filling(.safeArea)
center
使视图在其父视图中垂直居中centered(in: Other)
:使视图在另一个布局中居中,例如 filling(.layoutMargins)
centered(in: Other, between: Other)
:使视图在另一个布局中居中,同时约束到另一个布局,例如 centered(in: .superview, between: .safeArea)
overflow(Other)
:使视图不受约束:如果它不适合,它可以溢出其父视图,例如 .overflow(.center)
.top
使视图垂直方向与顶部边缘对齐,并根据需要占用尽可能多的空间,但不能超过底部边缘.bottom
使视图垂直方向与底部边缘对齐,并根据需要占用尽可能多的空间,但不能超过顶部边缘例子
// 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()
allowVerticalShrinking()
将垂直压缩阻力优先级设置为 .defaultLow
allowHorizontalShrinking()
将水平压缩阻力优先级设置为 .defaultLow
allowShrinking()
将压缩阻力优先级设置为 .defaultLow
disallowVerticalShrinking()
将垂直压缩阻力优先级设置为 .required
disallowHorizontalShrinking()
将水平压缩阻力优先级设置为 .required
disallowShrinking()
将压缩阻力优先级设置为 .required
allowVerticalGrowing()
将垂直拥抱优先级设置为 .defaultLow
allowHorizontalGrowing()
将水平拥抱优先级设置为 .defaultLow
allowGrowing()
将拥抱优先级设置为 .defaultLow
disallowVerticalGrowing()
将垂直拥抱优先级设置为 .required
disallowHorizontalGrowing()
将水平拥抱优先级设置为 .required
disallowGrowing()
将拥抱优先级设置为 .required
prefersExactHorizontalSize()
将水平压缩阻力和拥抱优先级设置为 .required
prefersExactVerticalSize()
将垂直压缩阻力和拥抱优先级设置为 .required
prefersExactHorizontalSize()
将两个轴上的压缩阻力和拥抱优先级设置为 .required
有时,您希望根据某些条件使用不同的约束。例如,您可能希望在屏幕垂直规则时视图的高度为 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 块,并且 then
和 else
闭包中的代码仅会运行一次,并且条件仅适用于创建的约束。
简而言之,对于 then
或 else
块中的代码
addSubview(subview,...)
,只要父视图相同:子视图只会添加到其父视图一次。可以使用几种不同类型的条件来激活约束。 所有这些约束都适用于相关的视图。 如果未指定自定义视图,则相关视图是我们为其添加约束的视图。
尺寸
.width(is:)
:宽度需要匹配。 例如,.width(is:.atLeast(200))
.height(is:)
:高度需要匹配。 例如,.height(is:.atMost(100))
.widthAndHeight(is:)
:宽度和高度需要匹配相同的值。 例如,.widthAndHeight(is: .exactly(100))
.width(is: heightIs:)
:宽度和高度需要匹配。 例如,.width(is: .exactly(100), heightIs: .atMost(50))
可见性
.hidden
:视图需要隐藏。 例如,(.view(someView, is: .hidden))
.visible
:视图需要不隐藏。 例如,(.view(someView, is: .visible))
Traits
.traits(in:)
:trait 集合必须包含给定的 traits。 例如,.traits(in: UITraitCollection(legibilityWeight: .bold))
trait(_:,is:)
:trait 集合中的属性必须匹配一个值。 例如,.trait(\.legibilityWeight, is: .bold)
.verticallyCompact
:当 trait 集合具有垂直紧凑尺寸类时匹配.verticallyRegular
:当 trait 集合具有垂直规则尺寸类时匹配.horizontallyCompact
:当 trait 集合具有水平紧凑尺寸类时匹配.horizontallyRegular
:当 trait 集合具有水平规则尺寸类时匹配.phone
:当 trait 集合的 idiom 是 phone 时匹配.pad
:当 trait 集合的 idiom 是 pad 时匹配.mac
:当 trait 集合的 idiom 是 mac 时匹配.tv
:当 trait 集合的 idiom 是 tv 时匹配.carPlay
:当 trait 集合的 idiom 是 carPlay 时匹配.light
:当 trait 集合的界面样式为 light 时匹配.dark
:当 trait 集合的界面样式为 dark 时匹配.leftToRight
:当 trait 集合的布局方向为从左到右时匹配.rightToLeft
:当 trait 集合的布局方向为从右到左时匹配回调:使用回调时,请小心不要强引用视图,以免创建保留周期。
.callback({ return someBool })
:当给定的闭包返回 true 时匹配。 不带参数。.callback({ view in return view.isHidden })
:当给定的闭包返回 true 时匹配。 参数是相关视图。组合
.all(...)
:当所有条件都匹配时匹配。 例如,.all(.verticallyCompact, .phone)
.any(...)
:当任何条件匹配时匹配。 例如,.any(.horizontallyCompact, .pad)
.not(...)
:当没有一个条件匹配时匹配。 例如,.not(.phone, .pad)
实例辅助方法
.isTrue
:当条件本身匹配时匹配,用于表达目的。 例如,.verticallyCompact.isTrue
.isFalse
:当条件不匹配时匹配。 例如,.verticallyCompact.isFalse
.and(...)
:当条件和其他条件匹配时匹配。 例如,.verticallyCompact.and(.phone)
.or(...)
:当条件匹配或任何其他条件匹配时匹配。 例如,.verticallyCompact.or(.phone)
特定视图
所有这些约束都适用于特定视图,而不是为其创建约束的视图。
.view(_:,has:)
当特定视图匹配一个条件时匹配。 视图被弱引用。 例如,(.view(label, has: .width(is: .exactly(100)))
.view(_:,is:)
是 .view(_:,is:)
的别名,用于表达目的。默认情况下,条件适用于获取约束的视图。 如果您要检查另一个视图,您有两种选择
.view(someOtherView, is: .verticalCompact)
UIView.if(view: someOtherView, is: .verticallyCompact) { ... }
这两者之间没有区别,只是偏好的问题。
当一个条件“改变”并可能发生变化时,条件约束将被重新评估,并且正确的约束将被激活。 这是通过监视视图的边界和/或 traitCollection 的更改来实现的。 对于简单条件,约束将在条件更改时直接应用。 对于更复杂的条件,对活动约束的更新将被合并,并在运行循环结束时执行。 这样做是为了避免在视图更改仍在进行中时不断地重新评估条件。
简单条件是指那些依赖于单个视图的边界或 trait 集合之一的条件,但不能同时依赖于两者。 任何其他情况(多个视图或同时依赖于边界或 traits)都是合并更新的复杂条件。
您可以通过两种方式禁用合并(从而实现直接更新)
UIView.if() {} else: {}
调用上调用 withoutCoalescing()
:UIView.if(.verticalCompact){} else: {}.withoutCoalescing()
UIView
上调用 useDirectConditionalUpdates()
:myLabel.useDirectConditionalUpdates()
如果您使用自定义回调作为条件,您可能需要自己强制更新。 你可以使用
view.setConditionalConstraintsNeedsUpdate()
将在可能的情况下重新评估此视图的条件。 如果需要和允许,将合并更新。view.updateConditionalConstraintsIfNeeded()
如果条件约束被标记为需要更新,则直接重新评估。view.forceUpdateConditionalConstraints()
即使未标记为需要更新,也直接重新评估条件约束。作为条件约束的结果的布局更改可以被动画化。 你可以 - 再次 - 通过两种方式做到这一点:- 在 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)
。
有四个预定义的配置名称
.main
如果没有对视图的 activeConditionalConstraintsConfigurationName
进行任何更改,则为默认配置.alternative
如果您有一个替代配置,则可以使用.visible
如果您的配置是“可见”状态,则可以使用。.hidden
如果您的配置是“隐藏”状态,则可以使用。您还可以使用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
}
条件约束带来一些小魔法
ConstraintsList.activate()
激活。UIView.if()
中时,任何使用ConstraintsList
创建的约束都会被ConstraintsList.intercept()
拦截。UIView.if()
收集所有约束,并将它们与相关条件一起存储在相关视图中的ConstraintListCollection
中。UIView.traitCollectionDidChange(_:)
只会被交换一次,因此我们可以拦截特性集合的变化。bounds
变化和traitCollectionDidChange(_:)
。当检测到变化时,我们会重新评估所有条件并激活正确的ConstraintsList
。UIView.if()
中时,多次相同的view.addSubview(subview)
调用将被忽略。我们使用自己的系统来表达条件,并且只执行一次then
和else
代码块的原因是,我们不希望创建循环引用:如果我们在每次更改时都执行闭包,我们需要存储闭包,并且任何使用的视图都将被强引用,从而导致循环引用。相反,我们使用自己的系统来定义条件并记录创建的约束。
跟踪约束有时可能很烦人:您必须捕获并存储ConstrainstList,停用它,设置新的约束才能进行小的更改,因为某些NSLayoutConstraint选项需要重新配置。 为了简化此操作,您可以使用Stored Constraints
。 如果您使用addStoredConstraints()
/replaceStoredConstraints()
,系统会自动将您在其闭包中创建的约束保存在identifier
下。 下次您使用相同的标识符调用此方法时,旧的约束将自动停用,并且您在闭包中创建的新约束将被安装。
请注意,如果尚未设置任何约束,replaceStoredConstraints
也将起作用,但为了提高可读性,有一个别名addStoredConstraints
执行完全相同的操作。
标识符的类型为ConstraintsList.Identifier
。 有几个标准标识符(main
,width
,height
),您也可以使用.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
有一个便捷的初始化程序,它接受视图、轴、对齐方式、分布、间距和插图。addArrangedSubviews()
一次将一堆视图添加到堆栈视图。reallyRemoveArrangedSubview()
将其也从视图中删除。所有这些方法都采用可选的spacing
和insets
参数。
verticallyStacked()
垂直堆叠给定的视图,水平alignment
默认为.fill
horizontallyStacked()
水平堆叠给定的视图,垂直alignment
默认为.fill
stacked(views, axis: ...)
沿指定的轴堆叠视图horizontally(aligned: )
将视图嵌入水平对齐的堆栈视图中vertically(aligned: )
将视图嵌入垂直对齐的堆栈视图中aligned(horizontally:vertically)
将视图嵌入两个堆栈视图中,一个水平对齐,另一个垂直对齐所有这些函数也都有**静态**变体,便于组合。
horizontallyCentered()
将视图嵌入水平居中的堆栈视图中verticallyCentered()
将视图嵌入垂直居中的堆栈视图中centered()
将视图嵌入两个堆栈视图中,这两个堆栈视图分别在其轴上居中所有这些函数也都有**静态**变体,便于组合。
insetted(by:)
将视图嵌入具有特定插图的堆栈视图中此函数也有一个**静态**变体,便于组合。
有两个UIStackView
子类,它们根据相对轴的紧凑性自动切换其轴
AutoAdjustingHorizontalStackView
AutoAdjustingVerticalStackView
autoAdjustingVerticallyStacked()
垂直堆叠给定的视图,必要时调整为水平autoAdjustingHorizontallyStacked()
水平堆叠给定的视图,必要时调整为垂直有两个UIScrollView
子类参与自动布局,并在需要时变为可滚动
VerticalOverflowScrollView
HorizontalOverflowScrollView
VerticalOverflowScrollView
可以通过设置isAdjustingForKeyboard = true
来避免键盘。
verticallyScrollable()
将视图嵌入垂直滚动视图中,该视图在需要时变为可滚动。 传递avoidsKeyboard: true
以使滚动视图自动调整键盘。horizontallyScrollable()
将视图嵌入垂直滚动视图中,该视图在需要时变为可滚动这些函数都有用于相对轴的参数,并且都有**静态**变体,便于组合。
一个辅助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)
UITableView的tableHeaderView
和tableFooterView
不能很好地与自动布局配合使用:您需要在分配这些视图之前对其进行预先调整大小,然后跟踪更改并重新分配视图以更新其在表视图中的大小。 另一个问题是,当表视图更改大小时(例如,在旋转时),您需要手动调整它们的大小。
您可以使用一个辅助类来拥有具有UITableView的自动调整大小的tableHeaderView
和tableFooterView
。 将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()
}
}