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

Swift中如何实现异步API调用并解决UICollectionView滚动卡顿问题

卡顿问题修复方案

现有代码核心问题

  • ApiManager使用静态变量存储请求参数,存在线程安全问题;你在循环遍历分类时仅覆盖了url参数,最终只发起了1次「all分类1000条数据」的请求,没有拿到4个分类的全部数据
  • 一次性加载1000条全量数据后直接调用reloadData,会触发UICollectionView一次性计算所有cell的布局、渲染内容,主线程被阻塞导致卡顿
  • 没有做数据分页和cell高度预适配,滚动时每次都要重新计算label的文本高度,加重主线程负担

修复步骤

1. 重构ApiManager,避免静态参数问题

将请求参数改为方法入参传递,移除全局静态可变参数,解决参数覆盖、线程不安全的问题:

class ApiManager {
    static let session = URLSession.shared
    static let commonHeader = ["x-rapidapi-host": "famous-quotes4.p.rapidapi.com", "x-rapidapi-key": "SECRET_API_KEY" ]
    
    static func getTask(category: String, count: Int = 50, itemName: String, completion:@escaping ([String]) -> Void) -> URLSessionDataTask {
        let urlStr = "https://famous-quotes4.p.rapidapi.com/random?category=\(category)&count=\(count)"
        guard let url = URL(string: urlStr) else {
            completion([])
            return URLSessionDataTask()
        }
        
        var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
        request.allHTTPHeaderFields = Self.commonHeader
        request.httpMethod = "GET"
        
        let dataTask = session.dataTask(with: request) { data, response, error in
            guard error == nil, let data = data else {
                completion([])
                return
            }
            do {
                guard let dicArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String:Any]] else {
                    completion([])
                    return
                }
                let itemArray = dicArray.compactMap { $0[itemName] as? String }
                completion(itemArray)
            } catch {
                completion([])
            }
        }
        return dataTask
    }
}

2. 改为分页加载+增量更新,避免全量刷新

不要一次性请求全量数据,改为每次滚动到底部时加载下一页,每次加载20-50条即可;更新数据时使用insertItems增量更新,不要全量调用reloadData:

class ViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    var quoteArray = [String]()
    var categories = ["love", "motivational", "mothersday", "all"]
    var currentCategoryIndex = 0
    var isLoading = false // 防止重复发起请求
    
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
        // 开启自动尺寸适配,设置预估高度
        if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        }
        // 加载第一页数据
        loadNextPage()
    }
    
    // 加载下一页数据
    func loadNextPage() {
        guard !isLoading, currentCategoryIndex < categories.count else { return }
        isLoading = true
        let currentCategory = categories[currentCategoryIndex]
        ApiManager.getTask(category: currentCategory, count: 50, itemName: "text") { [weak self] items in
            guard let self = self else { return }
            DispatchQueue.main.async {
                let startRow = self.quoteArray.count
                self.quoteArray.append(contentsOf: items)
                // 生成需要插入的indexPath数组
                let indexPaths = (0..<items.count).map { IndexPath(row: startRow + $0, section: 0) }
                // 增量插入cell,不要全量刷新
                self.collectionView.performBatchUpdates {
                    self.collectionView.insertItems(at: indexPaths)
                } completion: { _ in
                    self.isLoading = false
                    self.currentCategoryIndex += 1
                }
            }
        }.resume()
    }
}

3. 实现滚动监听,触发自动分页

通过UIScrollViewDelegate回调判断滚动位置,快到底部时自动加载下一页:

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        let frameHeight = scrollView.frame.height
        // 距离底部还有200px时触发下一页加载
        if offsetY > (contentHeight - frameHeight - 200), !isLoading {
            loadNextPage()
        }
    }
}

4. 优化Cell布局,避免重复计算

给Cell内的label固定最大宽度,减少滚动时的布局计算开销:

class Cell: UICollectionViewCell {
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(label)
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
            // 固定label最大宽度,避免自动布局重复计算
            label.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 32)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// cellForItemAt保持原有逻辑即可
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { fatalError("cell dequeue error") }
    cell.label.text = self.quoteArray[indexPath.row]
    return cell
}

内容的提问来源于stack exchange,提问作者ataberkturan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 09:15:03