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

SwiftUI中用ViewModel的@Published更新UIViewRepresentable视图异常

问题分析与解决

问题场景

将UIKit组件HorizontalComponent包装为SwiftUI可用视图,需求是初始化传入空白UIImage(),后续通过Kingfisher异步下载网络图片更新。首次显示视图时一切正常,但当ViewModel触发SwiftUI状态更新时,makeUIView和updateUIView均被调用,视图却重置为初始化状态,updateUIView中的修改(如隐藏指定视图)未生效,尽管该方法确实被执行。

相关代码

包装视图代码(WrappedView)

struct WrappedView: UIViewRepresentable {
    var viewModel: Model
    
    func makeUIView(context: Context) -> HorizontalComponent {
        HorizontalComponent(image: UIImage(), title: viewModel.title, subtitle: viewModel.subtitle)
    }
    
    func updateUIView(_ uiView: HorizontalComponent, context: Context) {
            KingfisherManager.shared.retrieveImage(with: URL(string: "someImageUrl"), completionHandler: { result in
                switch result {
                case .success(let value):
                    DispatchQueue.main.async {
                        uiView.setImage(value.image)
                    }
                case .failure(let error):
                    print("Error fetching image")
                }
            })
        DispatchQueue.main.async {
            uiView.setTitle(viewModel.title)
            uiView.setSubtitle(viewModel.subtitle)
            uiView.roundedImageView.isHidden = true
            uiView.circularImageView.isHidden = true
        }
    }
}

使用包装视图的SwiftUI视图(LandView)

struct LandView: View {
   private let models: [Model]
   private let isLoading: Bool

   init(models: [Model],
     isLoading: Bool) {
       self.models = models
       self.isLoading = isLoading
   }

var body: some View {
    VStack {
        if isLoading {
           ShimmerView()
        } else { 
          WrappedView(viewModel: models[0])
        }
    }
 }
}

父视图(HomeView)

struct HomeView: View {
@ObservedObject var viewModel: ViewModel

var body: some View {
    VStack {
      LandView(models: viewModel.cards, isLoading: viewModel.isLoading)
    }
}
}

ViewModel代码

class ViewModel: ObservableObject {
   @Published private(set) var cards: [Model] = []
   @Published private(set) var isLoading: Bool = true
}

问题原因与修复方案

核心原因

  1. 视图身份丢失:isLoading状态切换时,if-else会销毁旧视图(包括WrappedView对应的UIKit实例)并重建新视图,导致之前的状态全部重置。
  2. 重复操作与线程冗余:updateUIView本身在主线程执行,无需额外包裹DispatchQueue.main.async;且每次update都会重复发起图片请求、执行UI修改,视图重建后这些操作会失效。

具体修复步骤

1. 保持视图身份一致性

用透明度切换替代if-else,避免视图销毁重建:

struct LandView: View {
   private let models: [Model]
   private let isLoading: Bool

   init(models: [Model], isLoading: Bool) {
       self.models = models
       self.isLoading = isLoading
   }

var body: some View {
    VStack {
        WrappedView(viewModel: models[0])
            .opacity(isLoading ? 0 : 1)
        ShimmerView()
            .opacity(isLoading ? 1 : 0)
    }
 }
}

2. 优化WrappedView逻辑

  • 初始化时就设置视图隐藏状态,避免重复操作
  • 仅当数据变化时才执行更新,避免重复请求与无效操作
  • 用Coordinator记录请求状态,防止重复发起图片请求
struct WrappedView: UIViewRepresentable {
    let viewModel: Model
    
    func makeUIView(context: Context) -> HorizontalComponent {
        let view = HorizontalComponent(image: UIImage(), title: viewModel.title, subtitle: viewModel.subtitle)
        // 初始化时一次性设置隐藏状态
        view.roundedImageView.isHidden = true
        view.circularImageView.isHidden = true
        return view
    }
    
    func updateUIView(_ uiView: HorizontalComponent, context: Context) {
        // 仅当标题变化时更新
        if uiView.title != viewModel.title {
            uiView.setTitle(viewModel.title)
        }
        // 仅当副标题变化时更新
        if uiView.subtitle != viewModel.subtitle {
            uiView.setSubtitle(viewModel.subtitle)
        }
        
        // 假设Model新增imageUrl字段,仅当URL变化时发起请求
        guard let url = URL(string: viewModel.imageUrl), 
              context.coordinator.lastRequestedUrl != url else { return }
        
        context.coordinator.lastRequestedUrl = url
        KingfisherManager.shared.retrieveImage(with: url) { result in
            switch result {
            case .success(let value):
                uiView.setImage(value.image)
            case .failure:
                print("Error fetching image")
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator {
        var lastRequestedUrl: URL?
    }
}

3. 确保Model的可识别性

给Model添加Identifiable协议,让SwiftUI能正确复用视图实例:

struct Model: Identifiable {
    let id = UUID()
    let title: String
    let subtitle: String
    let imageUrl: String
    // 其他属性
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 13:56:04