为什么程序化布局

Storyboard 拖拽一时爽,合并冲突火葬场。程序化布局用代码描述约束:可审查、可复用、可测试,多人协作时 merge 干干净净。UIKit 并没有被淘汰,很多老项目、复杂交互、视频播放器仍然是最佳选择。

约束即代码,布局即逻辑。

程序化布局的起点很简单:translatesAutoresizingMaskIntoConstraints 设为 false,然后铺约束。下面从基础一步步搭起来。

Auto Layout 基础

Auto Layout 用约束描述视图间的关系:对齐、等宽、间距、优先级。一套约束适配所有屏幕尺寸,旋转、分屏、动态字体都自动应对。

let label = UILabel()
label.text = "Hello UIKit"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
  label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24)
])

记住两个铁律:每个子视图必须先 addSubview 再加约束;translatesAutoresizingMaskIntoConstraints 记得关。safeAreaLayoutGuide 处理刘海屏和横竖屏,比直接绑 view 边缘安全得多。

NSLayoutAnchor 约束实战

NSLayoutAnchor 是类型安全的约束 API:leading、trailing、top、bottom、centerX、width、height 等锚点,编译器帮你拦住左右写反这类低级错误。

NSLayoutConstraint.activate([
  iconView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
  iconView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
  iconView.widthAnchor.constraint(equalToConstant: 48),
  iconView.heightAnchor.constraint(equalToConstant: 48),
  titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
  titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16)
])

用 lessThanOrEqualTo 或 greaterThanOrEqualTo 表达「不超过」「不小于」,配合 compression resistance 和 hugging priority,让长文本有弹性而不是爆掉布局。

约束写完之后还有一个检查习惯:把视图的 translatesAutoresizingMaskIntoConstraints 设为 false,这是新手最容易漏的一步;之后用 view.hasAmbiguousLayout() 在调试期检查是否有模棱两可的约束,配合 Xcode 的 view hierarchy 工具可视化查看,通常能很快定位到约束冲突或缺失的约束。

另外建议把约束集中到一个方法里统一激活,用分组注释标明每组约束的作用,多人协作时读起来一目了然。

自定义视图与布局

自定义视图重写 layoutSubviews,用 frame 直接摆放子视图,适合性能敏感或无法用 Auto Layout 表达的布局。配合 intrinsicContentSize 声明固有尺寸。

final class ProgressBar: UIView {
  private let fillView = UIView()

  override init(frame: CGRect) {
    super.init(frame: frame)
    fillView.backgroundColor = .systemBlue
    addSubview(fillView)
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    let w = bounds.width * progress
    fillView.frame = CGRect(x: 0, y: 0, width: w, height: bounds.height)
  }
}

layoutSubviews 里不要做重活,它可能被频繁调用。尺寸计算缓存起来,布局只做分配,性能才有保障。

自定义视图与 Auto Layout 并不是二选一的关系,工程里更常见的做法是两者配合:复杂的容器视图用 Auto Layout 声明约束,个别对性能敏感或需要精确控制的内层视图用 frame 手动布局。选择框架时,遵循几个原则可以让代码更好维护:一是 setNeedsLayout 与 layoutIfNeeded 的语义要分清楚,前者只是标记需要重新布局,后者会同步执行一次布局,动画场景里常常要用到两者的组合;二是不要直接改 frame 触发隐式布局,应该通过约束或显式调用布局方法来驱动,否则容易出现布局不一致;三是把视图的固有尺寸声明好,intrinsicContentSize 和 contentHuggingPriority、contentCompressionResistancePriority 一起决定了视图在约束系统里的伸缩行为。把这些概念理清楚,程序化布局就不再是手算坐标,而是有章可循的设计。