SwiftUI观察数组内ObservableObject变更未触发视图刷新问题求解
问题分析
实现图片持有者类时,初始化会自动加载缩略图,也支持后续下载操作。当前异常表现为:图片加载完成后视图不会同步更新,第二次点击加载按钮时才会显示第一次加载好的图片。
现有代码
图片持有者类
class MyImage: ObservableObject { private static let sessionProcessingQueue = DispatchQueue(label: "SessionProcessingQueue") @Published var thumbnail: UIImage? @Published var loaded: Bool var fullName: String { "\(folderName)/\(fileName)" } var onThumbnailSet: ((UIImage?) -> Void) private var folderName: String private var fileName: String private var cancelableThumbnail: AnyCancellable? private var thumbnailUrl: URL? { return URL(string: "\(BASE_URL)/thumbnail/\(fullName)") } private var downloadUrl: URL? { return URL(string: "\(BASE_URL)/download/\(fullName)") } init(folderName: String, fileName: String) { self.folderName = folderName self.fileName = fileName self.loaded = false self.loadThumbnail() } private func loadThumbnail() { guard let requestUrl = thumbnailUrl else { fatalError() } self.cancelableThumbnail = URLSession.shared.dataTaskPublisher(for: requestUrl) .subscribe(on: Self.sessionProcessingQueue) .map { UIImage(data: $0.data) } .receive(on: DispatchQueue.main) .sink(receiveCompletion: { (suscriberCompletion) in switch suscriberCompletion { case .finished: break case .failure(let error): print(error.localizedDescription) } }, receiveValue: { [weak self] (value) in self?.objectWillChange.send() self?.loaded.toggle() self?.thumbnail = value }) } }
视图代码
struct MyView: View { @ObservedObject var imagesHolder: ImagesHolder = ImagesHolder() var body: some View { VStack { Button(action: { self.loadImages() }, label: { Text("Load images") }) ForEach(imagesHolder.images, id: \.self) { image in if image.loaded { Image(uiImage: image.thumbnail!) .frame(width: 600, height: 600) } else { Text("Not loaded") } } } } private func loadImages() -> Void { loadMediaList( onLoadDone: { myImages in myImages.forEach { image in imagesHolder.append(image) } } ) } }
图片数组存储类
class ImagesHolder: ObservableObject { @Published var images: [MyImage] = [] func append(_ myImage: MyImage) { objectWillChange.send() images.append(myImage) } }
数据加载器代码
func loadMediaList(onLoadDone: @escaping (([MyImage]) -> Void)) -> AnyCancellable { let url = URL(string: "\(BASE_URL)/medias") guard let requestUrl = url else { fatalError() } return URLSession.shared.dataTaskPublisher(for: requestUrl) .subscribe(on: MyImage.sessionProcessingQueue) .map { parseJSON(data: $0.data) } .receive(on: DispatchQueue.main) .sink(receiveCompletion: { (suscriberCompletion) in switch suscriberCompletion { case .finished: break case .failure(let error): print(error.localizedDescription) } }, receiveValue: { images in onLoadDone(images); }) }
根因说明
这是SwiftUI中嵌套ObservableObject观察失效的典型场景:
- 外层观察对象
ImagesHolder的@Published仅能监听到images数组本身的变化(比如添加、删除、替换元素),无法监听到数组内部MyImage实例的属性变化 - 第一次点击加载后,
MyImage实例的loaded、thumbnail属性更新,但外层ImagesHolder不会收到变更通知,所以视图不会刷新 - 第二次点击加载按钮时,新元素加入数组触发
ImagesHolder变更,视图重新渲染时才会读取到之前已经更新完成的MyImage属性,所以才会显示第一次加载的图片
修复方案
1. 抽离独立的图片子视图(最优方案)
把ForEach内的图片展示逻辑抽成单独的子视图,让子视图自己持有@ObservedObject修饰的MyImage实例,即可监听到单个图片实例的属性变化:
// 新增子视图 struct ImageItemView: View { @ObservedObject var image: MyImage var body: some View { if image.loaded { Image(uiImage: image.thumbnail!) .frame(width: 600, height: 600) } else { Text("Not loaded") } } } // 修改原MyView的ForEach部分 ForEach(imagesHolder.images, id: \.self) { image in ImageItemView(image: image) }
2. 清理冗余的手动通知代码
@Published属性会自动在值变更前发送objectWillChange通知,不需要手动调用,删除多余代码避免异常:
- 删除
MyImage中receiveValue闭包里的self?.objectWillChange.send() - 删除
ImagesHolder中append方法里的objectWillChange.send()
3. 修复全局函数的队列引用问题
loadMediaList是全局函数,没有Self上下文,把subscribe(on: Self.sessionProcessingQueue)改成subscribe(on: MyImage.sessionProcessingQueue),避免编译错误。
内容的提问来源于stack exchange,提问作者Majid
相关产品推荐
相关产品推荐

