swiftswiftuicombinewatchconnectivity

Using Combine to set up Publishers and while also satisfying requirement for initial values on non-nil variables


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)
    }
}

Solution

  • 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)
        }
    }