Search code examples
iosswiftuicombineobservableobjectobservedobject

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


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.


Solution

  • 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
        ...