如何让UIKit分步加载视图时实时更新UI并保持界面响应?
问题描述
在UIKit中创建大量视图时会导致主线程卡顿,需求是实现逐个添加视图→等待界面完成绘制→再添加下一个的流程,保证全程界面可响应。
测试代码如下(创建Swift/Storyboard项目后替换ViewController代码):
import UIKit class ViewController: UIViewController { let stack = UIStackView.Typical override func viewDidLoad() { super.viewDidLoad() _setup() } @objc func load() { stack.removeFullyAll() for _ in 1...2000 { let l = UILabel.Typical l.backgroundColor = .randomSoft l.text = [String](repeating: "string", count: 20).joined(separator: " ") stack.addArrangedSubview(l) } } func _setup() { let scroll = UIScrollView.Typical view.addSubview(scroll) scroll.addSubview(stack) scroll.backgroundColor = .systemPink stack.axis = .vertical stack.spacing = 2 let b = UIButton.Typical view.addSubview(b) b.setImage(.init(systemName: "wand.and.stars"), for: []) b.setTitle(" fill ", for: []) b.backgroundColor = .yellow b.setTitleColor(.label, for: []) b.addTarget(self, action: #selector(load), for: .primaryActionTriggered) NSLayoutConstraint.activate([ scroll.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 50), scroll.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -50), scroll.topAnchor.constraint(equalTo: view.topAnchor, constant: 100), scroll.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100), stack.leftAnchor.constraint(equalTo: scroll.leftAnchor, constant: 2), stack.rightAnchor.constraint(equalTo: scroll.rightAnchor, constant: -2), stack.topAnchor.constraint(equalTo: scroll.topAnchor, constant: 2), stack.bottomAnchor.constraint(equalTo: scroll.bottomAnchor, constant: -2), b.leftAnchor.constraint(equalTo: scroll.leftAnchor), b.bottomAnchor.constraint(equalTo: scroll.topAnchor, constant: -4), ]) } } extension UIView { static var Typical: Self { let v = Self() v.translatesAutoresizingMaskIntoConstraints = false v.backgroundColor = .clear return v } } extension UIColor { static var randomSoft: UIColor { return UIColor(hue: CGFloat(Int.random(in: 0..<20)) / 20.0, saturation: 0.3, brightness: 0.8, alpha: 1) } } extension UIStackView { func removeFully(view: UIView) { removeArrangedSubview(view) view.removeFromSuperview() } func removeFullyAll() { arrangedSubviews.forEach { (view) in removeFully(view: view) } } }
尝试过view.layoutIfNeeded()、RunLoop.main.run(mode: .common, before: Date.now)等方法均无效,延迟添加的演示方式虽能实现效果但存在崩溃风险,需要可靠的解决方案。
为什么
layoutIfNeeded和RunLoop调用无效? UIKit的UI更新流程分为三个核心阶段:
- 布局(Layout):计算视图的frame
- 绘制(Display):生成视图的像素内容
- 提交(Commit):将绘制结果发送到GPU显示
layoutIfNeeded仅能强制立即完成布局计算,但绘制和提交是在当前RunLoop周期的末尾,由CATransaction自动触发的——当前代码块执行完毕前,这两个阶段不会启动。RunLoop.main.run(...)会让RunLoop处理一次事件队列,但此时CATransaction尚未标记需要提交,没有待处理的绘制任务,因此不会触发屏幕刷新。
解决方案
方法1:利用CATransaction完成回调逐次添加
通过CATransaction的完成回调,确保每次视图添加后,等待界面完成绘制再继续添加下一个:
@objc func load() { stack.removeFullyAll() var count = 0 let total = 2000 func addNext() { guard count < total else { return } count += 1 let l = UILabel.Typical l.backgroundColor = .randomSoft l.text = [String](repeating: "string", count: 20).joined(separator: " ") stack.addArrangedSubview(l) // 提交当前事务,绘制完成后触发下一次添加 CATransaction.begin() CATransaction.setCompletionBlock { addNext() } CATransaction.commit() } addNext() }
方法2:分批添加+主线程空闲执行
如果不需要严格逐个添加,分批处理能平衡效率和流畅度,让主线程有时间处理用户事件:
@objc func load() { stack.removeFullyAll() var count = 0 let total = 2000 let batchSize = 20 // 可根据设备性能调整每批数量 func addBatch() { let end = min(count + batchSize, total) for _ in count..<end { let l = UILabel.Typical l.backgroundColor = .randomSoft l.text = [String](repeating: "string", count: 20).joined(separator: " ") stack.addArrangedSubview(l) } count = end if count < total { // 让主线程处理完当前事件后再执行下一批 DispatchQueue.main.async { addBatch() } } } addBatch() }
方法3:与屏幕刷新率同步(CADisplayLink)
绑定屏幕刷新频率,每帧添加少量视图,保证界面全程流畅:
@objc func load() { stack.removeFullyAll() var count = 0 let total = 2000 let perFrame = 5 // 每帧添加的视图数量 let displayLink = CADisplayLink(target: self, selector: #selector(addFrameViews(_:))) displayLink.add(to: .main, forMode: .common) @objc func addFrameViews(_ link: CADisplayLink) { let end = min(count + perFrame, total) for _ in count..<end { let l = UILabel.Typical l.backgroundColor = .randomSoft l.text = [String](repeating: "string", count: 20).joined(separator: " ") stack.addArrangedSubview(l) } count = end if count >= total { link.invalidate() } } }
最优实践:避免创建大量视图
如果实际场景需要展示大量数据,强烈推荐使用UITableView或UICollectionView,它们通过视图复用机制仅创建可见区域的视图,内存和性能效率远高于手动管理数千个独立UIView。即使是自定义布局,也可以基于UICollectionView实现。
内容的提问来源于stack exchange,提问作者Fattie
相关产品推荐
相关产品推荐

