diff --git a/Sources/OpenSwiftUI/App/App/FinishLaunchTestAction.swift b/Sources/OpenSwiftUI/App/App/FinishLaunchTestAction.swift index a71bcf4eb..05f76e9f9 100644 --- a/Sources/OpenSwiftUI/App/App/FinishLaunchTestAction.swift +++ b/Sources/OpenSwiftUI/App/App/FinishLaunchTestAction.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: Blocked by SceneModifier +// Status: Complete // ID: 71E21E30634D2453CAA80C5CA9EF3E2C (SwiftUI?) #if os(iOS) || os(visionOS) @@ -23,8 +23,10 @@ import OpenSwiftUICore @available(OpenSwiftUI_v3_0, *) extension Scene { nonisolated public func extendedLaunchTestName(_ name: String?) -> some Scene { - // TODO: _PreferenceWritingModifier - self + preference( + key: ExtendedLaunchTestNameKey.self, + value: name + ) } } diff --git a/Sources/OpenSwiftUI/App/Commands/Commands.swift b/Sources/OpenSwiftUI/App/Commands/Commands.swift index fc9267da0..568e47e2c 100644 --- a/Sources/OpenSwiftUI/App/Commands/Commands.swift +++ b/Sources/OpenSwiftUI/App/Commands/Commands.swift @@ -3,11 +3,12 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: Blocked by MainMenuItem and Scene +// Status: Complete - Blocked by MainMenuItem // ID: 0E12E75FDDFA412408873260803B3C8B (SwiftUI) import OpenAttributeGraphShims import COpenSwiftUI +@_spi(ForOpenSwiftUIOnly) @_spi(Private) public import OpenSwiftUICore @@ -180,7 +181,7 @@ public struct _ResolvedCommands { @available(*, unavailable) extension _ResolvedCommands: Sendable {} -// MARK: - Scene + commands [WIP] +// MARK: - Scene + commands @available(OpenSwiftUI_v2_0, *) @available(tvOS, unavailable) @@ -201,9 +202,7 @@ extension Scene { nonisolated public func commands( @CommandsBuilder content: () -> Content ) -> some Scene where Content: Commands { - // CommandModifier - _openSwiftUIUnimplementedWarning() - return self + modifier(CommandsModifier(content: content())) } } @@ -266,7 +265,7 @@ extension TypeConformance where P == CommandsDescriptor { } } -// MARK: - CommandsModifier [WIP] +// MARK: - CommandsModifier struct CommandsModifier: PrimitiveSceneModifier where Content: Commands { var content: Content @@ -276,7 +275,27 @@ struct CommandsModifier: PrimitiveSceneModifier where Content: Commands inputs: _SceneInputs, body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs ) -> _SceneOutputs { - _openSwiftUIUnimplementedFailure() + guard inputs.preferences.requiresSceneList else { + return body(_Graph(), inputs) + } + var commandsInputs = _CommandsInputs( + base: inputs.base, + preferences: .init(hostKeys: inputs.preferences.hostKeys) + ) + commandsInputs.preferences.add(CommandsList.Key.self) + var commandsOutputs = Content._makeCommands( + content: .init(modifier.value.content), + inputs: commandsInputs + ) + var outputs = body(_Graph(), inputs) + if let commandsList = outputs.preferences.commandsList { + outputs.preferences.makePreferenceTransformer( + inputs: commandsInputs.preferences, + key: CommandsList.Key.self, + transform: Attribute(UpdateList(list: commandsList)) + ) + } + return outputs } private struct UpdateList: Rule { diff --git a/Sources/OpenSwiftUI/App/Commands/CommandsFlags.swift b/Sources/OpenSwiftUI/App/Commands/CommandsFlags.swift index dcb4aae6f..e6707c868 100644 --- a/Sources/OpenSwiftUI/App/Commands/CommandsFlags.swift +++ b/Sources/OpenSwiftUI/App/Commands/CommandsFlags.swift @@ -39,14 +39,13 @@ struct WithCommandFlag: PrimitiveCommands where Content: Commands { content: content[offset: { .of(&$0.content) }], inputs: inputs ) - if inputs.preferences.contains(CommandsList.Key.self) { - let setFlag = Attribute( + if inputs.preferences.requiresCommandsList { + outputs.preferences.commandsList = Attribute( SetFlag( container: content.value, - list: .init(outputs.preferences[CommandsList.Key.self]), + list: .init(outputs.preferences.commandsList), ) ) - outputs.preferences[CommandsList.Key.self] = setFlag } return outputs } diff --git a/Sources/OpenSwiftUI/App/Commands/CommandsList.swift b/Sources/OpenSwiftUI/App/Commands/CommandsList.swift index 93cdb7710..8b04af069 100644 --- a/Sources/OpenSwiftUI/App/Commands/CommandsList.swift +++ b/Sources/OpenSwiftUI/App/Commands/CommandsList.swift @@ -5,6 +5,8 @@ // Audited for 6.5.4 // Status: Complete +import OpenAttributeGraphShims + // MARK: - CommandsList struct CommandsList: Hashable { @@ -68,3 +70,25 @@ extension CommandsList { } } } + +extension PreferencesInputs { + @inline(__always) + var requiresCommandsList: Bool { + get { contains(CommandsList.Key.self) } + set { + if newValue { + add(CommandsList.Key.self) + } else { + remove(CommandsList.Key.self) + } + } + } +} + +extension PreferencesOutputs { + @inline(__always) + var commandsList: Attribute? { + get { self[CommandsList.Key.self] } + set { self[CommandsList.Key.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUI/App/Scene/SceneList.swift b/Sources/OpenSwiftUI/App/Scene/SceneList.swift index 94e5eea70..c455e4b87 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneList.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneList.swift @@ -69,7 +69,7 @@ extension SceneList { case settings(AnyView) // case menuBarExtra(MenuBarExtraConfiguration) // case customScene(UISceneAdaptorConfiguration) -// case singleWindow(SingleWindowConfiguration) + case singleWindow(SingleWindowConfiguration) // case documentIntroduction(DocumentIntroductionConfiguration) // case alertDialog(DialogConfiguration) } @@ -136,3 +136,6 @@ extension PreferencesOutputs { set { self[SceneList.Key.self] = newValue } } } + +// TODO +struct SingleWindowConfiguration {} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/AppearanceActionSceneModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/AppearanceActionSceneModifier.swift new file mode 100644 index 000000000..7e1d669aa --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/AppearanceActionSceneModifier.swift @@ -0,0 +1,42 @@ +// +// AppearanceActionSceneModifier.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +import OpenAttributeGraphShims +package import OpenSwiftUICore + +@available(OpenSwiftUI_v4_0, *) +extension _AppearanceActionModifier: PrimitiveSceneModifier { + @MainActor + @preconcurrency + public static func _makeScene( + modifier: _GraphValue<_AppearanceActionModifier>, + inputs: _SceneInputs, + body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs + ) -> _SceneOutputs { + let effect = AppearanceEffect( + modifier: modifier.value, + phase: inputs.base.phase + ) + let attribute = Attribute(effect) + attribute.flags = [.transactional, .removable] + return body(_Graph(), inputs) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v4_0, *) +extension Scene { + @_alwaysEmitIntoClient + nonisolated public func onAppear(perform action: (() -> Void)? = nil) -> some Scene { + modifier(_AppearanceActionModifier(appear: action, disappear: nil)) + } + + @_alwaysEmitIntoClient + nonisolated public func onDisappear(perform action: (() -> Void)? = nil) -> some Scene { + modifier(_AppearanceActionModifier(appear: nil, disappear: action)) + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentSceneModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentSceneModifier.swift new file mode 100644 index 000000000..a55bf4de6 --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentSceneModifier.swift @@ -0,0 +1,66 @@ +// +// EnvironmentSceneModifier.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +@available(OpenSwiftUI_v2_0, *) +extension _EnvironmentKeyWritingModifier: PrimitiveSceneModifier {} + +extension Scene { + + /// Sets the environment value of the specified key path to the given value. + /// + /// Use this modifier to set one of the writable properties of the + /// ``EnvironmentValues`` structure, including custom values that you + /// create. For example, you can create a custom environment key + /// `styleOverrides` to set a value that represents style settings that for + /// the entire app: + /// + /// WindowGroup { + /// ContentView() + /// } + /// .environment(\.styleOverrides, StyleOverrides()) + /// + /// You then read the value inside `ContentView` or one of its descendants + /// using the ``Environment`` property wrapper: + /// + /// struct MyView: View { + /// @Environment(\.styleOverrides) var styleOverrides: StyleOverrides + /// + /// var body: some View { ... } + /// } + /// + /// This modifier affects the given scene, + /// as well as that scene's descendant views. It has no effect + /// outside the view hierarchy on which you call it. + /// + /// - Parameters: + /// - keyPath: A key path that indicates the property of the + /// ``EnvironmentValues`` structure to update. + /// - value: The new value to set for the item specified by `keyPath`. + /// + /// - Returns: A view that has the given value set in its environment. + @available(OpenSwiftUI_v2_0, *) + @_alwaysEmitIntoClient + nonisolated public func environment(_ keyPath: WritableKeyPath, _ value: V) -> some Scene { + modifier(_EnvironmentKeyWritingModifier(keyPath: keyPath, value: value)) + } +} + +@available(OpenSwiftUI_v5_0, *) +extension _EnvironmentKeyTransformModifier: PrimitiveSceneModifier {} + +@available(OpenSwiftUI_v5_0, *) +extension Scene { + + /// Transforms the environment value of the specified key path with the + /// given function. + nonisolated public func transformEnvironment( + _ keyPath: WritableKeyPath, + transform: @escaping (inout V) -> Void + ) -> some Scene { + modifier(_EnvironmentKeyTransformModifier(keyPath: keyPath, transform: transform)) + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentWritingModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentWritingModifier.swift new file mode 100644 index 000000000..89bee92bf --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/EnvironmentWritingModifier.swift @@ -0,0 +1,94 @@ +// +// EnvironmentWritingModifier.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 327A94017466EC589024364A56314D10 (SwiftUI) + +import OpenAttributeGraphShims +import OpenSwiftUICore + +// MARK: - Scene + environment + +extension Scene { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + nonisolated public func environment( + key: K.Type = K.self, + value: K.Value + ) -> some Scene where K: EnvironmentKey { + modifier(EnvironmentWritingSceneModifier(value: value)) + } +} + +// MARK: - EnvironmentWritingModifier + +private protocol EnvironmentWritingModifier: _GraphInputsModifier { + associatedtype Key: EnvironmentKey + + var value: Key.Value { get } +} + +// MARK: - EnvironmentWritingSceneModifier + +struct EnvironmentWritingSceneModifier: PrimitiveSceneModifier, EnvironmentWritingModifier where Key: EnvironmentKey { + var value: Key.Value + + static func _makeInputs( + modifier: _GraphValue, + inputs: inout _GraphInputs + ) { + inputs.environment = Attribute( + ChildEnvironment( + modifier: modifier.value, + environment: inputs.environment, + ) + ) + } +} + +// MARK: - ChildEnvironment + +private struct ChildEnvironment: StatefulRule, AsyncAttribute, CustomStringConvertible where Modifier: EnvironmentWritingModifier { + @Attribute var modifier: Modifier + @Attribute var environment: EnvironmentValues + var oldModifier: Modifier? + + init( + modifier: Attribute, + environment: Attribute, + oldModifier: Modifier? = nil + ) { + self._modifier = modifier + self._environment = environment + self.oldModifier = oldModifier + } + + var description: String { + "EnvironmentWriting: \(Modifier.self)" + } + + typealias Value = EnvironmentValues + + mutating func updateValue() { + let (modifier, modifierChanged) = $modifier.changedValue() + let (environment, environmentChanged) = $environment.changedValue() + var modifierNeedsUpdate: Bool { + guard modifierChanged else { + return false + } + if let oldModifier { + return compareValues(oldModifier.value, modifier.value) + } else { + return true + } + } + if environmentChanged || !hasValue || modifierNeedsUpdate { + var env = environment + env[Modifier.Key.self] = modifier.value + value = env + oldModifier = modifier + } + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift new file mode 100644 index 000000000..de6a908a4 --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift @@ -0,0 +1,77 @@ +// +// PreferenceSceneModifier.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - PreferenceWritingModifier + SceneModifier + +@available(OpenSwiftUI_v2_0, *) +extension _PreferenceWritingModifier: _SceneModifier { + @MainActor + @preconcurrency + public static func _makeScene( + modifier: _GraphValue, + inputs: _SceneInputs, + body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs + ) -> _SceneOutputs { + var inputs = inputs + inputs.preferences.remove(Key.self) + var outputs = body(_Graph(), inputs) + outputs.preferences + .makePreferenceWriter( + inputs: inputs.preferences, + key: Key.self, + value: modifier.value[offset: { .of(&$0.value) }] + ) + return outputs + } +} + +@available(OpenSwiftUI_v4_0, *) +extension Scene { + @inlinable + @MainActor + @preconcurrency + internal func preference( + key: K.Type = K.self, + value: K.Value + ) -> some Scene where K: PreferenceKey { + modifier(_PreferenceWritingModifier(value: value)) + } +} + +// MARK: - _PreferenceTransformModifier + SceneModifier + +@available(OpenSwiftUI_v2_0, *) +extension _PreferenceTransformModifier: _SceneModifier { + @MainActor + @preconcurrency + public static func _makeScene( + modifier: _GraphValue, + inputs: _SceneInputs, + body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs + ) -> _SceneOutputs { + var outputs = body(_Graph(), inputs) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: Key.self, + transform: modifier.value.transform + ) + return outputs + } +} + +@available(OpenSwiftUI_v4_0, *) +extension Scene { + @inlinable + @MainActor + @preconcurrency + internal func transformPreference( + _ key: K.Type = K.self, + _ callback: @escaping (inout K.Value) -> Void + ) -> some Scene where K: PreferenceKey { + modifier(_PreferenceTransformModifier(transform: callback)) + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/Scene+Environment.swift similarity index 92% rename from Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift rename to Sources/OpenSwiftUI/Modifier/SceneModifier/Scene+Environment.swift index 2eff13dc3..5113ef47f 100644 --- a/Sources/OpenSwiftUI/Modifier/SceneModifier/Environment+Scene.swift +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/Scene+Environment.swift @@ -1,9 +1,17 @@ +// +// Scene+Environment.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + #if OPENSWIFTUI_OPENCOMBINE public import OpenCombine #else public import Combine #endif public import OpenObservation +import OpenSwiftUICore extension Scene { @@ -36,7 +44,7 @@ extension Scene { /// the scene's subhierarchy. @available(OpenSwiftUI_v5_0, *) nonisolated public func environmentObject(_ object: T) -> some Scene where T: ObservableObject { - _openSwiftUIUnimplementedFailure() + environment(T.environmentStore, object) } } @@ -88,7 +96,7 @@ extension Scene { /// /// - Returns: A scene that has the specified object in its environment. @available(OpenSwiftUI_v5_0, *) - nonisolated public func environment(_ object: T?) -> some View where T: AnyObject, T: Observable { - _openSwiftUIUnimplementedFailure() + nonisolated public func environment(_ object: T?) -> some Scene where T: AnyObject, T: Observable { + environment(\EnvironmentValues[EnvironmentObjectKey()], object) } } diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/SceneActivationConditions.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/SceneActivationConditions.swift new file mode 100644 index 000000000..4f7846b39 --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/SceneActivationConditions.swift @@ -0,0 +1,165 @@ +// +// SceneActivationConditions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 8A419FB041C333B8D8BE93F2E426006E (SwiftUI) + +import OpenAttributeGraphShims +import OpenSwiftUICore + +// MARK: - Scene + handlesExternalEvents + +@available(OpenSwiftUI_v2_0, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +extension Scene { + + /// Specifies the external events for which OpenSwiftUI opens a new instance + /// of the modified scene. + /// + /// When your app receives an external event like a user activity or a + /// URL, OpenSwiftUI routes the event to a scene for processing. OpenSwiftUI + /// selects the scene that receives the event according to the following + /// rules, which it evaluates in order until it finds a destination scene: + /// + /// * On platforms that support only a single scene per app, send + /// the event to the one open scene. + /// * Find an open scene that indicates it prefers to or can handle the + /// event, if any, and send the event to that scene. You use the + /// ``View/handlesExternalEvents(preferring:allowing:)`` view modifier + /// on a view inside the scene to register this preference. + /// * Find a scene declaration with a `handlesExternalEvents(matching:)` + /// scene modifier containing `conditions` that match the external event. + /// Create a new instance of the first scene that matches and route the + /// event there. + /// * Find the first scene declaration that doesn't have the scene modifier. + /// Create a new instance of this scene and route the event there. + /// + /// Make sure that at least one of these rules succeeds in your app for all + /// events that your app claims to handle. Also, make sure + /// that the scene that receives an event actually handles it. For example, + /// be sure that a scene that receives user activities handles them with an + /// appropriate ``View/onContinueUserActivity(_:perform:)`` view modifier. + /// + /// Don't confuse the `handlesExternalEvents(matching:)` scene + /// modifier with the ``View/handlesExternalEvents(preferring:allowing:)`` + /// _view_ modifier. You use the scene modifier to help OpenSwiftUI choose a + /// new scene to open when no open scene handles an external event, + /// whereas you use the view modifier to indicate that an open scene can + /// or prefers to handle certain events. + /// + /// ### Matching an event + /// + /// To find a scene type that handles a particular external event, OpenSwiftUI + /// compares a property of the event against the strings that you specify + /// in the `conditions` set. OpenSwiftUI examines the following event + /// properties to perform the comparison: + /// + /// * For an + /// [NSUserActivity](https://developer.apple.com/documentation/foundation/nsuseractivity), + /// like when your app handles Handoff, OpenSwiftUI uses the activity's + /// [targetContentIdentifier](https://developer.apple.com/documentation/foundation/nsuseractivity/3238062-targetcontentidentifier) + /// property, or if that's `nil`, its + /// [webpageURL](https://developer.apple.com/documentation/foundation/nsuseractivity/1418086-webpageurl) + /// property rendered as an + /// [absoluteString](https://developer.apple.com/documentation/foundation/url/1779984-absolutestring). + /// * For a + /// [URL](https://developer.apple.com/documentation/foundation/url), + /// like when another process opens a URL that your app handles, + /// OpenSwiftUI uses the URL's + /// [absoluteString](https://developer.apple.com/documentation/foundation/url/1779984-absolutestring). + /// + /// An empty set of strings never matches. Similarly, empty strings never + /// match. Conversely, as a special case, the string that contains only an + /// asterisk (`*`) matches anything. The modifier performs string + /// comparisons that are case and diacritic insensitive. + /// + /// > Important: ``DocumentGroup`` scenes ignore this modifier. Instead, + /// document scenes decide whether to open a new scene to handle an + /// external event by comparing the incoming URL or user activity's + /// [webpageURL](https://developer.apple.com/documentation/foundation/nsuseractivity/1418086-webpageurl) + /// against the document group's supported types. + /// + /// ### Choosing a window to open + /// + /// The following example shows an app with a photo browser scene + /// that displays a collection of photos, and a photo detail scene that + /// enables closer examination of a particular photo: + /// + /// @main + /// struct MyPhotos: App { + /// var body: some Scene { + /// WindowGroup { + /// PhotosBrowser() + /// } + /// + /// WindowGroup("Photo") { + /// PhotoDetail() + /// } + /// .handlesExternalEvents(matching: ["photoIdentifier="]) + /// } + /// } + /// + /// The app uses the `handlesExternalEvents(matching:)` modifier on the + /// second scene to ensure that an external event with an identifier + /// that contains the string `photoIdentifier=` creates a new scene of + /// the second type. Other events, if not handled by an open scene, + /// cause the creation of a new browser window instead. + /// + /// - Parameter conditions: A set of strings that OpenSwiftUI compares against + /// the incoming user activity or URL to see if OpenSwiftUI + /// can open a new scene instance to handle the external event. + /// + /// - Returns: A scene type that limits the kinds of external events for + /// which OpenSwiftUI opens a new instance. + nonisolated public func handlesExternalEvents(matching conditions: Set) -> some Scene { + modifier(ActivationConditionsModifier(conditions: conditions)) + } +} + +// MARK: - ActivationConditionsModifier + +private struct ActivationConditionsModifier: PrimitiveSceneModifier { + var conditions: Set + + static func _makeScene( + modifier: _GraphValue, + inputs: _SceneInputs, + body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs + ) -> _SceneOutputs { + var outputs = body(_Graph(), inputs) + if let list = outputs.preferences.sceneList { + outputs.preferences.sceneList = Attribute( + ApplyActivationConditions( + conditions: modifier[offset: { .of(&$0.conditions) }].value, + list: list + ) + ) + } + return outputs + } +} + +// MARK: - ApplyActivationConditions + +private struct ApplyActivationConditions: Rule { + @Attribute var conditions: Set + @Attribute var list: SceneList + + var value: SceneList { + var result = SceneList(items: []) + for item in list.items { + switch item.value { + case .singleWindow, .windowGroup: + var item = item + item.activationConditions = conditions + result.items.append(item) + default: + result.items.append(item) + } + } + return result + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/ValueActionSceneModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/ValueActionSceneModifier.swift index ba299d0f8..139536b17 100644 --- a/Sources/OpenSwiftUI/Modifier/SceneModifier/ValueActionSceneModifier.swift +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/ValueActionSceneModifier.swift @@ -3,51 +3,258 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: WIP +// Status: Complete +import OpenAttributeGraphShims package import OpenSwiftUICore +// MARK: - Scene + onChange (deprecated) + @available(OpenSwiftUI_v2_0, *) extension Scene { + + /// Adds an action to perform when the given value changes. + /// + /// Use this modifier to trigger a side effect when a value changes, like + /// the value associated with an ``OpenSwiftUI/Environment`` value or a + /// ``OpenSwiftUI/Binding``. For example, you can clear a cache when you notice + /// that a scene moves to the background: + /// + /// struct MyScene: Scene { + /// @Environment(\.scenePhase) private var scenePhase + /// @StateObject private var cache = DataCache() + /// + /// var body: some Scene { + /// WindowGroup { + /// MyRootView() + /// } + /// .onChange(of: scenePhase) { newScenePhase in + /// if newScenePhase == .background { + /// cache.empty() + /// } + /// } + /// } + /// } + /// + /// The system may call the action closure on the main actor, so avoid + /// long-running tasks in the closure. If you need to perform such tasks, + /// detach an asynchronous background task: + /// + /// .onChange(of: scenePhase) { newScenePhase in + /// if newScenePhase == .background { + /// Task.detached(priority: .background) { + /// // ... + /// } + /// } + /// } + /// + /// The system passes the new value into the closure. If you need the old + /// value, capture it in the closure. + /// + /// Important: This modifier is deprecated and has been replaced with new + /// versions that include either zero or two parameters within the closure, + /// unlike this version that includes one parameter. This deprecated version + /// and the new versions behave differently with respect to how they execute + /// the action closure, specifically when the closure captures other values. + /// Using the deprecated API, the closure is run with captured values that + /// represent the "old" state. With the replacement API, the closure is run + /// with captured values that represent the "new" state, which makes it + /// easier to correctly perform updates that rely on supplementary values + /// (that may or may not have changed) in addition to the changed value that + /// triggered the action. + /// + /// - Important: This modifier is deprecated and has been replaced with new + /// versions that include either zero or two parameters within the + /// closure, unlike this version that includes one parameter. This + /// deprecated version and the new versions behave differently with + /// respect to how they execute the action closure, specifically when the + /// closure captures other values. Using the deprecated API, the closure + /// is run with captured values that represent the "old" state. With the + /// replacement API, the closure is run with captured values that + /// represent the "new" state, which makes it easier to correctly perform + /// updates that rely on supplementary values (that may or may not have + /// changed) in addition to the changed value that triggered the action. + /// + /// - Parameters: + /// - value: The value to check when determining whether to run the + /// closure. The value must conform to the + /// [Equatable](https://developer.apple.com/documentation/swift/equatable) + /// protocol. + /// - action: A closure to run when the value changes. The closure + /// provides a single `newValue` parameter that indicates the changed + /// value. + /// + /// - Returns: A scene that triggers an action in response to a change. @available(*, deprecated, message: "Use `onChange` with a two or zero parameter action closure instead.") @inlinable nonisolated public func onChange( of value: V, perform action: @escaping (_ newValue: V) -> Void ) -> some Scene where V: Equatable { - // modifier(_ValueActionModifier(value: value, action: action)) - return self + modifier(_ValueActionModifier(value: value, action: action)) } } +// MARK: - _ValueActionModifier + _SceneModifier + @available(OpenSwiftUI_v2_0, *) -extension _ValueActionModifier: _SceneModifier { +extension _ValueActionModifier: PrimitiveSceneModifier { @MainActor @preconcurrency public static func _makeScene( - modifier: _GraphValue<_ValueActionModifier>, + modifier: _GraphValue, inputs: _SceneInputs, body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs ) -> _SceneOutputs { - _openSwiftUIUnimplementedFailure() + let dispatcher = Attribute(ValueActionDispatcher( + modifier: modifier.value, + phase: inputs.base.phase + )) + dispatcher.flags = .transactional + return body(_Graph(), inputs) } } +// MARK: - Scene + onChange + @available(OpenSwiftUI_v5_0, *) extension Scene { + + /// Adds an action to perform when the given value changes. + /// + /// Use this modifier to trigger a side effect when a value changes, like + /// the value associated with an ``OpenSwiftUI/Environment`` key or a + /// ``OpenSwiftUI/Binding``. For example, you can clear a cache when you notice + /// that a scene moves to the background: + /// + /// struct MyScene: Scene { + /// @Environment(\.scenePhase) private var scenePhase + /// @StateObject private var cache = DataCache() + /// + /// var body: some Scene { + /// WindowGroup { + /// MyRootView(cache: cache) + /// } + /// .onChange(of: scenePhase) { oldScenePhase, newScenePhase in + /// if newScenePhase == .background { + /// cache.empty() + /// } + /// } + /// } + /// } + /// + /// The system may call the action closure on the main actor, so avoid + /// long-running tasks in the closure. If you need to perform such tasks, + /// detach an asynchronous background task: + /// + /// .onChange(of: scenePhase) { oldScenePhase, newScenePhase in + /// if newScenePhase == .background { + /// Task.detached(priority: .background) { + /// // ... + /// } + /// } + /// } + /// + /// When the value changes, the new version of the closure will be called, + /// so any captured values will have their values from the time that the + /// observed value has its new value. The system passes the old and new + /// observed values into the closure. + /// + /// - Parameters: + /// - value: The value to check when determining whether to run the + /// closure. The value must conform to the + /// [Equatable](https://developer.apple.com/documentation/swift/equatable) + /// protocol. + /// - initial: Whether the action should be run when this scene initially + /// appears. + /// - action: A closure to run when the value changes. + /// - oldValue: The old value that failed the comparison check (or the + /// initial value when requested). + /// - newValue: The new value that failed the comparison check. + /// + /// - Returns: A scene that triggers an action in response to a change. nonisolated public func onChange( of value: V, initial: Bool = false, _ action: @escaping (_ oldValue: V, _ newValue: V) -> Void ) -> some Scene where V: Equatable { - _openSwiftUIUnimplementedFailure() + let s = modifier(_ValueActionModifier2(value: value, action: action)) + let appear = initial ? { action(value, value) } : nil + return s.modifier(_AppearanceActionModifier(appear: appear, disappear: nil)) } + /// Adds an action to perform when the given value changes. + /// + /// Use this modifier to trigger a side effect when a value changes, like + /// the value associated with an ``OpenSwiftUI/Environment`` key or a + /// ``OpenSwiftUI/Binding``. For example, you can clear a cache when you notice + /// that a scene moves to the background: + /// + /// struct MyScene: Scene { + /// @Environment(\.locale) private var locale + /// @StateObject private var cache = LocalizationDataCache() + /// + /// var body: some Scene { + /// WindowGroup { + /// MyRootView(cache: cache) + /// } + /// .onChange(of: locale) { + /// cache.empty() + /// } + /// } + /// } + /// + /// The system may call the action closure on the main actor, so avoid + /// long-running tasks in the closure. If you need to perform such tasks, + /// detach an asynchronous background task: + /// + /// .onChange(of: locale) { + /// Task.detached(priority: .background) { + /// // ... + /// } + /// } + /// + /// When the value changes, the new version of the closure will be called, + /// so any captured values will have their values from the time that the + /// observed value has its new value. + /// + /// - Parameters: + /// - value: The value to check when determining whether to run the + /// closure. The value must conform to the + /// [Equatable](https://developer.apple.com/documentation/swift/equatable) + /// protocol. + /// - initial: Whether the action should be run when this scene initially + /// appears. + /// - action: A closure to run when the value changes. + /// + /// - Returns: A scene that triggers an action in response to a change. nonisolated public func onChange( of value: V, initial: Bool = false, _ action: @escaping () -> Void ) -> some Scene where V: Equatable { - _openSwiftUIUnimplementedFailure() + let s = modifier(_ValueActionModifier2(value: value, action: { _, _ in action() })) + let appear = initial ? action : nil + return s.modifier(_AppearanceActionModifier(appear: appear, disappear: nil)) + } +} + +// MARK: - _ValueActionModifier2 + _SceneModifier + +@available(OpenSwiftUI_v2_0, *) +extension _ValueActionModifier2: PrimitiveSceneModifier { + @MainActor + @preconcurrency + package static func _makeScene( + modifier: _GraphValue, + inputs: _SceneInputs, + body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs + ) -> _SceneOutputs { + let dispatcher = Attribute(ValueActionDispatcher( + modifier: modifier.value, + phase: inputs.base.phase + )) + dispatcher.flags = .transactional + return body(_Graph(), inputs) } } diff --git a/Sources/OpenSwiftUICore/Data/Combine/Environment+Objects.swift b/Sources/OpenSwiftUICore/Data/Combine/Environment+Objects.swift index b448c849e..3d783aa65 100644 --- a/Sources/OpenSwiftUICore/Data/Combine/Environment+Objects.swift +++ b/Sources/OpenSwiftUICore/Data/Combine/Environment+Objects.swift @@ -14,23 +14,23 @@ import Combine // MARK: - EnvironmentObjectKey -struct EnvironmentObjectKey: EnvironmentKey, Hashable where ObjectType: AnyObject { - init() { +package struct EnvironmentObjectKey: EnvironmentKey, Hashable where ObjectType: AnyObject { + package init() { _openSwiftUIEmptyStub() } - static var defaultValue: ObjectType? { nil } + package static var defaultValue: ObjectType? { nil } } // MARK: - EnvironmentValues + EnvironmentObjectKey extension EnvironmentValues { - subscript(_: EnvironmentObjectKey) -> ObjectType? where ObjectType: AnyObject { + package subscript(_: EnvironmentObjectKey) -> ObjectType? where ObjectType: AnyObject { get { self[objectType: ObjectType.self] } set { self[objectType: ObjectType.self] = newValue } } - subscript(forceUnwrapping key: EnvironmentObjectKey) -> ObjectType where ObjectType: AnyObject { + package subscript(forceUnwrapping key: EnvironmentObjectKey) -> ObjectType where ObjectType: AnyObject { get { guard let object = self[key] else { preconditionFailure("No Observable object of type \(ObjectType.self) found. A View.environmentObject(_:) for \(ObjectType.self) may be missing as an ancestor of this view.") @@ -42,7 +42,7 @@ extension EnvironmentValues { } } - subscript(objectType _: ObjectType.Type) -> ObjectType? where ObjectType: AnyObject { + package subscript(objectType _: ObjectType.Type) -> ObjectType? where ObjectType: AnyObject { get { self[EnvironmentObjectKey.self] } set { self[EnvironmentObjectKey.self] = newValue } } diff --git a/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift index f9670f6fc..c1c613866 100644 --- a/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift +++ b/Sources/OpenSwiftUICore/Data/Combine/EnvironmentObject.swift @@ -231,7 +231,7 @@ extension View { @available(OpenSwiftUI_v1_0, *) extension ObservableObject { @usableFromInline - static var environmentStore: WritableKeyPath { + package static var environmentStore: WritableKeyPath { \EnvironmentValues[EnvironmentObjectKey()] } diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index 8f522e966..af73d7fdf 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -119,9 +119,10 @@ package enum Update { @discardableResult package static func enqueueAction( - reason: ()?, // FIXME + reason: (CustomEventTrace.ActionEventType.Reason)?, _ action: @escaping () -> Void ) -> UInt32 { + // FIXME enqueueAction(action) return .zero } @@ -230,6 +231,12 @@ package enum Update { // FIXME package enum CustomEventTrace { package enum ActionEventType { - package enum Reason {} + package enum Reason { + case onAppear + case onChange + case onDisappear + case gesture + case didReleaseButton + } } } diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/ValueActionModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/ValueActionModifier.swift index 80b669095..aa6ede7f3 100644 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/ValueActionModifier.swift +++ b/Sources/OpenSwiftUICore/Modifier/ViewModifier/ValueActionModifier.swift @@ -6,7 +6,7 @@ // Status: Complete // ID: 4B528D9D60F208F316B29B7D53AC1FB9 (SwiftUICore) -import OpenAttributeGraphShims +package import OpenAttributeGraphShims // MARK: - View + onChange @@ -118,7 +118,7 @@ public struct _ValueActionModifier: ViewModifier, PrimitiveViewModifier, (self.value, self.action) = (value, action) } - func sendAction(old: Self?) { + package func sendAction(old: Self?) { let action = (old ?? self).action action(value) } @@ -162,7 +162,7 @@ extension _ValueActionModifier: Sendable {} // MARK: - ValueActionModifierProtocol -protocol ValueActionModifierProtocol { +package protocol ValueActionModifierProtocol { associatedtype Value: Equatable var value: Value { get } @@ -172,14 +172,14 @@ protocol ValueActionModifierProtocol { // MARK: - ValueActionDispatcher -struct ValueActionDispatcher: StatefulRule, AsyncAttribute where Modifier: ValueActionModifierProtocol { +package struct ValueActionDispatcher: StatefulRule, AsyncAttribute where Modifier: ValueActionModifierProtocol { @Attribute var modifier: Modifier @Attribute var phase: _GraphInputs.Phase var oldValue: Modifier? var lastResetSeed: UInt32 = .zero var cycleDetector: UpdateCycleDetector = .init() - init( + package init( modifier: Attribute, phase: Attribute<_GraphInputs.Phase> ) { @@ -187,9 +187,9 @@ struct ValueActionDispatcher: StatefulRule, AsyncAttribute where Modif self._phase = phase } - typealias Value = Void + package typealias Value = Void - mutating func updateValue() { + package mutating func updateValue() { if lastResetSeed != phase.resetSeed { lastResetSeed = phase.resetSeed oldValue = nil @@ -207,7 +207,7 @@ struct ValueActionDispatcher: StatefulRule, AsyncAttribute where Modif return } let oldValue = oldValue - Update.enqueueAction { // FIXME: Update.enqueueAction(reason:) + Update.enqueueAction(reason: .onChange) { newValue.sendAction(old: oldValue) } } @@ -340,21 +340,21 @@ extension View { // MARK: - ValueActionModifier2 -struct _ValueActionModifier2: ViewModifier, PrimitiveViewModifier, ValueActionModifierProtocol where Value: Equatable { - var value: Value +package struct _ValueActionModifier2: ViewModifier, PrimitiveViewModifier, ValueActionModifierProtocol where Value: Equatable { + package var value: Value var action: (Value, Value) -> () - init(value: Value, action: @escaping (Value, Value) -> Void) { + package init(value: Value, action: @escaping (Value, Value) -> Void) { self.value = value self.action = action } - func sendAction(old: Self?) { + package func sendAction(old: Self?) { action((old ?? self).value, value) } - nonisolated static func _makeView( + nonisolated package static func _makeView( modifier: _GraphValue, inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs @@ -367,7 +367,7 @@ struct _ValueActionModifier2: ViewModifier, PrimitiveViewModifier, ValueA return body(_Graph(), inputs) } - nonisolated static func _makeViewList( + nonisolated package static func _makeViewList( modifier: _GraphValue, inputs: _ViewListInputs, body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs @@ -467,7 +467,7 @@ private struct ValueActionDispatcher3: StatefulRule, AsyncAttribute where return } let transaction = Graph.withoutUpdate { self.transaction } - Update.enqueueAction { // FIXME: Update.enqueueAction(reason:) + Update.enqueueAction(reason: .onChange) { modifier.action(oldValue, newValue, transaction) } }