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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 21:45:00