iOS 开发手册IOS DEVELOPMENT

CHAPTER 04原生 UI · IMPERATIVE

UIKit

命令式界面的工程基石:视图控制器生命周期、视图与图层、Auto Layout 约束、响应链与手势、列表与集合视图。理解它,你才真正读懂 SwiftUI 之下在发生什么。

开始阅读 难度中级 先修Swift + 并发 约3小时
04.1

命令式心智模型

UIKit 是"你持有视图对象,手动改它的属性"。界面状态由你维护,刷新由你触发。它比 SwiftUI 更啰嗦,但也更可控、调试更直观。

为什么还要学 UIKit

大量存量代码、第三方库、以及 SwiftUI 尚未完全覆盖的能力(如复杂文本排版、某些系统弹窗定制)仍以 UIKit 为主。且 SwiftUI 底层就是构建在 UIKit/AppKit 之上的。懂 UIKit,出问题才能下钻。

04.2

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   // 每次出现刷新
    }
}
04.3

视图与图层

每个 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 会引发离屏渲染,滚动掉帧。列表场景优先用矢量圆角或预渲染图片。

04.4

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) }
优先 Stack View

UIStackView 用排列+间距表达线性布局,内部自动管理约束,既少写代码又天然自适应,是 UIKit 里最接近 SwiftUI 栈的体验。

04.5

响应链与手势

触摸事件沿"响应链"从最上层视图逐级向上传递,直到某个对象处理它。手势识别器(UIGestureRecognizer)是处理点击、滑动、长按的标准方式。

let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
view.addGestureRecognizer(tap)

@objc private func didTap() { print("tapped") }
04.6

列表与集合视图

现代 UIKit 列表用 UICollectionView + UICollectionViewDiffableDataSource,用快照(snapshot)声明"数据现在长什么样",框架自动计算差异动画。

var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
UITableView 还在用吗

单列出场景 UITableView 仍常见且简单;但新项目推荐 UICollectionView Compositional Layout,一套 API 同时覆盖列表与网格,配合 Diffable Data Source 免去手动 reload 的 index 错误。

04.7

与 SwiftUI 混编

在 UIKit 中嵌入 SwiftUI 用 UIHostingController;在 SwiftUI 中嵌入 UIKit 用上一章的 UIViewRepresentable。两者桥接很成熟。

// UIKit 中呈现一个 SwiftUI 视图
let hosting = UIHostingController(rootView: MySwiftUIView())
present(hosting, animated: true)
04.8

常见陷阱与 FAQ

为什么 IBOutlet 是 nil?

在 viewDidLoad 之前访问 storyboard/nib 里的 outlet 会是 nil——视图尚未加载。确保对 outlet 的操作放在 viewDidLoad 及之后。纯代码创建的视图不存在此问题。

Auto Layout 总报歧义约束怎么办?

约束不足以唯一确定布局(缺宽高或位置),或约束互相冲突。优先用 stack view 减少约束数量;调试用 view.debugDescription 或 Xcode 的约束冲突日志定位。

主线程更新 UI 的纪律?

任何 UIKit 操作必须在主线程。后台拿到数据后用 DispatchQueue.main.async 或 @MainActor 切回。在子线程改 UI 会触发随机崩溃或界面错乱。

04.9

实战练习与资源

延伸资源