如何从JSON URL下载商品数据及图片并在CollectionViewCell展示
问题描述
我有一个存储「商品分类:商品数组」结构的JSON接口,需要下载商品数组,同时下载每个商品对应的图片(图片链接在JSON里),最后把商品名称、价格、分类、图片展示在CollectionViewCell中。已经折腾3天了,之前也提问过一次,现在完全搞不清什么时候请求商品数据、后续怎么下载图片,写了ViewModel和NetworkHelper代码,但在CollectionView加载时一直卡壳。
我写的代码
ViewModel代码
public class ShopModel: Codable { private let shopItem: ShopItem init(item: ShopItem) { self.shopItem = item } public var category: String { shopItem.category! } public var name: String { shopItem.name! } public var price: Int { Int(shopItem.price!) } var image: UIImage { guard let imageURL = URL(string: shopItem.image_url!) else { fatalError("didnt find imageURL") } let task = URLSession.shared.dataTask(with: imageURL) { [weak self] data, response, error in guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode), let self = self else { fatalError() } guard let data = data else { fatalError() } guard let downloadedImage = UIImage(data: data) else { fatalError("didnt load image") } DispatchQueue.main.async { self.image = downloadedImage } } task.resume() } } struct ShopItem: Codable { let category: String? let name: String? let price: Double? let image_url: String? }
NetworkHelper代码
var items: [ShopItem] = [] let latestURL = URL(string: "https://run.mocky.io/v3/cc0071a1-f06e-48fa-9e90-b1c2a61eaca7") let flashSaleURL = URL(string: "https://run.mocky.io/v3/a9ceeb6e-416d-4352-bde6-2203416576ac") let urlSession = URLSession.shared func downloadLatest(_ completion: () -> Void) { guard let latestURL = latestURL else { fatalError() } let task = urlSession.dataTask(with: latestURL) { [weak self] data, response, error in guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode), let self = self else { fatalError() } guard let data = data else { fatalError() } if let results = try? JSONDecoder().decode(ShopModel.self, from: data) { self.items = results.shopItems ?? [ShopItem]() } else { fatalError("hello idiot") } } task.resume() // completion() }
代码问题与修正方案
1. 核心问题梳理
- 数据模型结构不匹配:接口返回的是商品数组,但你用
ShopModel解码,且ShopModel无对应数组属性,直接导致解码失败 - 图片加载逻辑错误:
image属性是同步UIImage类型,却在内部发起异步请求,还试图给自身赋值,不仅无法更新数据,还会重复发起请求 - 网络回调缺失:
downloadLatest的completion未在请求完成后调用,无法触发CollectionView刷新
2. 修正后的完整代码
数据模型与ViewModel
// 对应JSON的商品结构 struct ShopItem: Codable { let category: String? let name: String? let price: Double? let image_url: String? } // ViewModel:包装商品数据+处理图片加载 class ShopItemViewModel { private let item: ShopItem private(set) var image: UIImage? // 图片加载完成回调,用于刷新Cell var onImageLoaded: (() -> Void)? init(item: ShopItem) { self.item = item } // 安全获取属性,避免强制解包崩溃 var category: String { item.category ?? "未知分类" } var name: String { item.name ?? "未知名称" } var price: String { String(format: "¥%.2f", item.price ?? 0) } // 发起图片加载请求 func loadImage() { guard let imageURLString = item.image_url, let imageURL = URL(string: imageURLString) else { return } URLSession.shared.dataTask(with: imageURL) { [weak self] data, response, error in guard let self = self, error == nil, let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode), let data = data, let downloadedImage = UIImage(data: data) else { return } DispatchQueue.main.async { self.image = downloadedImage self.onImageLoaded?() } }.resume() } }
网络请求工具类
class NetworkHelper { static let shared = NetworkHelper() private let urlSession = URLSession.shared // 下载商品数据,用Result回调返回结果 func downloadShopItems(from url: URL, completion: @escaping (Result<[ShopItem], Error>) -> Void) { urlSession.dataTask(with: url) { data, response, error in if let error = error { DispatchQueue.main.async { completion(.failure(error)) } return } guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode), let data = data else { let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "请求失败"]) DispatchQueue.main.async { completion(.failure(error)) } return } do { // 接口返回直接是商品数组,解码成[ShopItem] let items = try JSONDecoder().decode([ShopItem].self, from: data) DispatchQueue.main.async { completion(.success(items)) } } catch { DispatchQueue.main.async { completion(.failure(error)) } } }.resume() } }
CollectionView使用示例
class ShopCollectionViewController: UICollectionViewController { private var viewModels: [ShopItemViewModel] = [] override func viewDidLoad() { super.viewDidLoad() collectionView.register(ShopItemCell.self, forCellWithReuseIdentifier: "ShopItemCell") loadShopData() } private func loadShopData() { guard let url = URL(string: "https://run.mocky.io/v3/cc0071a1-f06e-48fa-9e90-b1c2a61eaca7") else { return } NetworkHelper.shared.downloadShopItems(from: url) { [weak self] result in guard let self = self else { return } switch result { case .success(let items): self.viewModels = items.map { ShopItemViewModel(item: $0) } self.collectionView.reloadData() case .failure(let error): print("数据加载失败:\(error.localizedDescription)") } } } // MARK: - CollectionView DataSource override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { viewModels.count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ShopItemCell", for: indexPath) as! ShopItemCell let viewModel = viewModels[indexPath.item] // 设置基础数据 cell.categoryLabel.text = viewModel.category cell.nameLabel.text = viewModel.name cell.priceLabel.text = viewModel.price // 设置图片 if let image = viewModel.image { cell.itemImageView.image = image } else { // 占位图 cell.itemImageView.image = UIImage(named: "placeholder") // 加载图片,完成后刷新当前Cell viewModel.onImageLoaded = { [weak self] in self?.collectionView.reloadItems(at: [indexPath]) } viewModel.loadImage() } return cell } } // 自定义Cell示例 class ShopItemCell: UICollectionViewCell { let itemImageView = UIImageView() let categoryLabel = UILabel() let nameLabel = UILabel() let priceLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) // 配置子视图 itemImageView.contentMode = .scaleAspectFill itemImageView.clipsToBounds = true contentView.addSubview(itemImageView) contentView.addSubview(categoryLabel) contentView.addSubview(nameLabel) contentView.addSubview(priceLabel) // 此处添加AutoLayout布局代码,根据需求自行实现 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
关键注意事项
- 禁止强制解包:用
??提供默认值,避免nil导致崩溃 - 图片缓存优化:可以加入
NSCache存储已下载图片,减少重复请求 - 线程规范:所有UI操作必须在主线程执行
- 回调时机:网络请求完成后必须切回主线程刷新CollectionView
内容的提问来源于stack exchange,提问作者максим мартынов
相关产品推荐
相关产品推荐

