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 }
问题原因与修复方案
核心原因
- 视图身份丢失:
isLoading状态切换时,if-else会销毁旧视图(包括WrappedView对应的UIKit实例)并重建新视图,导致之前的状态全部重置。 - 重复操作与线程冗余:
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
相关产品推荐
相关产品推荐

