0

I'm having an Image holder that would load the thumdnail on init and allow for download later on. My issue is that the view is not updated with the images after I load them. After pressing the load button for the second time, my first images are then displayed.

I'm having trouble finding the reason behind this behaviour.

The image holder :

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
            })
    }

The view :

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)
                }
            }
        )
    }
}

The observed object containing the array of loaded images :

class ImagesHolder: ObservableObject {
    @Published var images: [MyImage] = []

    func append(_ myImage: MyImage) {
        objectWillChange.send()
        images.append(myImage)
    }
}

And finally my data loader :

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: Self.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);
      })
}
Majid
  • 654
  • 2
  • 7
  • 28
  • Why is `MyImage` an `ObservableObject` if it doesn't have any `@Published` properties? That aside, nested `ObservableObject`s are always going to be challenging to deal with. You'll be much better off storing your models in `struct`s if you can and getting the updating behavior for free. See an answer I wrote on this yesterday: https://stackoverflow.com/a/69081334/560942 And the most common thread on the issue: https://stackoverflow.com/questions/58406287/how-to-tell-swiftui-views-to-bind-to-nested-observableobjects – jnpdx Sep 07 '21 at 22:38
  • I updated it, it was a last moment change miss. I will check the links your provided. – Majid Sep 08 '21 at 08:18

1 Answers1

1

What I ended up doing and worked great for me was having a seperate view for the display of my Image like this :


struct MyImageView: View {

    @ObservedObject var image: MyImage

    init(image: MyImage) {
        self.image = image
    }

    var body: some View {
        if image.loaded {
            Image(uiImage: image.thumbnail!)
                .resizable()
        } else {
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle())
                .frame(width: 100, height: 100, alignment: .center)
        }
    }
}

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
                MyImageView(image: image)
            }
        }
    }

    private func loadImages() -> Void {
        loadMediaList(
            onLoadDone: { myImages in
                myImages.forEach { image in
                    imagesHolder.append(image)
                }
            }
        )
    }
}
Majid
  • 654
  • 2
  • 7
  • 28