WindowGroup on macOS not creating independent windows

88 views Asked by At

Considering the following code

@main
struct so_tcaMultipleIndepententWindowsOnMacOSApp: App {
  var body: some Scene {
    WindowGroup {
      ContentViewVanilla()
    }
  }
}

struct ContentViewVanilla: View {
  @State private var id = UUID()

  var body: some View {
    Text(id.uuidString)
  }
}

according to Apple's documentation on WindowGroup, opening a new window on macOS should open windows that are independent from each other:

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

So, I'd expect the UUID differ in every window I open when running the app–which is not the case. As it is, each new window displays the same UUID.

I am on Xcode 15.1 running on macOS 14.2.

What am I missing?

edit: working with a StateObject works as expected, working with @State still does not

When I refactor above code to use a ViewModel that holds the UUID, it works as I would expect:

@main
struct so_tcaMultipleIndepententWindowsOnMacOSApp: App {
  var body: some Scene {
    WindowGroup {
      ContentViewVanilla()
    }
  }
}

struct ContentViewVanilla: View {
  @StateObject private var viewModel = ViewModel()

  var body: some View {
    Text(viewModel.id.uuidString)
  }
}

class ViewModel: ObservableObject {
  let id = UUID()
}

I do understand that there's a difference in using a reference type rather than a value type, but given the information in Apple's documentation (see above), I would hope that I would not need to have to create a separate class here.

The reason I do not want to do this is that I want to use TCA which is based on struct and not on class.

As per Sweeper's comment below, it seems that for Xcode 15.0 and macOS 14.1.1 my first approach is totally creating independent windows, so there is still some hope that this is a bug in Apple's stack that I perhaps should file a feedback for.

Any further thoughts on this?

1

There are 1 answers

1
appfrosch On BEST ANSWER

Even though I am not agreeing with the documentation that tells me to expect separate Windows for @State (cause it does not for me, even not for the macOS 13 I tried it with, using a virtual machine installation), I found a workaround I can live with right now.

The workaround is wrapping the TCA structs I want to use in an @ObservableObject like so:

import ComposableArchitecture
import SwiftUI

@main
struct so_tcaMultipleIndepententWindowsOnMacOSApp: App {
  var body: some Scene {
    WindowGroup {
      ContentViewWrapper()
    }
  }
}

struct ContentViewWrapper: View {
  @StateObject private var viewModel = ViewModel(
    store: Store(
      initialState: AppFeature.State(),
      reducer: {
        AppFeature()
      }
    )
  )

  var body: some View {
    ContentViewTCA(store: viewModel.store)
  }
}

class ViewModel: ObservableObject {
  let store: StoreOf<AppFeature>

  init(store: StoreOf<AppFeature>) {
    self.store = store
  }
}

struct ContentViewTCA: View {
  @State var store: StoreOf<AppFeature>

  var body: some View {
    AppView(store: store)
  }
}

@Reducer
struct AppFeature {
  @ObservableState
  struct State {
    let id = UUID()
  }

  enum Action {}

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {}
    }
  }
}

struct AppView: View {
  let store: StoreOf<AppFeature>

  var body: some View {
    Text(store.id.uuidString)
  }
}

I will next incorporate this into my real TCA project and see if there are any issues with this approach down the TCA layer–I don't expect any, but than again I was hoping not to need anything like this workaround neither…