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!
mbrandonw
(Brandon Williams)
2
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.