Swift使用Alamofire下载图片赋值给CollectionView的ImageView的乱序与显示异常问题
问题根因定位
两个异常的核心触发逻辑完全来自代码逻辑错位:
- 图片数组顺序错乱:异步网络请求完成顺序完全不可控,使用
append向数组尾部追加图片的方式,会导致谁先下载完成谁排在前面,和接口返回的原始数据索引完全不匹配 - 视图初始化后图片不显示,需滚动才展示:发起所有图片请求后立刻调用
reloadData()时,图片下载任务还在执行,数组里根本没有对应图片数据,所有cell都被赋值为nil;后续图片陆续下载完成写入数组后,没有主动触发对应cell的刷新,只有用户滚动视图触发cell重新复用、重新执行cellForItemAt方法时,才会读到已经下载好的图片 - 额外的隐藏崩溃风险:代码中多处使用强制解包,网络请求失败、图片数据损坏时会直接触发崩溃
针对性修复方案
- 放弃动态append的图片存储方式,提前按照接口返回的游戏列表长度,初始化定长的可选图片数组,数组下标和游戏列表的索引一一对应,从结构上避免顺序错乱
- 拆分刷新时机:接口返回游戏列表文本数据后,先刷新一次列表,把游戏名称这类即时可得的内容展示出来,图片位置先留占位状态
- 单张图片下载完成后,仅刷新对应索引的cell,不需要等所有图片全部下载完成再统一刷新,保证图片加载完成后立刻展示
- cell复用时主动重置图片状态,避免复用逻辑导致的图片错位
- 移除所有强制解包逻辑,网络异常时做降级处理,避免崩溃
优化后可直接运行的代码
// 提前定义定长的可选图片数组,和游戏列表索引一一对应 private var gameImages = [UIImage?]() func fetchData(){ AF.request("https://api.rawg.io/api/games?key=ab904f882bf94fa9960857911498aa78", method: .get, parameters: nil, headers: nil, interceptor: nil) .validate(statusCode: 200 ..< 299) .responseData { [weak self] response in guard let self = self else { return } do { let game = try JSONDecoder().decode(Game.self, from: response.data!) self.games = game guard let gameResults = game.results else { return } // 初始化定长图片数组,长度和游戏列表完全一致 self.gameImages = Array(repeating: nil, count: gameResults.count) // 先刷新一次列表,展示游戏名称文本 DispatchQueue.main.async { self.gamesCollectionView.reloadData() } // 逐个发起图片下载,携带原始索引 for (index, gameItem) in gameResults.enumerated() { guard let imageURL = gameItem.backgroundImage else { continue } AF.request(imageURL, method: .get) .validate(statusCode: 200 ..< 299) .responseData { imageResponse in guard let imageData = imageResponse.data, let image = UIImage(data: imageData) else { return } // 把图片写入对应索引的位置,不做追加操作 self.gameImages[index] = image // 仅刷新对应位置的cell DispatchQueue.main.async { let indexPath = IndexPath(item: index, section: 0) self.gamesCollectionView.reloadItems(at: [indexPath]) } } } } catch let jsonErr { print("接口解析错误: \(jsonErr)") } } } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GameCollectionViewCell.identifier, for: indexPath) as! GameCollectionViewCell cell.backgroundColor = .blue // 先重置cell状态,避免复用错位 cell.imageView.image = nil cell.gameNameLabel.text = games?.results?[indexPath.item].name // 直接读取对应索引位置的图片,没有就留空 cell.imageView.image = gameImages[indexPath.item] return cell }
额外优化提示:生产环境中不建议直接在业务代码里写批量图片下载逻辑,可接入图片加载能力自动做内存缓存、磁盘缓存、cell复用时的请求取消,避免滚动时重复发起图片请求导致的流量浪费
内容的提问来源于stack exchange,提问作者AtakanK
相关产品推荐
相关产品推荐

