SwiftUI: Observe internal changes in published object

Viewed 5671

SwiftUI, Swift 5.2, Xcode 11.4

I'm trying to observe changes in a singleton, but I'm not always getting my SwiftUI view refreshed:

final class Patient: ObservableObject {
      static var shared: Patient
      @Published var medicalData: MedicalData

      init { ... }

      final class MedicalData { 
            var allergies: String
            var ...

            init { ... }
      }
}

So, in my SwiftUI view:

struct ContentView: View {
       @ObservedObject var patient: Patient = Patient.shared
       var body: some view { ... }
}

If any object replaces the medical data, the publisher will inform my SwiftUI correctly:

patient.medicalData = NEW_MEDICAL_DATA --> OK! View refreshed

But if any object changes a value IN current medical data, the SwiftUI View is not refreshed:

patient.medicalData.allergies = "Alcanfor" --> NOT PUBLISHED

Does anyone knows how to accomplish this? Thank you in advance.

2 Answers

Found a elegant way to perform this (see didSet):

final class Patient: ObservableObject {
      static var shared: Patient
      @Published var medicalData = MedicalData() { 
           didSet {
                  subscription = medicalData.objectWillChange.sink { [weak self] _ in
                        self?.objectWillChange.send()
                  }
           }
      }
      var subscription: AnyCancellable?

      init { ... }
}

This works out of the box if the property was initialized. If not, just write also the didSet code after initialize the medicalData propertie at 'init()`.

Of course, MedicalData must conform the ObservableObject protocol.

The simplest is to make it value type

  struct MedicalData { 
        var allergies: String
        var ...

This gives

  patient.medicalData.allergies = "Alcanfor" --> PUBLISHED

Update: Possible approach for CoreData object... I would still prefer to use struct, now as wrapper. It requires a bit additional efforts but allows to control what should affect view refresh and should not, and refresh itself works automatically via existed publish

// helper struct wrapper
struct CoreDataWrapper<T:NSManagedObject> {
    let value: T

    init(_ wrapped: T) {
        value = wrapped
    }

    private var refreshed = false
    private mutating func refresh() {
        refreshed.toggle()
    }
}

// helper protocol of properties visible to extensions (optionals - works w/o it as well)
protocol MedicalDataProtocol {
    var allergies: String? { get set }
}

// extension to wrapper for a specific class properties
extension CoreDataWrapper: MedicalDataProtocol where T == MedicalData {
    var name: String? {
        get { value.allergies }
        set { value.allergies = newValue; refresh() }
    }
}


// usage in view model

final class Patient: ObservableObject {
      static var shared: Patient
      @Published var medicalData: CoreDataWrapper<MedicalData>

      // ... other code here
Related