Skip to content

Conversation

@stephencelis
Copy link
Member

We can add an ObservableObject conformance to Store so that it can be held in @StateObject. This conformance is inert and should not imply that the store is observable as an @ObservedObject.

@stephencelis stephencelis requested a review from mbrandonw March 14, 2025 19:20
@stephencelis stephencelis merged commit 3927f26 into main Apr 1, 2025
@stephencelis stephencelis deleted the observable-object-conformance branch April 1, 2025 23:52
@arnauddorgans
Copy link
Contributor

@stephencelis Should we not provide something like this instead of conforming to protocols without proper implementation?

import SwiftUI
import ComposableArchitecture

/// A property wrapper that mimics `@StateObject` for root-level TCA features.
///
/// Useful when your feature has no parent (e.g. in your `App`, modal entry point, or top-level view).
/// Ensures the store is only initialized once.
///
/// ```swift
/// @RootStore(MyFeature(), state: MyFeature.State()) var store
/// ```
@MainActor
@propertyWrapper
struct RootStore<R>: @preconcurrency DynamicProperty where R: Reducer {
    @State private var storage: Storage
    var wrappedValue: StoreOf<R> { storage.store }
    var projectedValue: Perception.Bindable<StoreOf<R>> {
        .init(wrappedValue)
    }

    init(
        _ reducer: @escaping @autoclosure () -> R,
        state initialState: @escaping @autoclosure () -> R.State,
        withDependencies prepareDependencies: ((inout DependencyValues) -> Void)? = nil
    ) {
        self._storage = .init(initialValue: .init(makeStore: {
            .init(initialState: initialState(), reducer: reducer, withDependencies: prepareDependencies)
        }))
    }

    func update() {
        storage.update()
    }

    @Perceptible
    fileprivate final class Storage {
        private let makeStore: () -> StoreOf<R>
        private(set) var store: StoreOf<R>!

        init(makeStore: @escaping () -> StoreOf<R>) {
            self.makeStore = makeStore
        }

        func update() {
            guard store == nil else { return }
            store = makeStore()
        }
    }
}

@stephencelis
Copy link
Member Author

@arnauddorgans The main reason we want to support @StateObject is because, unlike @State, its wrapped value is lazily evaluated on creation, and we want to support that for both root stores and scoped stores.

And while we considered a custom property wrapper, it's not only additional API surface area to learn about and maintain, but we think Apple probably is better equipped to provide such an API and any internals it uses than us.

We're down to chat about this further if you think it's worth it, but want to start a dedicated discussion?

@arnauddorgans
Copy link
Contributor

@stephencelis I am just afraid about the fact that a SwiftUI view observes changes from StateObject based on ObservableObject protocol, but as the Store is Observable and not ObservableObject, we do not send events to the objectWillChange publisher

Is it safe? Are we not afraid that internal SwiftUI logic makes us miss view updates?

@stephencelis
Copy link
Member Author

@arnauddorgans We don't believe it is a problem but we can revisit if you encounter any!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants