A TCA toy project for sharing a common store using SwiftUI's environment

TCA-Shared-Reducer

This is a toy project using TCA to demonstrate sharing a common store through SwiftUI’s environment.

Use case:

  • In above screenshots we have common player control view, that is displayed to all tabs using .safeAreaInset view modifier to display at the bottom edge of the view.

  • This view is having common state (and so common actions) but the this view’s height does not propogate via NavigationView/NavigationStack as an additional safe area. Therefore, app is displaying these views as a seperate view to each tab item view and not above TabView itself.

    + @Environment(\.playbackControlStore) private var playbackControlStore
    ...
    
    CaseLet(/TabFeature.State.listen, action: TabFeature.Action.listen) { listenStore in
        NavigationStack {
            ListenView(store: listenStore)
                .safeAreaInset(edge: .bottom) {
    +               PlaybackControlMiniView(store: playbackControlStore)
                }
        }
        .tabItem {
            Tab.listen.label
        }
    }
    CaseLet(/TabFeature.State.search, action: TabFeature.Action.search) { searchStore in
        NavigationStack {
            SearchView(store: searchStore)
                .safeAreaInset(edge: .bottom) {
    +               PlaybackControlMiniView(store: playbackControlStore)
                }
        }
        .tabItem {
            Tab.search.label
        }
    }
  • To derive the playbackControlStore which is in the AppRootFeature, app uses SwiftUI’s environment to pass down to children views using below snippet.

    enum PlaybackControlStoreKey: EnvironmentKey {
        static var defaultValue: StoreOf<PlaybackControlFeature> = .init(initialState: .init(), reducer: PlaybackControlFeature())
    }
    
    extension EnvironmentValues {
        var playbackControlStore: StoreOf<PlaybackControlFeature> {
            get {
                self[PlaybackControlStoreKey.self]
            }
            set {
                self[PlaybackControlStoreKey.self] = newValue
            }
        }
    }
  • App injects the the playbackControlStore at the AppRootView‘s body as below snippet.

    init(store: StoreOf<AppRootFeature>) {
        self.store = store
    +   self.playbackControlStore = store.scope(state: \.playbackControl, action: AppRootFeature.Action.playbackControl)
    }
    
    var body: some View {
        WithViewStore(store, observe: {$0}) { viewStore in
            TabView {
                ...
            }
            .sheet(item: viewStore.binding(\.$destination)) { destination in
                switch destination {
                case .expanded:
                    PlaybackControlExpandedView(store: playbackControlStore)
                }
            }
    +        .environment(\.playbackControlStore, playbackControlStore)
        }
    }

GitHub

View Github