SwiftUI: How can I catch changing value from observed object when I execute function

10,347

Solution 1

It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.

Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)

Tested with Xcode 11.4 / iOS 13.4

Modified code below (see also important comments inline)

extension UserDefaults { 
    @objc dynamic var textKey2: String { // helper keypath 
        return string(forKey: "textKey2") ?? ""
    }
}

class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!

    private static let userDefaultTextKey = "textKey2"
    @Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""

    private var canc: AnyCancellable!
    private var observer: NSKeyValueObservation!

    init() {
        canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
            UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
        }
        observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
            if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
                self.text = newValue
            }
        }
    }
}

class NetworkManager: ObservableObject {

    var codeTwo = SettingCodeTwo() // no @ObservedObject needed here
    ...

Solution 2

Non-SwiftUI Code

  • Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
  • Use a subscriber like Sink to observe changes to any publisher. (Every @Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.

Reason for SwiftUI View not reacting to class property changes:

  • struct is a value type so when any of it's properties change then the value of the struct has changed
  • class is a reference type, when any of it's properties change, the underlying class instance is still the same.
    • If you assign a new class instance then you will notice that the view reacts to the change.

Approach:

  • Use a separate view and that accepts codeTwoText as @Binding that way when the codeTwoText changes the view would update to reflect the new value.
  • You can keep the model as a class so no changes there.

Example

class Model : ObservableObject {

    @Published var name : String //Ensure the property is `Published`.

    init(name: String) {
        self.name = name
    }
}

struct NameView : View {

    @Binding var name : String

    var body: some View {

        return Text(name)
    }
}

struct ContentView: View {

    @ObservedObject var model : Model

    var body: some View {
        VStack {
            Text("Hello, World!")
            NameView(name: $model.name) //Passing the Binding to name
        }
    }
}

Testing

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {

        let model = Model(name: "aaa")

        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            model.name = "bbb"
        }

        return ContentView(model: model)
    }
}
Share:
10,347
alice_ix
Author by

alice_ix

Updated on July 21, 2022

Comments

  • alice_ix
    alice_ix almost 2 years

    I have a problem with observed object in SwiftUI. I can see changing values of observed object on the View struct. However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.

    here's my code sample (this is my ObservableObject)

    class settingCodeTwo: ObservableObject {
    
    private static let userDefaultTextKey = "textKey2"
    @Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
    
    private var canc: AnyCancellable!
    
         init() {
            canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
            UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
    
        }
    }
    
    
       deinit {
         canc.cancel()
       }
    
    }
    

    and the main problem is... "self.codeTwo.text" never changed!

    class NetworkManager: ObservableObject {
    
    
    @ObservedObject var codeTwo = settingCodeTwo()
    @Published var posts = [Post]()
    
    
    func fetchData() {
        var urlComponents = URLComponents()
        urlComponents.scheme = "http"
    
    
    
        urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
    
    
    
    
        urlComponents.path = "/mob_json/mob_json.aspx"
        urlComponents.queryItems = [
            URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
            URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
        ]
    
        if let url = urlComponents.url {
            print(url)
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error == nil {
                    let decoder = JSONDecoder()
                    if let safeData = data {
                        do {
                            let results = try decoder.decode(Results.self, from: safeData)
    
                            DispatchQueue.main.async {
                                self.posts = results.Table
                            }   
                        } catch {
                            print(error)
                        }
                    }
                }
            }
            task.resume()
        }
    
    }
    }
    

    and this is view, I can catch change of the value in this one

    import SwiftUI
    import Combine
    
    
    
    struct SettingView: View {
    
      @ObservedObject var codeTwo = settingCodeTwo()
      var body: some View {
        ZStack {
            Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
            VStack {
    
    
                TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
    
                Text(codeTwo.text)
            }
        }
    }
    }
    

    Help me please.