Hypothetical WithSceneStore
for communications across SwiftUI Scenes.
#2170
Replies: 6 comments 13 replies
-
I would be happy to see something like this in TCA, though I'm sort of agnostic on what it actually looks like. Just wanted to share for context of the discussion that a less-specialized version of something like this was briefly possible (#336), though it was deprecated and removed (#1910). |
Beta Was this translation helpful? Give feedback.
-
I wonder if the API could look a lot like the complement of pieces that make up stack navigation, but with scene state held in an unordered scene-id-to-scene-enum dictionary instead of an identified array. And in the root app reducer, you implement a nested |
Beta Was this translation helpful? Give feedback.
-
One problem with my proposal. Upon further inspection, it looks like the id on WindowGroup is unique to the WindowGroup, not the individual Windows in the WindowGroup. (See (Though, I'm not sure if that's a big deal. Off the top of my head, I can't think of a use case where I would need that). |
Beta Was this translation helpful? Give feedback.
-
I've just added this comment on the slack channel. Copying it here as well.
|
Beta Was this translation helpful? Give feedback.
-
I don’t think I said this explicitly in the proposal but this proposal is inspired by the TCA Navigation series. The idea is that:
Does that model line up correctly? Please let me know if there are inconsistencies. Currently we need to confirm that:
|
Beta Was this translation helpful? Give feedback.
-
Allow me to join the discussion, if not too late. Currently I am trying to marry TCA with Scenes and SceneDelegates. Let me start with the use case. This is multi-persona application, meaning that the user can have several logins (or profiles) visible in the same app. Somewhat like Slack has or Netflix. But the idea here is to have multiple windows/scenes open at the same time (on iPad), where each scene could belong to different profile, for example, one window I have work account, on the other personal. I am using func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let sessionId = UUID(uuidString: session.persistentIdentifier) ?? UUID()
session.sessionId = sessionId
let mainView = ATAppView(
store: Store(initialState: ATAppFeature.State(sessionId: sessionId)) {
ATAppFeature()
})
if let userActivity = connectionOptions.userActivities.first,
let atScene = userActivity.scene {
var view: any View
switch atScene {
case .main: view = mainView
case .login: view = mainView
}
self.window = atScene.setup(scene: scene, session: session, options: connectionOptions, view: view)
} else {
self.window = ATScene.main.setup(scene: scene, session: session, options: connectionOptions, view: mainView)
} I use public func setup(scene: UIScene, session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions, view: some View) -> UIWindow {
guard let windowScene = scene as? UIWindowScene else {
fatalError("Expected a UIWindowScene")
}
let predicate = NSPredicate(format: "self == '\(kDeepLinkBase)/\(self.rawValue)/*'")
scene.activationConditions.canActivateForTargetContentIdentifierPredicate = predicate
scene.activationConditions.prefersToActivateForTargetContentIdentifierPredicate = predicate
if let userActivity = connectionOptions.userActivities.first {
session.accountId = userActivity.accountId
session.theme = ATTheme(rawValue: Int(userActivity.theme) ?? 0) ?? .default
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: view
.environment(\.sceneSession, session)
.environment(\.sessionId, session.sessionId)
.environment(\.accountId, session.accountId)
.environment(\.theme, session.theme)
)
window.makeKeyAndVisible()
return window
}
public typealias ATUserActivityConfiguration = (NSUserActivity) -> Void
public func open(account: ATUserAccount, configure: ATUserActivityConfiguration? = nil) {
let userActivity = NSUserActivity.forScene(account: account, scene: self)
configure?(userActivity)
UIApplication.shared.requestSceneSessionActivation(
nil,
userActivity: userActivity,
options: nil,
errorHandler: nil
)
} The So far the only way to make it work with the TCA is passing it down in the .onChange(of: viewStore.accountId) { oldValue, newValue in
accountId = newValue!
sceneSession?.accountId = newValue
sceneSession?.theme = config.theme(for: newValue)
}
.onChange(of: viewStore.theme!) { oldValue, newValue in
sceneSession?.theme = newValue
}
...
let _ = ATNotification.Config.AccountSwitch.observe { notification in
guard let account = notification.value as? String,
let targetedSessionId = notification.flags as? UUID else {
return
}
if targetedSessionId == viewStore.sessionId {
viewStore.send(.switchAccount(account))
}
} This works, to some extent. The first thing that I don't like is that it all in the View. Cannot use it (environment) in the Reducer. |
Beta Was this translation helpful? Give feedback.
-
The Problem
As far as I know there is not a TCA native way to facilitate communication across SwiftUI Scenes. Currently most TCA examples I've seen look something like this:
However, the naming of this is slightly incorrect. It's not correct to name this
AppFeature
because it does not have App-level domain. Only Scene-level. TheWindowGroup
Scene is not just one window but a group of windows. On iOS this is largely irrelevant because WindowGroup will only ever display one window, which is the whole app.However macOS can display multiple windows. It also supports the
Window
,Settings
, andDocumentGroup
scenes. And this is not a macOS only problem. iPadOS also supports running multiple instances of the app side by side in their own. Currently we can pass State between Scenes (e.g. like this ).To complicate matters more each instance of a
WindowGroup
Scene has it's own State:Also, in the above example, I don't believe AppFeature is able to observe and react to changes of ScenePhase, let alone handle these on a per-instance level of WindowGroup scenes.
As far as I know there isn't a way yet to make truly App-Wide Reducers i.e. one Reducer which can observe and react to all child Scenes (including each individual instance of a WindowGroup Scene).
The Proposal
Fortunately, I don't think we need to rework too much in TCA. I propose adding a new type called
WithSceneStore
. WithSceneStore would be designed to look and behave the same way that we already useWithViewStore
, however instead of creating a SwiftUIView
, it would create a SwiftUIScene
.It could have a callsite that looks something like this:
Note: WithSceneStore would be entirely optional. If you are using a single scene app (like all iOS apps essentially) then you could continue as before without using WithSceneStore.
Advantages
.onChange(of: ScenePhase)
under the hood.commands
: https://developer.apple.com/documentation/swiftui/scene/commands(content:)See also
Beta Was this translation helpful? Give feedback.
All reactions