I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
@Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn
):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
@Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView
observes the containedView
property and displays the correct view based on it.
My problem is that isLoggedIn
is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained
is not subscriber and publisher at the same time, but I think the AnyCancellable
is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
@Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger
objectWillChange
, so it's useless forObservableObject
. But it may be useful for some related problems.
Main idea is to create propertyWrapper
that will update property value on change in linked Publisher
:
@propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
@Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
@Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject
to a View, SwiftUI adds a receiver for the objectWillChange
publisher and you need to do the same. As objectWillChange
is sent before isLoggedIn
changes it might be an idea to add a publisher that sends in its didSet
. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never>
is probably best. In your HostViewModel
you then need to subscribe to AppState
's new publisher and update containedView
using the published value. Using assign
can cause reference cycles so sink
with a weak reference to self
is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink
to an AnyCancellable?
otherwise your subscriber will disappear.
A generic solution for subscribing to changes of @Published
variables in embedded ObservedObject
s is to pass objectWillChange
notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
@Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
@Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
@ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With