You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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更新流程分为三个核心阶段:

  1. 布局(Layout):计算视图的frame
  2. 绘制(Display):生成视图的像素内容
  3. 提交(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 21:54:55