Replies: 9 comments 8 replies
-
If possible, provide the source code (or at least a subset of it) so we could help with spotting the problem :) Without the source code, my best guess is that if the entire app structure is recreated, the most probable explanation is that you're observing the whole app state everywhere. In other words, you are using either You can control what views are refreshed by scoping the Please consult the https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift to see the basics of how to scope the app store to smaller stores so that the views are refreshed independently. |
Beta Was this translation helpful? Give feedback.
-
First, thank you for your response :D I will try to provide the relevant source here, and if needed perhaps offline? I have included the views, all reducers are combined all the way up to the main app reducer (as I thought was expected?) All sub views are scoped I believe correctly. Main App
|
Beta Was this translation helpful? Give feedback.
-
To be clear , or perhaps I don't understand something , if AppState is not changed it should not cause a refresh of anything ? body of withViewStore should only be refreshed when the appropriate state for that view store is changed yes? |
Beta Was this translation helpful? Give feedback.
-
As @siejkowski pointed out, this is an issue with observing more state than you actually need, and it's a problem that plagues vanilla SwiftUI applications as much as TCA applications. Any time you use WithViewStore(self.appStore.scope(state: \.signInStatus)) { viewStore in
if viewStore.isSignedIn {
MainAppView(store: appStore).environmentObject(sheetManager)
} else {
LandingView(store: appStore).environmentObject(sheetManager)
}
} That would guarantee that this view code is re-evaluated only when the Further, the usage of struct InviteFriendsView: View {
...
struct ViewState: Equatable {
let searchText: String
let title: String
init(state: InviteFriendsState) {
self.searchText = state.searchText
self.title = state.title
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
}
}
} This will prevent the view from being re-evaluated until either It's also worth mentioning that this problem definitely does affect vanilla SwiftUI views, including using class VM: ObservableObject {
@Published var searchText: String = ""
@Published var tabIndex = 0
}
struct ContentView: View {
@ObservedObject var vm: VM
var body: some View {
let _ = print("ContentView")
TabView.init(selection: self.$vm.tabIndex) {
NavigationView {
let _ = print("Tab 0")
Tab0View(text: self.$vm.searchText)
}
.tabItem { Text("One") }
.tag(0)
NavigationView {
let _ = print("Tab 1")
Text("Tab 1")
}
.tabItem { Text("Two") }
.tag(1)
NavigationView {
let _ = print("Tab 2")
Text("Tab 2")
}
.tabItem { Text("Three") }
.tag(2)
}
}
}
struct Tab0View: View {
@Binding var text: String
var body: some View {
Form {
Text("Tab 0")
TextField("Test", text: self.$text)
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Also I’m going to transfer this issue to the discussions tab of the repo because I think it’ll be more appropriate there. |
Beta Was this translation helpful? Give feedback.
-
Hi @mbrandonw, First, thank you for taking the time to help everyone around TcA, much appreciated. I am posting on this thread as I am hitting the same problem. Changing one property of the state reloads every single view. Your suggestion To make the discussion easier I've created 2 example projects. Both are based on the episode 147 with a You can download the projects here I experience the same issue with vanilla SwiftUI with I am very curious to hear your thoughts on this. Thanks! |
Beta Was this translation helpful? Give feedback.
-
A key is that I had to make sure I did this all the way back to the root view / store. Since a change in a child state held by the root store would of course cause the root view to reload and all nested views, so even if you fix the child view it would still reload.
John
…On May 30, 2021, 1:32 PM -0700, Wes ***@***.***>, wrote:
I see, it's not what I've done. I will try that and report back. Thanks a lot!
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
Beta Was this translation helpful? Give feedback.
-
@wes-nz I've been able to reproduce the behavior you are seeing, and even with a However, even with that bug there are ways to work around. Rather than wrapping the entire view in a struct TcaContentView: View {
let store: Store<AppState, AppAction>
var body: some View {
TabView {
WithViewStore(self.store.scope(state: \.count)) { viewStore in
TcaCounterView(
...
)
.tabItem { Text("Counter \(viewStore.state)") }
}
WithViewStore(self.store.scope(state: \.favorites.count)) { viewStore in
TcaProfileView(
...
)
.tabItem { Text("Profile \(viewStore.state)") }
}
}
}
} That fixes the problem you are seeing in the toy TCA app and may help you with your real app. |
Beta Was this translation helpful? Give feedback.
-
@grangej @mbrandonw Thanks to both of you for your help, it's sorted. Both methods work, either multiple If anyone stumble on this thread, here is an example. struct TcaProfileView: View {
let store: Store<ProfileState, ProfileAction>
@ObservedObject var viewStore: ViewStore<ViewState, ProfileAction>
// only list the properties the view should listen to
struct ViewState: Equatable {
var favorites: Set<Int> = []
init(state: ProfileState) {
self.favorites = state.favorites
}
}
init(store: Store<ProfileState, ProfileAction>) {
self.store = store
self.viewStore = ViewStore(store.scope(state: ViewState.init))
}
var body: some View {
List {
ForEach(viewStore.favorites.sorted(), id: \.self) { number in
HStack {
Text("\(number)")
Spacer()
Button("Remove") {
viewStore.send(.removeButtonTapped(number))
}
}
}
}
}
} or struct TcaProfileView: View {
let store: Store<ProfileState, ProfileAction>
struct ViewState: Equatable {
var favorites: Set<Int> = []
init(state: ProfileState) {
self.favorites = state.favorites
}
}
var body: some View {
WithViewStore(self.store.scope(state: ViewState.init)) { viewStore in
List {
ForEach(viewStore.favorites.sorted(), id: \.self) { number in
HStack {
Text("\(number)")
Spacer()
Button("Remove") {
viewStore.send(.removeButtonTapped(number))
}
}
}
}
}
}
} I would probably rename the discussion in case someone has the same issue. |
Beta Was this translation helpful? Give feedback.
-
I am going around in circles and not sure if this is behaving the way it should be:
[AppReducer]
[tab1Reducer, tab2Reducer , tab3Reducer, tab4Reducer]
[tab1Presented]
[tab1PresentedPresented]
On tab1PresentedPresented I have a search field that is to filter some results. the problem is that it seems to trigger a complete refresh of the entire app structure. All tabs are refreshed. This causes tab1PresentedPresented to be reconstructed and is dismissing the keyboard / causing crash when embedded in a NavigationView.
The only state that was manipulated was tab1PresentedPresented, no other states were changed.
If I don't have tab1Presented combine tab1PresnetedPresented reducer the problem goes away but of course state for tab1PresentedPresented doesn't update since the reducer is not actually created / held onto.
Beta Was this translation helpful? Give feedback.
All reactions