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

如何从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,提问作者максим мартынов

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 23:27:56