What it's the recommended way to support multiple windows without sharing state?

Im currently working with iOS14b4 and Xcode 12b4 and given this basic setup, it can be seen that any change on AppState it's propagated to every scene instances.
So my question is if anybody knows a solution that avoids the unwanted store sharing on the multi window scenario.

struct AppState: Equatable {
  var searchQuery = ""
}

enum AppAction: Equatable {
  case searchQueryChanged(String)
}

struct AppEnvironment {
  var mainQueue: AnySchedulerOf<DispatchQueue>
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  case let .searchQueryChanged(query):
    state.searchQuery = query
    return .none
  }
}
struct ContentView: View {
  
  let store: Store<AppState, AppAction>
  
  var body: some View {
    WithViewStore(store) { viewStore in
      TextField(
        "Berlin, Amsterdam, ...",
        text: viewStore.binding(
          get: { $0.searchQuery },
          send: AppAction.searchQueryChanged
        )
      ).padding()
    }
  }
}
@main
struct MultiWindowApp: App {
  
  let store = Store(
    initialState: AppState(),
    reducer: appReducer,
    environment: AppEnvironment(mainQueue: DispatchQueue.main.eraseToAnyScheduler())
  )
  
  var body: some Scene {
    WindowGroup {
      ContentView(store: store)
    }
  }
}
2 Likes

Did you ever find the solution? I would have thought moving the store initialization to the ContentView itself it would cause two states to be created.

Could we try passing a scoped store using a forEach from an IdentifiedArray as a parameter to the content view? Keyed somehow to the tag or id of the instance of the WindowGroup?

Running into this issue as well. Did you ever find a fix?

Yes, still figuring out a more elegant solution, but basically if you store your "TCA state" on a property @State inside a children of the view hierarchy (and not on the first level), then you can use it for constructing independent stores when instantiating windows.

import SwiftUI
import ComposableArchitecture

@main
struct TCAMultiWindowApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

struct AppState: Identifiable, Equatable {
  let id: UUID //Only used for demo purposes
  var color: Color = .clear
}

enum AppAction: Equatable {
  case changeColor(Color)
}

struct AppEnvironment {}

let appReducer = Reducer<AppState, AppAction, AppEnvironment> { (state, action, _) in
  switch action {
  case let .changeColor(color):
    state.color = color
    return .none
  }
}

struct ContentView: View {
  
  // This is what will allow an independent state for each window
  @State var appState: AppState? = nil
  
  var body: some View {
    if let state = appState {
      
      let store = Store(
        initialState: state,
        reducer: appReducer,
        environment: AppEnvironment()
      )
      
      WithViewStore(store) { viewStore in
        VStack {
          Text(state.id.description).background(viewStore.color)
          
          Button("Change Color") {
            viewStore.send(.changeColor(.pink))
          }
        }
        .padding()
      }
      
    }
    else {
      Spacer()
        .onAppear {
          appState = AppState(id: UUID())
        }
    }
  }
}

Tried also combined with mathieutozer idea (using IdentifiedArray and ForEachStore) and also works with the same @State on children technic and even if it gives you centralised control on the windows, in my experience its rather easy to enter on recursion.

Note that I've mostly focused on making this work on macOS, so I didn't tested properly yet on iOS.

The key reference for me has being this mention on the WindowGroup documentation:

Every window created from the group maintains independent state. For example, for each new window created from the group the system allocates new storage for any State or StateObject variables instantiated by the scene’s view hierarchy.

1 Like

Isn’t the issue here that every time a new window is created, each new ContentView is referencing the same store?

Couldn’t you just have a factory function, e.g. “makeStore()” that returns a new store each time and use that when injecting your store into the ContentView?

That didn't work for me

In what way didn’t it work?

It works for me.

Terms of Service

Privacy Policy

Cookie Policy