AutoLayoutHelpers

MIT License Swift Package Manager compatible Platforms

一套小型实用程序,旨在消除 autolayout API 中最常见的一些不足之处。其中包含几个不同但相关的实用程序集。

约束扩展

操作约束,尤其是在设置约束时,其开发者体验并不总是那么顺畅。虽然主要是语法糖,但此处提供的 @inlinable 实用程序允许编写更简单但又不失可读性的代码来设置约束。

例如,如果您想以高优先级设置高度约束,而不是必须编写以下代码:

let heightConstraint = view.heightAnchor.constraint(equalToConstant: Self.height)
heightConstraint.priority = .defaultHigh
heightConstraint.isActive = true

您可以像这样操作(假设之后我们不需要该约束):

view.heightAnchor.constraint(equalToConstant: Self.height).priority(.defaultHigh).activate()

约束构建器

每次有人在代码中构建 UI 并需要将其子视图与父视图边缘对齐时,都会感到非常痛苦。不再需要了。只需使用其中的实用程序一次性生成所有约束,或者生成满足您需求的任何约束子集。然后使用约束实用程序或属性包装器来激活它们。

请记住,构建这些实用程序的目的是为了处理使用常规 API 比需要的更痛苦的情况。如果您在这里寻找某些东西但没有找到,很可能我们已经确定布局锚点 API 本身已经做得足够好了。

例如,如果我们需要一个视图靠在其父视图的安全区域的左右和顶部,而不是编写以下代码:

contentView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(contentView)
NSLayoutConstraint.activate([
    contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    contentView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])

我们可以编写以下代码:

contentView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(contentView)
NSLayoutConstraint.activate(contentView.constraintsAgainstEnclosing(
    layoutArea: view.safeAreaLayoutGuide,
    edges: [.top, .leading, .trailing]
))

或者更好的是,利用整个包:

view.add(subview: contentView)
contentView.constraintsAgainstEnclosing(
    layoutArea: view.safeAreaLayoutGuide,
    edges: [.top, .leading, .trailing]
).activate()

属性包装器

在 autolayout 环境中,布局更改是通过添加和删除约束或通过调整现有约束的属性(constantpriority)来完成的。在这两种情况下,都需要在框架自动管理之外保留对这些约束的引用。

提供了一对属性包装器,ActiveConstraint(用于单个约束)和 @ActiveConstraints(用于约束数组),它们将确保放置在其中的任何约束都被激活,并且从中删除的任何约束都被停用。这可以平滑 UI 需要时的约束更改。

例如,假设我们有一个 contentView 约束到上面的 headerView,但我们希望在解除动画发生时保持其高度。我们可以这样做:

[...]
@ActiveConstraint
private var contentHeightHolder: NSLayoutConstraint?

func setupUI() {
    [...]
    contentHeightHolder = contentView.topAnchor.constraint(equalTo: headerView.bottomAnchor)
    [...]
}

func prepareForDismissal() {
    [...]
    // Layout is supposed to stable here, don't grab your frames or bounds otherwise kids!
    contentHeightHolder = contentView.heightAnchor.constraint(equalToConstant: heightHolder.frame.height)
    [...]
}

translatesAutoResizingMaskIntoConstraints 管理

管理子视图的 translatesAutoResizingMaskIntoConstraints 标志是父视图的责任,所有较新的容器 API 都会这样做(例如 UIStackView.addArrangedSubview 等)。但是,由于历史原因,原始的视图树管理 API 不会触及该标志,这会导致人们忘记手动关闭它而导致错误。

提供了一组用于框架视图层次结构管理的包装器。它们在每个平台上都是不同的,以便允许 1 对 1 替换现有逻辑。基本上,用新方法替换旧方法,并从 UI 设置逻辑中删除所有提及 translatesAutoResizingMaskIntoConstraints 的地方。

强制执行

可以安全地假设,如今手动布局在 UI 逻辑中是一个例外,但如果人们忘记使用这些实用程序,那么拥有这些实用程序也无济于事。为了鼓励开发者避免使用旧方法并使用新方法,您可以将以下规则添加到您的 linter 配置文件中:

  avoid_manually_disabling_TARMiC:
    regex: 'translatesAutoresizingMaskIntoConstraints = false'
    message: "Do not manually turn translatesAutoresizingMaskIntoConstraints off, use the managed view hierarchy methods add(subview:) and insert(subview:) instead"
    match_kinds: identifier
    
  avoid_addsubview:
    regex: 'addSubview\('
    message: "Do not call `addSubview(...)` directly, use the `add(subview:...)` wrappers instead"
    match_kinds: identifier

  avoid_insertsubview:
    regex: 'insertSubview\('
    message: "Do not call `insertSubview(...)` directly, use the `insert(subview:...)` wrappers instead"
    match_kinds: identifier

与任何其他 linter 规则一样,它们可以在有意义的地方暂时禁用。