3

I have a following scenario.

I have AppState which consists of an object of type Foo. Foo has a counter variable and I want to call objectWillChange when counter value updates so I can update the UI.

At present nothing happens. The increment function gets called but the UI never gets updated.




import Foundation
import Combine

class Foo: ObservableObject {
    @Published var counter: Int = 999
    
    func increment() {
        counter += 1 // how to get notified when counter value changes
    }
}

class AppState: ObservableObject {
    
    @Published var foo: Foo = Foo()
}

// usage in Scene Delegate as Environment Object
let appState = AppState()

// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = UIHostingController(rootView: accountSummaryScreen.environmentObject(appState))

UPDATE

class Foo: ObservableObject {
    @Published var counter: Int = 999 {
        didSet {
            objectWillChange.send() 
        }
    }
    
    func increment() {
        counter += 1 // how to get notified when counter value changes
    }
}

john doe
  • 9,220
  • 23
  • 91
  • 167
  • That's because `Foo` doesn't actually change - it's a reference-type - so `@Published` doesn't help here, since it's always the same reference – New Dev Aug 04 '20 at 17:05
  • Yes that is clear! What is the solution for this, apart from making Foo a struct. How can I get changes from Foo object. – john doe Aug 04 '20 at 17:06
  • See if this helps: https://stackoverflow.com/a/62988407/968155, but in a nutshell - you'd need to manually subscribe to changes in `.counter` and call your own `objectWillChange.send` – New Dev Aug 04 '20 at 17:07
  • I added update. Calling manually inside the Foo class but even that does not do anything. – john doe Aug 04 '20 at 17:11

2 Answers2

3

Changes aren't detected because Foo, being a reference-type, doesn't actually change - it's the same reference, so @Published doesn't help here.

AppState would need to manually subscribe to changes and call its own objectWillChange.send:

class AppState: ObservableObject {
    
    var foo: Foo = Foo() {
       didSet {
          cancellables = []
          foo.$counter
             .map { _ in } // ignore actual values
             .sink(receiveValue: self.objectWillChange.send)
             .store(in: &cancellables)
       }
    }

    private var cancellables: Set<AnyCancellable> = []
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • @johndoe - I amended the answer to account for the case when `foo` itself is updated – New Dev Aug 04 '20 at 17:24
  • In my real app I have AppState which consists an array of items and each item has another item which has a balance property. Seems like this will be a LOT of work if I go that route. Easy way is to simply use the struct ; – john doe Aug 04 '20 at 17:33
  • @johndoe - definitely. If they are values, not living objects, then they should be structs – New Dev Aug 04 '20 at 17:34
1

In SwiftUI our model types are structs, e.g.

struct Foo: Identifiable {
    let id = UUID()
    var counter: Int = 999 
    
    mutating func increment() {
        counter += 1 
    }
}

Now it can be used with @Published in an ObservableObject.

malhal
  • 26,330
  • 7
  • 115
  • 133