如何在推入UINavigationController前预加载控制器并管控生命周期方法
问题场景
我正在重构一款遗留iOS应用,此前使用自定义推入视图控制器动画,被“推入”的控制器会在屏外完成加载/显示后再移入屏幕。现在需求改为使用UINavigationController,应用可正常运行,但首次推入主控制器的视图控制器加载并显示内容时会有明显的几秒延迟,而此前这段时间被动画掩盖。
核心疑问:
- 是否可以在推入前将控制器加载到内存(并调用
viewDidLoad),并在动画开始时调用viewDidAppear? - 由于所用控制器集合固定,是否可在UINavigationController显示时一次性全部预加载?
(注:所有操作均通过代码实现,无故事板)
控制器基类代码
class CPProgress: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = ColorTheme.bgFFF000 setupView() } let progress: UIProgressView = { let ctrl = UIProgressView() // some looks stuff here return ctrl }() let btnContinue: UIButton = { let ctrl = UIButton() ctrl.backgroundColor = ColorTheme.text04F ctrl.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium) ctrl.setTitleColor(UIColor.white, for: .normal) ctrl.setTitle(NSLocalizedString("survey_next_step_button_caption", comment: ""), for: .normal) ctrl.layer.cornerRadius = 10 ctrl.translatesAutoresizingMaskIntoConstraints = false return ctrl }() override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) rotate(isPortrait: Orientation.isPortrait(size: size)) // 处理横竖屏布局切换 } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) rotate(isPortrait: UIViewController.layoutShouldBePortrait()) // 初始化横竖屏布局 self.navigationItem.title = Constants.sampleTitle } func setupView() { self.navigationItem.hidesBackButton = true let item = UIBarButtonItem(customView: self.close) self.navigationItem.setLeftBarButton(item, animated: false) self.navigationItem.title = NSLocalizedString("scale_title", comment: "") } }
所有派生类均重写了setupView(),包含布局常量等内容;viewDidAppear()也被重写,包含数据计算、控件图片设置以及UIScrollView中UIImageView的居中操作,且每次显示时数据可能不同。
解决方案
一、推入前预加载控制器并控制生命周期时机
完全可以实现,核心是手动触发视图加载并控制viewDidAppear的执行逻辑:
提前触发
viewDidLoad
UIViewController的view属性被访问时会自动触发viewDidLoad。初始化目标控制器后,主动访问controller.view即可强制加载视图,执行viewDidLoad和setupView:let targetVC = YourDerivedCPProgressController() // 主动访问view,触发viewDidLoad及setupView执行 _ = targetVC.view这样推入前,控制器的视图布局、导航栏设置都已完成,避免推入时的延迟。
控制
viewDidAppear的调用时机
系统默认会在推入完成后调用viewDidAppear,你可以通过开关变量控制逻辑执行时机:- 在派生类中添加控制变量,重写
viewDidAppear:class YourDerivedVC: CPProgress { var shouldRunAppearLogic = false override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard shouldRunAppearLogic else { return } // 原有viewDidAppear中的数据计算、图片设置等逻辑 } } - 推入前设置开关,或者在动画回调中手动触发:
// 预加载完成后准备推入 targetVC.shouldRunAppearLogic = true navigationController?.pushViewController(targetVC, animated: true) // 或者和动画同步触发 navigationController?.pushViewController(targetVC, animated: true) { targetVC.shouldRunAppearLogic = true targetVC.viewDidAppear(true) }
注意:要避免逻辑重复执行,若系统自动调用和手动调用冲突,可在执行后重置开关。
- 在派生类中添加控制变量,重写
二、一次性预加载所有固定控制器的可行性
由于控制器集合固定,完全可以在UINavigationController显示时批量预加载,但需注意以下问题:
内存占用评估
若控制器数量多或包含大量资源(如高清图片),批量预加载会增加内存消耗,需先评估单个控制器的内存 footprint,确保不会触发内存警告。预加载时机与实现
可在根控制器的viewDidAppear中异步执行预加载,避免阻塞主线程:class RootVC: UIViewController { // 存储预加载的控制器实例 var preloadedControllers: [CPProgress] = [] override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // 后台异步预加载 DispatchQueue.global().async { [weak self] in // 列出所有需要预加载的控制器类型 let controllerTypes: [CPProgress.Type] = [VC1.self, VC2.self, VC3.self] let controllers = controllerTypes.map { type in let vc = type.init() // 触发viewDidLoad _ = vc.view return vc } // 回到主线程保存实例 DispatchQueue.main.async { self?.preloadedControllers = controllers } } } }需要推入时,直接从
preloadedControllers中取出对应实例即可。动态数据更新处理
由于派生类viewDidAppear包含每次显示可能变化的逻辑,预加载时不要在viewDidLoad中处理动态数据,而是在推入前刷新:// 取出预加载的控制器 guard let targetVC = preloadedControllers.first(where: { $0 is VC1 }) as? VC1 else { return } // 刷新动态数据 targetVC.refreshDynamicData() // 自定义方法,更新数据、图片等 navigationController?.pushViewController(targetVC, animated: true)
针对基类的适配说明
- 基类
setupView在viewDidLoad中调用,预加载时会自动执行,符合你的布局需求。 - 基类
viewWillAppear中的布局旋转逻辑,只有在控制器显示时才会被系统调用,预加载阶段不会执行,无需额外处理。 - 确保派生类的
viewDidAppear逻辑可重复执行,因为预加载的控制器实例可能被多次推入。
内容的提问来源于stack exchange,提问作者Afinainflowers

