Passing an ObservableObject model through another ObObject?

Spa*_*ard 3 ios swift swiftui combine observableobject

I feel like I can sort of understand why what I'm doing isn't working but I'm still trying to wrap my head around Combine and SwiftUI so any help here would be welcome.

Consider this example:

Single view app that stores some strings in UserDefaults, and uses those strings to display some Text labels. There are three buttons, one to update the title, and one each to update the two UserDefaults-stored strings to a random string.

The view is a dumb renderer view and the title string is stored directly in an ObservableObject view model. The view model has a published property that holds a reference to a UserSettings class that implements property wrappers to store the user defined strings to UserDefaults.

Observations:

• Tapping "Set A New Title" correctly updates the view to show the new value

• Tapping either of the "Set User Value" buttons does change the value internally, however the view does not refresh. If "Set A New Title" is tapped after one of these buttons, the new values are shown when the view body rebuilds for the title change.

View:

import SwiftUI

struct ContentView: View {
    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            Text(model.title).font(.largeTitle)
            Form {
                Section {
                    Text(model.settings.UserValue1)
                    Text(model.settings.UserValue2)
                }

                Section {
                    Button(action: {
                        self.model.title = "Updated Title"
                    }) { Text("Set A New Title") }
                    Button(action: {
                        self.model.settings.UserValue1 = "\(Int.random(in: 1...100))"
                    }) { Text("Set User Value 1 to Random Integer") }
                    Button(action: {
                        self.model.settings.UserValue2 = "\(Int.random(in: 1...100))"
                    }) { Text("Set User Value 2 to Random Integer") }
                }

                Section {
                    Button(action: {
                        self.model.settings.UserValue1 = "Initial Value One"
                        self.model.settings.UserValue2 = "Initial Value Two"
                        self.model.title = "Initial Title"
                    }) { Text("Reset All") }
                }

            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

ViewModel:

import Combine

class ViewModel: ObservableObject {

    @Published var title = "Initial Title"

    @Published var settings = UserSettings()

}
Run Code Online (Sandbox Code Playgroud)

UserSettings model:

import Foundation
import Combine

@propertyWrapper struct DefaultsWritable<T> {
    let key: String
    let value: T

    init(key: String, initialValue: T) {
        self.key = key
        self.value = initialValue
    }

    var wrappedValue: T {
        get { return UserDefaults.standard.object(forKey: key) as? T ?? value }
        set { return UserDefaults.standard.set(newValue, forKey: key) }
    }
}



final class UserSettings: NSObject, ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>()

    @DefaultsWritable(key: "UserValue", initialValue: "Initial Value One") var UserValue1: String {
        willSet {
            objectWillChange.send()
        }
    }
    @DefaultsWritable(key: "UserBeacon2", initialValue: "Initial Value Two") var UserValue2: String {
        willSet {
            objectWillChange.send()
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

When I put a breakpoint on willSet { objectWillChange.send() } in UserSettings I see that the objectWillChange message is going to the publisher when I would expect it to so that tells me that the issue is likely that the view or the view model is not properly subscribing to it. I know that if I had UserSettings as an @ObservedObject on the view this would work, but I feel like this should be done in the view model with Combine.

What am I missing here? I'm sure it's really obvious...

Asp*_*eri 14

ObsesrvedObject监听@Published属性的变化,而不是更深的内部发布者,所以下面的想法是加入内部发布者,即PassthroughSubject, ,@Published var settings,表示后者已经更新。

使用 Xcode 11.2 / iOS 13.2 测试

唯一需要的改变是ViewModel......

class ViewModel: ObservableObject {

    @Published var title = "Initial Title"
    @Published var settings = UserSettings()

    private var cancellables = Set<AnyCancellable>()
    init() {
        self.settings.objectWillChange
            .sink { _ in
                self.objectWillChange.send()
            }
            .store(in: &cancellables)
    }
}
Run Code Online (Sandbox Code Playgroud)