SwiftUI: How can I catch changing value from observed object when I execute function
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 thestruct
has changedclass
is a reference type, when any of it's properties change, the underlyingclass
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 thecodeTwoText
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)
}
}
alice_ix
Updated on July 21, 2022Comments
-
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.