How to Maintain Persistent State in Child Views with SwitchStore in TCA

Hello Swift and TCA Community,

I am working on a SwiftUI project utilizing TCA and facing a challenge with maintaining persistent state in child views when switching tabs using SwitchStore.

Issue Description: In my application, I have a tabbed interface where each tab is represented by a separate view and state in TCA. I am using SwitchStore to switch between these tabs. I have an .onAppear(), which gets ran once, that updates the state of my child ReminderPreferencesFeature state. However, every time I switch tabs, the state of the child views seems to be reinitialized, losing any stateful information or changes made in those child views.

Here is a simplified version of my code structure:

Store:

import Foundation
import ComposableArchitecture

// Model
struct PreferenceTab: Identifiable, Hashable {
    let id: Int
    var imgString: String
    let name: String
    var isSelected: Bool
}

@Reducer
struct PreferencesFeature {
    
    enum State: Equatable {
        case reminderTab(ReminderPreferencesFeature.State)
        case sharingTab(SharingPreferencesFeature.State)
        case sortingTab(SortingPreferencesFeature.State)
        
        // Default selected the first tab
        init() { self = .reminderTab(ReminderPreferencesFeature.State()) }
    }
    
    enum Action: Equatable {
        case selectTab(Int)
        case dismissSheet
        case reminderTab(ReminderPreferencesFeature.Action)
        case sharingTab(SharingPreferencesFeature.Action)
        case sortingTab(SortingPreferencesFeature.Action)
    }
    
    var body: some ReducerOf<Self> {
        Scope(state: \.reminderTab, action: \.reminderTab) {
            ReminderPreferencesFeature()._printChanges()
        }
        
        Reduce { state, action in
            switch action {
            case .selectTab(let id):
                switch id {
                case 0:
                    state = .reminderTab(ReminderPreferencesFeature.State())
                case 1:
                    state = .sharingTab(SharingPreferencesFeature.State())
                case 2:
                    state = .sortingTab(SortingPreferencesFeature.State())
                default:
                    break
                }
                return .none
                
            // On dismissal, reset default tab back to reminder
            case .dismissSheet:
                state = .reminderTab(ReminderPreferencesFeature.State())
                return .none
                
            case .reminderTab:
                return .none
                
            case .sharingTab:
                return .none
                
            case .sortingTab:
                return .none
            }
        }
        .ifCaseLet(\.reminderTab, action: \.reminderTab) {
            ReminderPreferencesFeature()
        }
        .ifCaseLet(\.sharingTab, action: \.sharingTab) {
            SharingPreferencesFeature()
        }
        .ifCaseLet(\.sortingTab, action: \.sortingTab) {
            SortingPreferencesFeature()
        }
    }
}

SwitchStore:

SwitchStore(self.store) { state in
                            switch state {
                            case .reminderTab:
                                // Present reminders
                                CaseLet(\PreferencesFeature.State.reminderTab, action: PreferencesFeature.Action.reminderTab) { store in
                                    ReminderPreferencesView(store: store, event: event, recipient: recipient)
                                }
                                
                            case .sharingTab:
                                // Present sharing preferences
                                CaseLet(\PreferencesFeature.State.sharingTab, action: PreferencesFeature.Action.sharingTab) { store in
                                    SharingPreferencesView(store: store)
                                }
                                
                            case .sortingTab:
                                // Present sorting preferences
                                CaseLet(\PreferencesFeature.State.sortingTab, action: PreferencesFeature.Action.sortingTab) { store in
                                    SortingPreferencesView(store: store)
                                }
                            }
                        }

I am looking for guidance or best practices on how to maintain the state of each child view when switching tabs with SwitchStore in TCA. Any suggestions or insights on how to properly structure the state and actions to achieve this would be greatly appreciated.

Thank you in advance for your help!

Hi @kevintv789, an enum is not the best way to model the state for a tab-based feature because state exists for each tab even if the tab is not being actively displayed. And as you are seeing, with an enum each time you switch out of a tab and back the state is cleared out. Instead you should hold onto the state for each tab in the parent feature.

Thank you Brandon! As I traverse this vast world of TCA, I might have been misunderstanding the purpose of SwitchStores.

I'm currently refactoring out of using SwitchStores to the more basic approach of creating a RootStore, then lazily loading all items on the RootStore's state to pass it down to its children stores.