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.07k stars 1.41k forks source link

SharedState with UserDefaults(suiteName:) not working properly #3100

Closed rcasula closed 3 months ago

rcasula commented 3 months ago

Description

I'm trying to migrate an existing app to use the new SharedState functionality. My app uses an app group shared UserDefaults.

When I set the defaultAppStorage dependency to be $0.defaultAppStorage = UserDefaults(suiteName: ".."), I get some unexpected behaviour. Seems like if I use even @Shared or @SharedReader in pushed views in a NavigationStack, doesn't update properly.

This is the structure that I have: Home with a TabBar and 3 tabs. Each tab has a button to present a Settings screen with a sheet modal. The Settings screen has a NavigationStack and pushes a new view ("ThirdView). The root SettingsView and the ThirdView contains a Picker that changes the default tab that has to be selected upon opening the app.

Using the UserDefaults(suiteName: "..") this happens: If I change the value in the root SettingsView everything works as expected. But if I do it in the ThirdView, the value is not even updated if I go back to the previous screen (which is the root SettingsView).

Using the UserDefaults.standard, everything works as expected.

This is an example project that showcases this weird behaviour: https://github.com/rcasula/TCASharedStateShowcase

Seems that trying a similar example with @AppStorage works.

Checklist

Steps to reproduce

  1. Open the sample project https://github.com/rcasula/TCASharedStateShowcase
  2. Change the defaultAppStorage dependency to either .standard or the UserDefaults(suiteName: "..")
  3. Try updating the settings
  4. Observe the differences

The Composable Architecture version information

1.10.4

Destination operating system

iOS 17

Xcode version information

15.3

Swift Compiler version information

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
OguzYuuksel commented 3 months ago

There is little bit of work to complete / verify in the Appstore Connect to be able to use App groups. Did you complete that? Do you try it with an erased simulator? So you don't have any previous cache.

rcasula commented 3 months ago

There is little bit of work to complete / verify in the Appstore Connect to be able to use App groups. Did you complete that? Do you try it with an erased simulator? So you don't have any previous cache.

What kind of work are you referring to? Usually Xcode is taking care of updating the provisioning profile correctly, when you add the capability. Using @AppStorage directly in a vanilla SwiftUI project (see the example repo that I shared) works even with the AppGroup configured.

stephencelis commented 3 months ago

@rcasula The third view is being instantiated like so:

NavigationLink("Third view") {
  ThirdView(store: .init(initialState: .init()) { Third() })
}

This store is completely independent of the root store and doesn't share its dependencies unless you explicitly pass it along.

If ThirdView took a store that was scoped from the parent it should work just fine.

Alternately, if you require spinning up a new store you need to use withDependencies: again. Observation across both stores requires the same user defaults object, though, since it is done via KVO. I think that UserDefaults.init(suiteName:) will create a new object each time, so you would want to create a static that preserves the object for the lifetime of the app:

extension UserDefaults {
  static let mySuite = UserDefaults(
    suiteName: "group.dev.casula.TCA.SharedStateShowcase"
  )!
}

// ...

let store = Store(initialState: .init()) {
  Home()
} withDependencies: {
  $0.defaultAppStorage = .mySuite
}

// ...

ThirdView(store: .init(initialState: .init()) {
  Third()
} withDependencies: {
  $0.defaultAppStorage = . mySuite
}

Because this is not a bug and just how TCA and user defaults work, I'm going to convert to a discussion.