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

SwiftUI使用Combine加载App内图片时卡在进度视图问题排查

故障原因与修复方案

核心问题汇总

  • 首次渲染ApodImageView时,vm.apodData还未加载完成,你传入了url和thumbnailUrl都为空的默认ApodModel,而@StateObject仅会在视图首次创建时初始化一次,后续父视图传入的apodData更新不会触发ViewModel重新初始化,导致始终请求无效空链接,不会返回任何结果
  • ViewModel初始化时序不合理,先触发图片请求再添加订阅,可能错过请求返回的回调
  • 缺少网络请求失败兼容逻辑,请求出错后加载状态不会自动结束,永久卡住进度条

修复代码

1. 调整图片网络层代码

新增主动触发请求的方法,添加错误兼容逻辑:

import Foundation
import SwiftUI
import Combine

class ApodImageService: ObservableObject {
    
    @Published var image: UIImage?
    
    private var imageSubscription: AnyCancellable?
    private var apodData: ApodModel
    
    init(apodData: ApodModel) {
        self.apodData = apodData
    }
    
    // 新增公开请求方法,外部订阅完成后再调用
    func fetchImage() {
        guard let url = URL(string: apodData.thumbnailUrl ?? apodData.url) else {
            // 无效URL直接返回空,结束加载状态
            image = nil
            return
        }
        imageSubscription = NetworkingManager.download(url: url)
            .tryMap({ data -> UIImage? in
                return UIImage(data: data)
            })
            .catch { error -> Just<UIImage?> in
                // 捕获请求错误,返回空,保证订阅能收到回调
                print("图片加载失败: \(error.localizedDescription)")
                return Just(nil)
            }
            .sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedImage in
                self?.image = returnedImage
                self?.imageSubscription?.cancel()
            })
    }
    
    // 新增数据更新方法,外部传入新的ApodModel时调用
    func updateApodData(_ newData: ApodModel) {
        imageSubscription?.cancel()
        image = nil
        apodData = newData
        fetchImage()
    }
}

2. 调整图片ViewModel代码

修正初始化时序,新增数据更新入口:

import Foundation
import SwiftUI
import Combine

class ApodImageViewModel: ObservableObject {
    
    @Published var image: UIImage?
    @Published var isLoading: Bool = false
    
    private let dataService: ApodImageService
    private var cancellables = Set<AnyCancellable>()
    
    init(apodData: ApodModel) {
        self.dataService = ApodImageService(apodData: apodData)
        self.isLoading = true
        addSubscribers()
        self.dataService.fetchImage()
    }
    
    private func addSubscribers() {
        dataService.$image
            .sink { [weak self] returnedImage in
                self?.isLoading = false
                self?.image = returnedImage
            }
            .store(in: &cancellables)
    }
    
    // 对外暴露数据更新方法
    func updateApodData(_ newData: ApodModel) {
        isLoading = true
        dataService.updateApodData(newData)
    }
}

3. 调整图片视图代码

监听apodData变化,自动触发重新加载:

import SwiftUI

struct ApodImageView: View {
    
    @StateObject var vm: ApodImageViewModel
    private let apodData: ApodModel
    
    init(apodData: ApodModel) {
        self.apodData = apodData
        _vm = StateObject(wrappedValue: ApodImageViewModel(apodData: apodData))
    }
    
    var body: some View {
        ZStack {
            if let image = vm.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            } else if vm.isLoading {
                ProgressView()
            } else {
                Image(systemName: "questionmark")
                    .foregroundColor(Color.theme.secondaryText)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: 250)
        .cornerRadius(10)
        // 监听apodData变化,更新ViewModel数据
        .onChange(of: apodData) { newData in
            vm.updateApodData(newData)
        }
    }
}

可选优化

如果你的ApodModel没有实现Equatable,onChange无法正常监听,可以等主视图的vm.apodData加载完成后再渲染ApodImageView,避免首次传入空模型:

// 主视图中替换ApodImageView的初始化代码
if let apodData = vm.apodData {
    ApodImageView(apodData: apodData)
} else {
    ProgressView()
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 09:15:01