本节摘要:UIKit 用一串按时间报到的回调管理界面的完整一生:loadView 建视图树、viewDidLoad 备料、viewWillAppear 上妆、viewDidAppear 开演、viewWillDisappear 谢幕、deinit 收工。本节给出全时序总表与各站职责边界,并示范定时器与通知的清理时机。结论先行:UIKit 的生命周期回调是"控制权与责任并存"的时刻表,理解每站的次数与时机,才能避免重复加载与泄漏。
SwiftUI 把生命周期浓缩成两个时刻(7.1 节),UIKit 则摊开成一张完整时刻表——本节的剧场终于有名有姓的报幕员。
import UIKit final class BoardViewController: UIViewController { var showtimes: [String] = [] // 第一站:加载视图树(几乎总是交给系统,别手写) override func loadView() { super.loadView() // 系统创建默认视图树 print("1 loadView:视图树出生") } // 第二站:一生只来一次,备料主场 override func viewDidLoad() { super.viewDidLoad() print("2 viewDidLoad:配置一次性资源") title = "今日排片" showtimes = ["雾中灯塔 19:00", "星际列车 20:30"] } // 第三站:每次将要上屏(含从返回导航回来) override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) print("3 viewWillAppear:上妆,刷新动态数据") } // 第四站:已上屏,开演(启动定时器、发起首屏请求的惯用站) override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("4 viewDidAppear:开演,启动刷新定时器") } // 第五站:将要离屏 override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) print("5 viewWillDisappear:卸妆,保存现场") } // 第六站:已离屏 override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) print("6 viewDidDisappear:暂停动画与轮询") } deinit { print("7 deinit:对象谢幕,账本清零(第4章)") // ARC 账本在此收尾 } }

⚠️ 常见坑一:把网络请求塞进 viewWillAppear——用户每次从下级页返回都会重新触发,列表闪回顶部、流量翻倍。坑二:定时器与通知观察者在 deinit 里才拆——页面被压栈后台时轮询仍在跑。正确姿势是成对出现在 appear/disappear。
把七站按"一次性 / 每次上屏 / 持续运行"三层分派,是 UIKit 工程的惯用法:
final class TimedBoardViewController: UIViewController { private var pollTimer: Timer? // 持续消耗品 override func viewDidLoad() { super.viewDidLoad() title = "余票实时板" // 一次性:界面骨架与数据源注册 } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startPolling() // 开演才轮询 } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) stopPolling() // 离屏即停 } private func startPolling() { guard pollTimer == nil else { return } // 防重复启动 pollTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in self?.refreshSeats() // [weak self]:第4.2 节的保命符 } } private func stopPolling() { pollTimer?.invalidate() // 定时器不拆就泄漏(还会拉着 self) pollTimer = nil } private func refreshSeats() { print("轮询:余票已刷新 \(Date())") } deinit { stopPolling() // 兜底:万一没走 viewDidDisappear print("TimedBoard 已释放") } }
对照 7.1 节:SwiftUI 里这件事只需要 onAppear 里 Task、onDisappear 里 cancel 两行——UIKit 版本样板多出数倍,但你精确控制了"动画结束才开轮询""压栈后台立刻停"这类时序,复杂交互里这份控制权正是价值所在。
背景:从影院列表页推入排片页再返回,观测两页回调的交错顺序。操作(示意,导航由用户点击触发):
// 列表页推入排片页时,回调时序为: // 排片页 loadView → viewDidLoad // 排片页 viewWillAppear → 列表页 viewWillDisappear // 排片页 viewDidAppear → 列表页 viewDidDisappear // 返回时反向再来一轮:排片页 viewWillDisappear → 列表页 viewWillAppear …
结果解读:转场期间新旧两页的回调是交错报到的(新页 willAppear 在旧页 willDisappear 前后依系统版本略有差异,但都在转场动画区间内)。这解释了两个惯用法的由来:传参给下一页要在 push 之前(下一页 viewDidLoad 早于你Disappear);返回时回传数据用委托或闭包回调(你的 viewWillAppear 晚于子页的 viewWillDisappear)。变式:把刷新逻辑放 viewWillAppear 并打印时间戳,反复进出页面几次,观察它被触发的次数——这就是坑一的现场证据。
💡 关键直觉:把七站时刻表想象成剧场的一天——loadView 搭台、viewDidLoad 摆道具(只摆一次)、viewWillAppear 演员候场、viewDidAppear 大幕拉开、viewWillDisappear 大幕合上、viewDidDisappear 熄灯、deinit 拆台。同一个界面被多次"候场—合幕",但台只搭一次、只拆一次。
界面的一生还有最后一块拼图:它脚下踩着的地基——Foundation 的日期、数据与格式化,以及上一代主演 Objective-C 的同台规则。下一节进后台库房。