命令式心智模型
UIKit 是"你持有视图对象,手动改它的属性"。界面状态由你维护,刷新由你触发。它比 SwiftUI 更啰嗦,但也更可控、调试更直观。
大量存量代码、第三方库、以及 SwiftUI 尚未完全覆盖的能力(如复杂文本排版、某些系统弹窗定制)仍以 UIKit 为主。且 SwiftUI 底层就是构建在 UIKit/AppKit 之上的。懂 UIKit,出问题才能下钻。
ViewController 生命周期
视图控制器有一组清晰的回调,时机错了就会出现"视图还没加载就改它"或"在错误时机做重活"。
| 回调 | 时机 | 典型用途 |
|---|---|---|
loadView() | 视图即将被访问且为空时 | 用代码创建根视图(少用) |
viewDidLoad() | 视图已从 nib/storyboard 加载完 | 一次性初始化、绑定数据 |
viewWillAppear(_:) | 即将显示 | 刷新数据、注册通知 |
viewDidAppear(_:) | 已显示 | 启动动画、埋点 |
viewWillDisappear(_:) | 即将消失 | 保存草稿、注销通知 |
class ProfileViewController: UIViewController {
private let nameLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad() // 先调用父类
view.backgroundColor = .systemBackground
view.addSubview(nameLabel)
configureConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
nameLabel.text = UserSession.shared.name // 每次出现刷新
}
}
视图与图层
每个 UIView 背后有一个 CALayer 负责实际渲染。理解"视图负责交互、图层负责绘制"有利于做高性能动画与圆角裁剪。
let card = UIView()
card.layer.cornerRadius = 12
card.layer.shadowColor = UIColor.black.cgColor
card.layer.shadowOpacity = 0.15
card.clipsToBounds = false // 阴影需要不裁剪
给大量滚动 cell 设置 cornerRadius + masksToBounds 会引发离屏渲染,滚动掉帧。列表场景优先用矢量圆角或预渲染图片。
Auto Layout
Auto Layout 用"约束"描述视图间与视图-父容器间的位置关系,系统据此求解每个视图的 frame。推荐用锚点 API 或 UIStackView 减少约束数量。
nameLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
])
// 更简洁:布局锚点 DSL(需 import SnapKit 或自封装)
// nameLabel.snp.makeConstraints { $0.leading.top.equalToSuperview().offset(16) }
UIStackView 用排列+间距表达线性布局,内部自动管理约束,既少写代码又天然自适应,是 UIKit 里最接近 SwiftUI 栈的体验。
响应链与手势
触摸事件沿"响应链"从最上层视图逐级向上传递,直到某个对象处理它。手势识别器(UIGestureRecognizer)是处理点击、滑动、长按的标准方式。
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
view.addGestureRecognizer(tap)
@objc private func didTap() { print("tapped") }
列表与集合视图
现代 UIKit 列表用 UICollectionView + UICollectionViewDiffableDataSource,用快照(snapshot)声明"数据现在长什么样",框架自动计算差异动画。
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
单列出场景 UITableView 仍常见且简单;但新项目推荐 UICollectionView Compositional Layout,一套 API 同时覆盖列表与网格,配合 Diffable Data Source 免去手动 reload 的 index 错误。
与 SwiftUI 混编
在 UIKit 中嵌入 SwiftUI 用 UIHostingController;在 SwiftUI 中嵌入 UIKit 用上一章的 UIViewRepresentable。两者桥接很成熟。
// UIKit 中呈现一个 SwiftUI 视图
let hosting = UIHostingController(rootView: MySwiftUIView())
present(hosting, animated: true)
常见陷阱与 FAQ
在 viewDidLoad 之前访问 storyboard/nib 里的 outlet 会是 nil——视图尚未加载。确保对 outlet 的操作放在 viewDidLoad 及之后。纯代码创建的视图不存在此问题。
约束不足以唯一确定布局(缺宽高或位置),或约束互相冲突。优先用 stack view 减少约束数量;调试用 view.debugDescription 或 Xcode 的约束冲突日志定位。
任何 UIKit 操作必须在主线程。后台拿到数据后用 DispatchQueue.main.async 或 @MainActor 切回。在子线程改 UI 会触发随机崩溃或界面错乱。