Update of @Published array does not update view

Hi,


I have a view that displays pictures loaded via web service call. The model for the view is


@ObservedObject private var pictureController: PictureController = PictureController()


The pictures are provided in an array from the controller:

@Published var images:[Picture] = []


and the View displays the data from the array with a List (my Picture class conforms to the Identifiable protocol)

List(pictureController.images){ item in
  Text(item.bkey)
  }


When loading the pictures from the web service, I store them in a buffer array and afterwards copy the array over to the images array

DispatchQueue.main.async {
  self.images = self.imageBuffer
  completionHandler(true)
  }


I call the controller in the View's onAppear()

self.pictureController.fetchPicturesByTask(bkey_issue: self.bkey_task, completionHandler: { success in
  print(self.pictureController.images.count)

  self.pictureController.images.forEach(){picture in
  print(picture.id)
  print(picture.bkey!)
  }
  })


Here is the thing: self.pictureController.images.count actually return the proper count of items in the array, the prints below show the correct values but for whatever reason, the List in the View does not get updated ...


Just for the sake of it I have also added a button to the View that calls a function on the controller to manually add dummy items to the array and that works perfect ...


Any ideas what I am missing here?


Max

Seems to be one of the known issues of the current implementation of SwiftUI.


You may need to tell SwiftUI update views when `pictureController.images` has changed.

Though I cannot write any examples, as you have not shown enough code.

Thanks, I have also tried to extend the images array with this method:


var willChange = PassthroughSubject<Void, Never>()

@Published var images:[Picture] = []{
  willSet{
  willChange.send()
  }
  }


to make sure that a notification is sent but that also did not work.


Max

that also did not work.

So, I expect. It is not the right way under the current implementation of SwiftUI.

Can you maybe point me in a correct direction? 🙂

you have not shown enough code.

And code you shall get ... 🙂


  • View
  • Controller
  • Picture class


import SwiftUI

struct PictureGalleryDemo: View {

  @ObservedObject private var pictureController: PictureController = PictureController()

    var body: some View {
  List(pictureController.images){ item in
  Text(item.bkey)
  }.onAppear{
  self.pictureController.fetchPicturesByTask(bkey_issue: "XYZ", completionHandler: { success in
  print(success)
  })
  }
    }
}


//
//

import Alamofire
import Combine
import Foundation
import SwiftUI
import SWXMLHash

final class PictureController: ObservableObject {
  var bkey_issue: String!
  var bkey: String!
  var statusMessage = ""
  var reference = ""
  var referenceType = ""
  var subProcess = ""
  var mode = ""
  var picture: Picture! = Picture()

  @Published var images: [Picture] = []

  var imageBuffer: [Picture] = []

  @Published var isLoading: Bool = false {
  willSet {
  self.willChange.send()
  }
  }

  @Published var statusCode: Int = 0 {
  willSet {
  willChange.send()
  }
  }

  @Published var loadError: Bool = false {
  willSet {
  self.willChange.send()
  }
  }

  var willChange = PassthroughSubject<Void, Never>()

  private let userSettings = UserSettings()

  init() {}

  public func fetchPicturesByTask(bkey_issue: String, completionHandler: @escaping (Bool) -> Void) {
  self.referenceType = ""
  self.reference = ""
  self.subProcess = "fetchPicturesByReference"
  self.bkey_issue = bkey_issue

  self.loadData(completionHandler: { success in
  if success {
  completionHandler(true)
  } else {
  completionHandler(false)
  }
  })
  }

  public func loadData(completionHandler: @escaping (Bool) -> Void) {
  let connectionPreCheck = ConnectionPreCheck()

  if !connectionPreCheck.validate() {
  self.statusMessage = connectionPreCheck.statusMessage
  self.loadError = true
  self.isLoading = false
  return
  }

  self.isLoading = true
  self.imageBuffer = []

  if self.bkey_issue == nil {
  self.bkey_issue = ""
  }

  let feedUrl = self.userSettings.host + ":\(self.userSettings.port!)/ws/kn-getOrderByClient-V1-VS/1.0/generic?userDomain=\(self.userSettings.userDomain!)&userID=\(self.userSettings.userID!)&token=\(self.userSettings.userToken!)&process=\(self.userSettings.process!)&subProcess=\(self.subProcess)&client=\(self.userSettings.client!)&depot=\(self.userSettings.depot!)&bkey_issue=\(self.bkey_issue!)&connection=\(self.userSettings.connectionKey!)"

  print(feedUrl)

  var request = URLRequest(url: URL(string: feedUrl)!)
  request.timeoutInterval = self.userSettings.timeout

  let urlSession = URLSession.shared
  let task = urlSession.dataTask(with: request, completionHandler: { (data, _, error) -> Void in
  guard let data = data else {
  if let error = error {
  // self.connectionErrorHandler(error: error as NSError)
  }
  return
  }

  let dataString = String(data: data, encoding: .utf8)
  // print(dataString)

  let xml = SWXMLHash.parse(dataString!)

  for elem in xml["DocumentXML"]["Message"]["Header"]["Pictures"]["Picture"].all {
  let item = Picture()
  item.bkey = elem["bkey"].element!.text
  item.imageType = elem["imageType"].element!.text
  self.imageBuffer.append(item)
  }

  DispatchQueue.main.async {
  self.images = self.imageBuffer
  completionHandler(true)
  }
  })

  task.resume()
  }
}

extension UIImage {
  func toBase64() -> String? {
  guard let imageData = self.pngData() else { return nil }
  return imageData.base64EncodedString(options: Data.Base64EncodingOptions.lineLength64Characters)
  }
}
import Foundation

class Picture: Codable, Identifiable, ObservableObject {

  var id = UUID()
  var bkey: String!
  var tmp_create: Date!
  var userDomain: String!
  var userID: String!
  var depot: String!
  var client: String!
  var process: String!
  var imageData: String!
  var imageType: String!
  var imageName: String!
  var reference: String!
  var referenceType: String!


  init() {

  }

}

Thanks for showing your code. But, sorry, it is not enough to check which workaround would fix the issue.


I needed to replace `loadData(completionHandler:)` with some dummy code, and the List in `PictureGalleryDemo` is updated as expected without any fixes.

I'm afraid `PictureGalleryDemo` is not the actual code causing the issue.

Ok, so I checked my actual complete code over and over again and the view where I called the PictureGallery from: turns out that that view had the variable


@Environment(\.presentationMode) var presentationMode


and once I commented that out, it works like a charme ...

I have no idea why that would make any difference ...

That may happen.


The reason I requested enough code, is that such subtle changes, which seem to be irrelevant to view updates, would change the behavior.


I do expect that SwiftUI 2, introduced in WWDC 2020 soon, would fix all such issues and we would not need any more do-not-know-why workarounds.

I have been debugging a similar issue (UI become out of sync) with my app, which involves no array. With additional debugging code, I found out that the SwiftUI attempts to update the UI when some values (in @ObservedObject) change, but the new value is not yet available at that time.

This is probably associated with a tricky update mechanism in SwiftUI, which uses Combine, which sends the notification before the value change.
https://developer.apple.com/forums/thread/128096

My code happened to have the following code and removing this line has solved this issue.

@Environment(\.presentationMode) var presentationMode

I faced a similar problem to the original issue.

I noticed,

  • if you have any array of objects(class) : [ObjectA]
  • as an @Published variable in a class: ClassA
  • if you update any property of the objectA in the array [ObjectA], then the subscribers of @Published [ObjectA] will NOT trigger in ClassA
import Foundation
import Combine

class Object {
  var value: Int = 6

  init(value: Int) {
    self.value = value
  }
}

class MyClass {
  @Published var objects: [Object] = []
  var value: String = "Hello"
  var subscriptions = Set<AnyCancellable>()

  init(index: Int, value: String) {
    self.objects = [Object(value: index)]
    self.value = value

    $objects
      .sink {
        print($0.first?.value)
      }
      .store(in: &subscriptions)
  }
}


func changeMemberProperty(_ obj: MyClass) {
  if var first = obj.objects.first {
    print("Will change member property")
    first.value = Int.random(in: 100...400)
  }
  obj.value += "World"
}

var obj = MyClass(index: 1, value: "Hi")
changeMemberProperty(obj)
changeMemberProperty(obj)

func changeMember(_ obj: MyClass) {
  print("Will change member")
  obj.objects = [Object(value: Int.random(in: 100...400))]
}

changeMember(obj)
changeMember(obj)

Output:

Optional(1) /* Because of Print-command during initial setup */
Will change member property /* Didn't trigger Print-command */
Will change member property /* Didn't trigger Print-command */
Will change member
Optional(292) /* Due to triggered change: Print-command */
Will change member
Optional(259)  /* Due to triggered change: Print-command */
Update of @Published array does not update view
 
 
Q