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
相关产品推荐
相关产品推荐

