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

基于MVVM模式使用AlamofireImage获取图片的最优方案咨询

遵循MVVM原则的AlamofireImage图片加载最优方案

核心思路

把图片下载逻辑抽离到独立的图片服务层,ViewModel只负责状态管理和业务调度,视图层仅负责订阅状态并更新UI,彻底解耦三者的依赖关系,同时避免ViewModel导入UIKit。

具体实现步骤

1. 定义图片服务协议与实现

先通过协议抽象图片下载能力,再基于AlamofireImage实现具体服务,兼顾灵活性与封装性:

import Foundation
import AlamofireImage

// 图片服务协议,ViewModel依赖此抽象协议而非具体实现
protocol ImageServiceType {
    func downloadImage(with url: URL, completion: @escaping (Result<Data, Error>) -> Void)
    // 可选:Combine版本,适配响应式开发
    func downloadImagePublisher(with url: URL) -> AnyPublisher<Data, Error>
}

// 基于AlamofireImage的具体服务实现
class AlamofireImageService: ImageServiceType {
    private let imageDownloader = ImageDownloader.default
    
    func downloadImage(with url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
        let request = URLRequest(url: url)
        imageDownloader.download(request) { response in
            switch response.result {
            case .success(let image):
                // 将UIImage转为Data,避免ViewModel依赖UIKit
                guard let data = image.pngData() ?? image.jpegData(compressionQuality: 0.8) else {
                    completion(.failure(NSError(domain: "ImageConversionError", code: -1, userInfo: nil)))
                    return
                }
                completion(.success(data))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
    // Combine版本实现
    func downloadImagePublisher(with url: URL) -> AnyPublisher<Data, Error> {
        return Future { [weak self] promise in
            guard let self = self else { return }
            let request = URLRequest(url: url)
            self.imageDownloader.download(request) { response in
                switch response.result {
                case .success(let image):
                    guard let data = image.pngData() ?? image.jpegData(compressionQuality: 0.8) else {
                        promise(.failure(NSError(domain: "ImageConversionError", code: -1, userInfo: nil)))
                        return
                    }
                    promise(.success(data))
                case .failure(let error):
                    promise(.failure(error))
                }
            }
        }.eraseToAnyPublisher()
    }
}

2. ViewModel设计:仅管理状态与调度

ViewModel依赖抽象的ImageServiceType,通过发布状态枚举让视图层响应,完全不涉及UIKit类型:

import Foundation
import Combine

// 图片加载状态枚举,覆盖所有业务场景
enum ImageLoadState {
    case idle
    case loading
    case success(Data)
    case failure(Error)
}

class ProductViewModel {
    let imageURL: URL?
    private let imageService: ImageServiceType
    private var cancellables = Set<AnyCancellable>()
    
    // 发布图片加载状态,供视图层订阅
    @Published var imageLoadState: ImageLoadState = .idle
    
    // 构造注入图片服务,便于测试时替换为Mock实现
    init(imageURL: URL?, imageService: ImageServiceType = AlamofireImageService()) {
        self.imageURL = imageURL
        self.imageService = imageService
    }
    
    // 触发图片加载的业务方法
    func loadProductImage() {
        guard let url = imageURL else {
            imageLoadState = .failure(NSError(domain: "InvalidURL", code: -2, userInfo: nil))
            return
        }
        
        imageLoadState = .loading
        
        // 闭包版本调用(二选一)
        imageService.downloadImage(with: url) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let data):
                self.imageLoadState = .success(data)
            case .failure(let error):
                self.imageLoadState = .failure(error)
            }
        }
        
        // Combine版本调用(二选一)
//        imageService.downloadImagePublisher(with: url)
//            .receive(on: DispatchQueue.main)
//            .sink(receiveCompletion: { [weak self] completion in
//                if case .failure(let error) = completion {
//                    self?.imageLoadState = .failure(error)
//                }
//            }, receiveValue: { [weak self] data in
//                self?.imageLoadState = .success(data)
//            })
//            .store(in: &cancellables)
    }
}

3. 视图层实现:仅订阅状态更新

视图层(UIViewController/SwiftUI View)只需要订阅ViewModel的状态,根据状态更新UI,不包含任何下载逻辑:

import UIKit
import Combine

class ProductViewController: UIViewController {
    @IBOutlet weak var productImageView: UIImageView!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
    @IBOutlet weak var errorLabel: UILabel!
    
    var viewModel: ProductViewModel!
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
        viewModel.loadProductImage()
    }
    
    private func setupBindings() {
        viewModel.$imageLoadState
            .receive(on: DispatchQueue.main)
            .sink { [weak self] state in
                self?.handleImageLoadState(state)
            }
            .store(in: &cancellables)
    }
    
    private func handleImageLoadState(_ state: ImageLoadState) {
        switch state {
        case .idle:
            loadingIndicator.isHidden = true
            errorLabel.isHidden = true
            productImageView.image = nil
        case .loading:
            loadingIndicator.isHidden = false
            loadingIndicator.startAnimating()
            errorLabel.isHidden = true
            productImageView.image = nil
        case .success(let data):
            loadingIndicator.stopAnimating()
            loadingIndicator.isHidden = true
            errorLabel.isHidden = true
            productImageView.image = UIImage(data: data)
        case .failure(let error):
            loadingIndicator.stopAnimating()
            loadingIndicator.isHidden = true
            errorLabel.isHidden = false
            errorLabel.text = error.localizedDescription
            productImageView.image = UIImage(named: "placeholder_error")
        }
    }
}

方案优势

  • 严格遵循MVVM:ViewModel不依赖UIKit,仅负责状态与业务调度;视图层只做UI渲染;下载逻辑完全封装在服务层。
  • 可测试性强:ViewModel依赖抽象协议,测试时可以Mock ImageServiceType,无需真实网络请求。
  • 复用性高:ImageService可以在整个项目中复用,统一管理图片下载、缓存逻辑。
  • 状态覆盖全面:通过枚举状态统一管理加载、成功、失败、闲置场景,避免零散的状态变量。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 06:30:39