pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.53k stars 1.45k forks source link

Stack based navigation throw error when write to userdefault in reducer and use scenePhase environment key #2534

Closed Doraemoe closed 1 year ago

Doraemoe commented 1 year ago

Description

When include @Environment(\.scenePhase) var scenePhase in the root view and use stack based navigation, I cannot write to UserDefaults in child view reducer.

Errors:

A "forEach" at "userdefault_tca/ContentView.swift:19" received an action for a missing element. …

  Action:
    RootFeature.Path.Action.child(.test)

This is generally considered an application logic error, and can happen for a few reasons:

• A parent reducer removed an element with this ID before this reducer ran. This reducer must run before any other reducer removes an element, which ensures that element reducers can handle their actions while their state is still available.

• An in-flight effect emitted this action when state contained no element at this ID. While it may be perfectly reasonable to ignore this action, consider canceling the associated effect before an element is removed, especially if it is a long-living effect.

• This action was sent to the store while its state contained no element at this ID. To fix this make sure that actions for this reducer can only be sent from a view store when its state contains an element at this id. In SwiftUI applications, use "NavigationStackStore".

Checklist

Expected behavior

I can write to UserDefaults in reducer

Actual behavior

Xcode throw errors

Steps to reproduce

Please see example: https://github.com/Doraemoe/tca-scenephase-userdefault

The Composable Architecture version information

1.3.0

Destination operating system

iOS 17

Xcode version information

Version 15.0.1 (15A507)

Swift Compiler version information

swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
Target: x86_64-apple-macosx14.0
stephencelis commented 1 year ago

@Doraemoe Your root app scene is creating a brand new store in its body, so when it detects a scene change your state is replaced with a brand new store with no path, which puts SwiftUI in a bad state. I believe you'd have the same problem in vanilla SwiftUI using an observable object.

The fix is to hold onto your store in your app so that it is never recreated:

@main
struct userdefault_tcaApp: App {
  @Environment(\.scenePhase) var scenePhase
  let store = Store(initialState: RootFeature.State()) {
    RootFeature()
  }

  var body: some Scene {
    WindowGroup {
      ContentView(store: self.store)
    }
  }
}

In general you should not create new models in the body of scenes or views as SwiftUI's observation can recreate them at any time.

Since this isn't a bug with the library, I'm going to convert it to a discussion instead.