diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift new file mode 100644 index 000000000..450ac7d6f --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift @@ -0,0 +1,39 @@ +#if OPENSWIFTUI_OPENCOMBINE +public import OpenCombine +#else +public import Combine +#endif + +extension Scene { + /// Supplies an `ObservableObject` to a view subhierarchy. + /// + /// The object can be read by any child by using `EnvironmentObject`: + /// + /// final class Profile: ObservableObject { ... } + /// + /// @main + /// struct MyApp: App { + /// var body: some View { + /// WindowGroup { + /// ContentView() + /// } + /// .environment(ProfileService.currentProfile) + /// } + /// } + /// + /// You then read the object inside `ContentView` or one of its descendants + /// using the ``EnvironmentObject`` property wrapper: + /// + /// struct ContentView: View { + /// @EnvironmentObject private var currentAccount: Account + /// + /// var body: some View { ... } + /// } + /// + /// - Parameter object: the object to store and make available to + /// the scene's subhierarchy. + @available(OpenSwiftUI_v5_0, *) + nonisolated public func environmentObject(_ object: T) -> some Scene where T: ObservableObject { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift new file mode 100644 index 000000000..f9670f6fc --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift @@ -0,0 +1,241 @@ +// +// EnvironmentObject.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 1BEC77534769ADF54BD6111359D32D97 (SwiftUICore) + +import OpenAttributeGraphShims +#if OPENSWIFTUI_OPENCOMBINE +public import OpenCombine +#else +public import Combine +#endif + +// MARK: - EnvironmentObject + +/// A property wrapper type for an observable object that a parent or ancestor +/// view supplies. +/// +/// An environment object invalidates the current view whenever the observable +/// object that conforms to +/// [ObservableObject](https://swiftpackageindex.com/openswiftuiproject/opencombine/main/documentation/opencombine/observableobject) +/// changes. If you declare a property as an environment object, be sure +/// to set a corresponding model object on an ancestor view by calling its +/// ``View/environmentObject(_:)`` modifier. +/// +/// > Note: If your observable object conforms to the +/// [Observable](https://swiftpackageindex.com/openswiftuiproject/openobservation/main/documentation/openobservation/observable) +/// protocol, use ``Environment`` instead of `EnvironmentObject` and set the +/// model object in an ancestor view by calling its ``View/environment(_:)`` +/// or ``View/environment(_:_:)`` modifiers. +@available(OpenSwiftUI_v1_0, *) +@frozen +@propertyWrapper +@preconcurrency +@MainActor +public struct EnvironmentObject: DynamicProperty where ObjectType: ObservableObject { + + /// A wrapper of the underlying environment object that can create bindings + /// to its properties using dynamic member lookup. + @dynamicMemberLookup + @frozen + @preconcurrency + @MainActor + public struct Wrapper { + let root: ObjectType + + /// Returns a binding to the resulting value of a given key path. + /// + /// - Parameter keyPath: A key path to a specific resulting value. + /// + /// - Returns: A new binding. + public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { + Binding(root, keyPath: keyPath) + } + } + + /// The underlying value referenced by the environment object. + /// + /// This property provides primary access to the value's data. However, you + /// don't access `wrappedValue` directly. Instead, you use the property + /// variable created with the ``EnvironmentObject`` attribute. + /// + /// When a mutable value changes, the new value is immediately available. + /// However, a view displaying the value is updated asynchronously and may + /// not show the new value immediately. + @inlinable + public var wrappedValue: ObjectType { + guard let store = _store else { error() } + return store + } + + @inlinable + var optionalWrappedValue: ObjectType? { + _store + } + + @usableFromInline + var _store: ObjectType? + + @usableFromInline + var _seed: Int = 0 + + /// A projection of the environment object that creates bindings to its + /// properties using dynamic member lookup. + /// + /// Use the projected value to pass an environment object down a view + /// hierarchy. + public var projectedValue: Wrapper { + guard let store = _store else { error() } + return .init(root: store) + } + + @usableFromInline + func error() -> Never { + preconditionFailure("No ObservableObject of type \(ObjectType.self) found. A View.environmentObject(_:) for \(ObjectType.self) may be missing as an ancestor of this view.") + } + + /// Creates an environment object. + public init() { + _openSwiftUIEmptyStub() + } + + nonisolated public static func _makeProperty( + in buffer: inout _DynamicPropertyBuffer, + container: _GraphValue, + fieldOffset: Int, + inputs: inout _GraphInputs + ) { + let attribute = Attribute(value: ()) + let box = StoreBox( + host: .currentHost, + environment: inputs.environment, + signal: WeakAttribute(attribute) + ) + buffer.append(box, fieldOffset: fieldOffset) + addTreeValue( + attribute, + as: ObjectType.self, + at: fieldOffset, + in: V.self, + flags: .environmentObjectSignal + ) + } +} + +@available(OpenSwiftUI_v3_0, *) +extension EnvironmentObject { + @MainActor + @preconcurrency + public static var _propertyBehaviors: UInt32 { + DynamicPropertyBehaviors.requiresMainThread.rawValue + } +} + +// MARK: - StoreBox + +private struct StoreBox: DynamicPropertyBox where ObjectType: ObservableObject { + + typealias Upstream = ObjectType.ObjectWillChangePublisher + + typealias Property = EnvironmentObject + + @Attribute var environment: EnvironmentValues + + let signal: WeakAttribute<()> + + let subscriber: AttributeInvalidatingSubscriber + + let lifetime: SubscriptionLifetime = .init() + + var seed: Int = .zero + + var oldStore: ObjectType? + + init( + host: GraphHost, + environment: Attribute, + signal: WeakAttribute<()> + ) { + self._environment = environment + self.signal = signal + self.subscriber = .init(host: host, attribute: signal) + } + + mutating func reset() { + oldStore = nil + } + + mutating func update(property: inout Property, phase: ViewPhase) -> Bool { + let (env, envChanged) = $environment.changedValue() + var changed = envChanged + if let oldStore, !envChanged { + property._store = oldStore + } else { + let store = env[keyPath: ObjectType.environmentStore] + property._store = store + if oldStore === store { + changed = false + } + } + let newStore = property._store + if let newStore { + let shouldForceSubscription = isLinkedOnOrAfter(.v6) ? false : !ObjectType.hasDefaultPublisher + let isUninitialized = oldStore == nil + if oldStore == nil || isUninitialized || newStore !== oldStore || shouldForceSubscription { + lifetime.subscribe(subscriber: subscriber, to: newStore.objectWillChange) + } + } + let signalChanged = signal.changedValue()?.changed ?? false + changed = changed || signalChanged + if changed { + seed &+= 1 + } + property._seed = seed + oldStore = newStore + return changed + } +} + +// MARK: - View + environmentObject + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Supplies an observable object to a view's hierarchy. + /// + /// Use this modifier to add an observable object to a view's environment. + /// The object must conform to the + /// [ObservableObject](https://swiftpackageindex.com/openswiftuiproject/opencombine/main/documentation/opencombine/observableobject) + /// protocol. + /// + /// Adding an object to a view's environment makes the object available to + /// subviews in the view's hierarchy. To retrieve the object in a subview, + /// use the ``EnvironmentObject`` property wrapper. + /// + /// > Note: If the observable object conforms to the + /// [Observable](https://swiftpackageindex.com/openswiftuiproject/openobservation/main/documentation/openobservation/observable) + /// protocol, use either ``View/environment(_:)`` or the + /// ``View/environment(_:_:)`` modifier to add the object to the view's + /// environment. + /// + /// - Parameter object: The object to store and make available to + /// the view's hierarchy. + @inlinable + nonisolated public func environmentObject(_ object: T) -> some View where T: ObservableObject { + environment(T.environmentStore, object) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension ObservableObject { + @usableFromInline + static var environmentStore: WritableKeyPath { + \EnvironmentValues[EnvironmentObjectKey()] + } + + static var hasDefaultPublisher: Bool { + ObjectWillChangePublisher.self == ObservableObjectPublisher.self + } +} diff --git a/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObjectKey.swift b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObjectKey.swift new file mode 100644 index 000000000..dfe0a69f4 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObjectKey.swift @@ -0,0 +1,36 @@ +// +// EnvironmentObjectKey.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +#if OPENSWIFTUI_OPENCOMBINE +import OpenCombine +#else +import Combine +#endif + +// MARK: - EnvironmentObjectKey + +struct EnvironmentObjectKey: EnvironmentKey, Hashable where ObjectType: AnyObject { + init() { + _openSwiftUIEmptyStub() + } + + static var defaultValue: ObjectType? { nil } +} + +// MARK: - EnvironmentValues + EnvironmentObjectKey + +extension EnvironmentValues { + subscript(key: EnvironmentObjectKey) -> ObjectType? where ObjectType: AnyObject { + get { self[objectType: ObjectType.self] } + set { self[objectType: ObjectType.self] = newValue } + } + + subscript(objectType _: ObjectType.Type) -> ObjectType? where ObjectType: AnyObject { + get { self[EnvironmentObjectKey.self] } + set { self[EnvironmentObjectKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift b/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift index 3d9580c70..f14c353fd 100644 --- a/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift +++ b/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift @@ -293,8 +293,8 @@ private struct ObservedObjectPropertyBox: DynamicPropertyBox where O mutating func update(property: inout Property, phase: ViewPhase) -> Bool { let object = property.wrappedValue - let shouldForceSubscription = isLinkedOnOrAfter(.v6) ? false : Upstream.self != ObservableObjectPublisher.self - if object !== lastObject || lifetime.isUninitialized || shouldForceSubscription { + let shouldForceSubscription = isLinkedOnOrAfter(.v6) ? false : !ObjectType.hasDefaultPublisher + if lastObject == nil || object !== lastObject || lifetime.isUninitialized || shouldForceSubscription { lifetime.subscribe(subscriber: subscriber, to: object.objectWillChange) } lastObject = object