I'm relatively new to reactive programming concepts, and I'm trying to build a simple view model to update @Published Bool
values that are used to keep UI updated with SwiftUI.
This particular model is setting up those Bool
values based on other values from the WatchConnectivity
framework as they change over time.
Even though this is a simple example and it's working, I feel like I'm missing opportunities to reduce redundancy.
Specifically, it feels weird that I am repeating the logic used to set up the initial values of appNotInstalled
and complicationNotInstalled
later when I use Publishers.CombineLatest
and Publishers.CombineLatest3
.
Even though the initial values are passed through the publishers, so they go through the CombineLatest
pipelines and set the initial values, it feels wrong to arbitrarily set the published variables to true
or false
, but the compiler is making me set initial values for them somewhere.
If I don't set initial values, I get the Variable 'self.appNotInstalled' used before being initialized
error.
Is there a way I can avoid setting initial values, without making them nil, or another way to avoid duplicating the logic used to determine their values?
Here's the working code I have:
class WatchConnectivityModel: ObservableObject {
// values used to show/hide UI
@Published var appNotInstalled: Bool
@Published var complicationNotInstalled: Bool
private var cancellables: [AnyCancellable] = []
init() {
// initialize based on the values of everything at class init
let activated = WCSession.default.activationState == .activated
let appInstalled = WCSession.default.isWatchAppInstalled
let complicationInstalled = WCSession.default.isComplicationEnabled
appNotInstalled = !(activated && appInstalled)
complicationNotInstalled = activated && appInstalled && !complicationInstalled
// set up the publishers for any changes
let activationStatePublisher = WCSession.default.publisher(for: \.activationState)
let isWatchAppInstalledPublisher = WCSession.default
.publisher(for: \.isWatchAppInstalled)
let isComplicationEnabledPublisher = WCSession.default
.publisher(for: \.isComplicationEnabled)
// set up assignment of appNotInstalled for changes
Publishers.CombineLatest(activationStatePublisher.removeDuplicates(),
isWatchAppInstalledPublisher.removeDuplicates())
.map { (state, installed) in
// repeated logic from above
return !(state == .activated && installed)
}.receive(on: RunLoop.main)
.assign(to: \.appNotInstalled, on: self)
.store(in: &cancellables)
// set up assignment of complicationNotInstalled for changes
Publishers.CombineLatest3(activationStatePublisher.removeDuplicates(),
isWatchAppInstalledPublisher.removeDuplicates(),
isComplicationEnabledPublisher.removeDuplicates())
.map { (state, appInstalled, complicationInstalled) in
// repeated logic again
return state == .activated && appInstalled && !complicationInstalled
}.receive(on: RunLoop.main)
.assign(to: \.complicationNotInstalled, on: self)
.store(in: &cancellables)
}
}
I created a new initializer struct for each that just includes the logic for each. Whenever I set the initial values, or update the published variables, I use this to avoid duplicating the logic. I'm open to other possibilities if someone has a better solution to this.
import Foundation
import Combine
import WatchConnectivity
class WatchConnectivityModel: ObservableObject {
// values used to show/hide UI
@Published var appNotInstalled: Bool
@Published var complicationNotInstalled: Bool
private struct AppNotInstalled {
let value: Bool
init(_ activationState: WCSessionActivationState,
_ appInstalled: Bool) {
value = !(activationState == .activated && appInstalled)
}
}
private struct ComplicationNotInstalled {
let value: Bool
init(_ activationState: WCSessionActivationState,
_ appInstalled: Bool,
_ complicationInstalled: Bool) {
value = activationState == .activated && appInstalled && !complicationInstalled
}
}
private var cancellables: [AnyCancellable] = []
init() {
// initilize based on the current values
let state = WCSession.default.activationState
let appInstalled = WCSession.default.isWatchAppInstalled
let complicationInstalled = WCSession.default.isComplicationEnabled
appNotInstalled = AppNotInstalled(state,
appInstalled).value
complicationNotInstalled = ComplicationNotInstalled(state,
appInstalled,
complicationInstalled).value
// set up the publishers
let activationStatePublisher = WCSession.default
.publisher(for: \.activationState)
let isWatchAppInstalledPublisher = WCSession.default
.publisher(for: \.isWatchAppInstalled)
let isComplicationEnabledPublisher = WCSession.default
.publisher(for: \.isComplicationEnabled)
// set up assignment of appNotInstalled
Publishers.CombineLatest(activationStatePublisher.removeDuplicates(),
isWatchAppInstalledPublisher.removeDuplicates())
.map { (state, installed) in
return AppNotInstalled(state, installed).value
}.receive(on: RunLoop.main)
.assign(to: \.appNotInstalled,
on: self)
.store(in: &cancellables)
// set up assignment of complicationNotInstalled
Publishers.CombineLatest3(activationStatePublisher.removeDuplicates(),
isWatchAppInstalledPublisher.removeDuplicates(),
isComplicationEnabledPublisher.removeDuplicates())
.map { (state, appInstalled, complicationInstalled) in
return ComplicationNotInstalled(state, appInstalled, complicationInstalled).value
}.receive(on: RunLoop.main)
.assign(to: \.complicationNotInstalled,
on: self)
.store(in: &cancellables)
}
}