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

