为什么程序化布局
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 一起决定了视图在约束系统里的伸缩行为。把这些概念理清楚,程序化布局就不再是手算坐标,而是有章可循的设计。
与 SwiftUI 混用
SwiftUI 项目里也能用 UIKit:UIViewRepresentable 把 UIKit 视图包进 SwiftUI,UIViewControllerRepresentable 包进控制器。反之,SwiftUI 视图用 UIHostingController 嵌进 UIKit 层级。
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.showsUserLocation = true
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
迁移策略从边缘开始:新页面用 SwiftUI,复杂老页面留在 UIKit,用 representable 做桥梁。桥接视图记得处理生命周期回调,别把内存泄漏留给系统。
实战:登录页代码布局
把前面技巧串起来:垂直堆叠输入框和按钮,底部安全区留白,键盘弹起时整体上移。用 UIStackView 省去一半约束。
let stack = UIStackView(arrangedSubviews: [usernameField, passwordField, loginButton])
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
UIStackView 自动管理子视图的排列与间距,加一个删一个都自动重排。键盘避让监听 keyboardWillShow 通知,把 stack 的 constant 抬高一个键盘高度。
小结
程序化 UIKit 布局三件套:NSLayoutAnchor 做常规约束、UIStackView 做线性排列、layoutSubviews 做复杂自定义。约束务必 activate 成组、safe area 优先、translatesAutoresizingMaskIntoConstraints 关干净。UIKit 和 SwiftUI 不是二选一,representable 让它们互相借力。布局代码写整洁了,跨设备适配和后期维护都会轻松很多。