From 8ee494487e5116b3e78aba2fe1ac1415a30d9f06 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Feb 2026 02:55:46 +0800 Subject: [PATCH 1/5] Add handlesExternalEvents and openURL API --- .../OpenSwiftUI/App/Scene/SceneBridge.swift | 448 +++++++++++++++++- 1 file changed, 442 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift index 56c07a2a2..287490355 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift @@ -3,14 +3,14 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: Complete +// Status: Complete - UserActivityModifier WIP // ID: A9714FE7FB47B9EE521B92A735A59E38 (SwiftUI) #if canImport(Darwin) #if os(iOS) || os(visionOS) -import UIKit +public import UIKit #elseif os(macOS) -import AppKit +public import AppKit #endif #if OPENSWIFTUI_OPENCOMBINE import OpenCombine @@ -22,7 +22,8 @@ import OpenSwiftUICore // MARK: - UserActivityTrackingInfo -var _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled = false +@available(OpenSwiftUI_v2_0, *) +public var _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled = false class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { var userActivity: NSUserActivity? @@ -103,7 +104,7 @@ class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { // MARK: - SceneBridge -final class SceneBridge: CustomStringConvertible { +final class SceneBridge: ObservableObject, CustomStringConvertible { private var sceneBridgePublishers: [AnyHashable: [AnyHashable: PassthroughSubject]] = [:] var isAnimatingSceneResize: Bool = false #if os(iOS) || os(visionOS) @@ -133,7 +134,16 @@ final class SceneBridge: CustomStringConvertible { var initialSceneSizeState: InitialSceneSizeState = .none private var enqueuedEvents: [String: [Any]] = [:] - private static var _devNullSceneBridge: SceneBridge? + fileprivate static var _devNullSceneBridge: SceneBridge? + + fileprivate static func sceneBridgePublisher( + _ type: Any.Type, + identifier: String, + sceneBridge: SceneBridge + ) -> PassthroughSubject { + // TODO + .init() + } init() { _openSwiftUIEmptyStub() @@ -431,4 +441,430 @@ final class SceneBridge: CustomStringConvertible { } #endif } + +// MARK: - SceneBridgeReader + +private struct SceneBridgeReader: View where V: View { + @Environment private var sceneBridge: SceneBridge? + var handler: (SceneBridge) -> V + + init( + sceneBridge: Environment = .init(SceneBridge.environmentStore), + handler: @escaping (SceneBridge) -> V + ) { + self._sceneBridge = sceneBridge + self.handler = handler + } + + var body: some View { + let sceneBridge = sceneBridge + let bridge: SceneBridge + if let sceneBridge { + bridge = sceneBridge + } else { + Log.externalWarning("Cannot use Scene methods for URL, NSUserActivity, and other External Events without using OpenSwiftUI Lifecycle. Without OpenSwiftUI Lifecycle, advertising and handling External Events wastes resources, and will have unpredictable results.") + if SceneBridge._devNullSceneBridge == nil { + SceneBridge._devNullSceneBridge = SceneBridge() + } + bridge = SceneBridge._devNullSceneBridge! + } + return handler(bridge) + } +} + +// MARK: - UserActivityModifier [WIP] + +struct UserActivityModifier: ViewModifier { + let activityType: String + let isActive: Bool + let update: (NSUserActivity) -> () + @State private var info: UserActivityTrackingInfo? + + var scrapeableAttachment: ScrapeableContent.Content? { + guard isActive else { + return nil + } + let activity: NSUserActivity + if let info, + info.activityType == activityType, + let userActivity = info.userActivity { + activity = userActivity + } else { + activity = NSUserActivity(activityType: activityType) + } + return .userActivity(activity) + } + + func body(content: Content) -> some View { + // TODO: + // macOS: ScrapeableAttachmentViewModifier + // iOS: IdentifiedPreferenceTransformModifier + _openSwiftUIUnimplementedFailure() + } +} + +@available(OpenSwiftUI_v2_0, *) +extension View { + + /// Advertises a user activity type. + /// + /// You can use `userActivity(_:isActive:_:)` to start, stop, or modify the + /// advertisement of a specific type of user activity. + /// + /// The scope of the activity applies only to the scene or window the + /// view is in. + /// + /// - Parameters: + /// - activityType: The type of activity to advertise. + /// - isActive: When `false`, avoids advertising the activity. Defaults + /// to `true`. + /// - update: A function that modifies the passed-in activity for + /// advertisement. + nonisolated public func userActivity( + _ activityType: String, + isActive: Bool = true, + _ update: @escaping (NSUserActivity) -> () + ) -> some View { + modifier( + UserActivityModifier( + activityType: activityType, + isActive: isActive, + update: update + ) + ) + } + + /// Advertises a user activity type. + /// + /// The scope of the activity applies only to the scene or window the + /// view is in. + /// + /// - Parameters: + /// - activityType: The type of activity to advertise. + /// - element: If the element is `nil`, the handler will not be + /// associated with the activity (and if there are no handlers, no + /// activity is advertised). The method passes the non-`nil` element to + /// the handler as a convenience so the handlers don't all need to + /// implement an early exit with + /// `guard element = element else { return }`. + /// - update: A function that modifies the passed-in activity for + /// advertisement. + nonisolated public func userActivity

( + _ activityType: String, + element: P?, + _ update: @escaping (P, NSUserActivity) -> () + ) -> some View { + userActivity( + activityType, + isActive: element == nil + ) { activity in + guard let element else { return } + update(element, activity) + } + } + + /// Registers a handler to invoke in response to a user activity that your + /// app receives. + /// + /// Use this view modifier to receive + /// [NSUserActivity](https://developer.apple.com/documentation/foundation/nsuseractivity) + /// instances in a particular scene within your app. The scene that OpenSwiftUI + /// routes the incoming user activity to depends on the structure of your + /// app, what scenes are active, and other configuration. For more + /// information, see ``Scene/handlesExternalEvents(matching:)``. + /// + /// UI frameworks traditionally pass Universal Links to your app using a + /// user activity. However, OpenSwiftUI passes a Universal Link to your app + /// directly as a URL. To receive a Universal Link, use the + /// ``View/onOpenURL(perform:)`` modifier instead. + /// + /// - Parameters: + /// - activityType: The type of activity that the `action` closure + /// handles. Be sure that this string matches one of the values that + /// you list in the + /// [NSUserActivityTypes](https://developer.apple.com/documentation/bundleresources/information_property_list/nsuseractivitytypes) + /// array in your app's Information Property List. + /// - action: A closure that OpenSwiftUI calls when your app receives a user + /// activity of the specified type. The closure takes the activity as + /// an input parameter. + /// + /// - Returns: A view that handles incoming user activities. + nonisolated public func onContinueUserActivity( + _ activityType: String, + perform action: @escaping (NSUserActivity) -> () + ) -> some View { + SceneBridgeReader { bridge in + let publisher = SceneBridge.sceneBridgePublisher( + NSUserActivity.self, + identifier: activityType, + sceneBridge: bridge + ) + return self.onReceive(publisher) { output in + guard let activity = output as? NSUserActivity else { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "onUserActivity skipping event with " + + "identifier \(activityType), published object is not " + + "a NSUserActivity: \(output)" + ) + } + return + } + action(activity) + } + } + } + + /// Registers a handler to invoke in response to a URL that your app + /// receives. + /// + /// Use this view modifier to receive URLs in a particular scene within your + /// app. The scene that OpenSwiftUI routes the incoming URL to depends on the + /// structure of your app, what scenes are active, and other configuration. + /// For more information, see ``Scene/handlesExternalEvents(matching:)``. + /// + /// UI frameworks traditionally pass Universal Links to your app using an + /// [NSUserActivity](https://developer.apple.com/documentation/foundation/nsuseractivity). + /// However, OpenSwiftUI passes a Universal Link to your app directly as a URL, + /// which you receive using this modifier. To receive other user activities, + /// like when your app participates in Handoff, use the + /// ``View/onContinueUserActivity(_:perform:)`` modifier instead. + /// + /// For more information about linking into your app, see + /// [Allowing Apps and Websites to Link to Your Content](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content). + /// + /// - Parameter action: A closure that OpenSwiftUI calls when your app receives + /// a Universal Link or a custom + /// [URL](https://developer.apple.com/documentation/foundation/url). + /// The closure takes the URL as an input parameter. + /// + /// - Returns: A view that handles incoming URLs. + nonisolated public func onOpenURL(perform action: @escaping (URL) -> ()) -> some View { + SceneBridgeReader { bridge in + let publisher = SceneBridge.sceneBridgePublisher( + OpenURLContext.self, + identifier: "OpenURLContext", + sceneBridge: bridge + ) + return self.onReceive(publisher) { output in + guard let context = output as? OpenURLContext else { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "onURL skipping event for OpenURLContext, " + + "published object is not a OpenURLContext: \(output)" + ) + } + return + } + action(context.url) + } + } + } + + #if os(iOS) || os(visionOS) + @_spi(Private) + @available(OpenSwiftUI_v4_0, *) + @available(macOS, unavailable) + @available(watchOS, unavailable) + nonisolated public func onOpenURL(perform action: @escaping (URL, OpenURLOptions?) -> ()) -> some View { + SceneBridgeReader { bridge in + let publisher = SceneBridge.sceneBridgePublisher( + OpenURLContext.self, + identifier: "OpenURLContext", + sceneBridge: bridge + ) + return self.onReceive(publisher) { output in + guard let context = output as? OpenURLContext else { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "onURL skipping event for OpenURLContext, " + + "published object is not a OpenURLContext: \(output)" + ) + } + return + } + action(context.url, context.options) + } + } + } + #endif +} + +@available(OpenSwiftUI_v2_0, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +extension View { + + /// Specifies the external events that the view's scene handles + /// if the scene is already open. + /// + /// Apply this modifier to a view to indicate whether an open scene that + /// contains the view handles specified user activities or URLs that your + /// app receives. Specify two sets of string identifiers to distinguish + /// between the kinds of events that the scene _prefers_ to handle + /// and those that it _can_ handle. You can dynamically update the + /// identifiers in each set to reflect changing app state. + /// + /// When your app receives an event on a platform that supports multiple + /// simultaneous scenes, OpenSwiftUI sends the event to the first + /// open scene it finds that prefers to handle the event. Otherwise, + /// it sends the event to the first open scene it finds that can handle + /// the event. If it finds neither --- including when you don't add + /// this view modifier --- OpenSwiftUI creates a new scene for the event. + /// + /// > Important: Don't confuse this view modifier with the + /// ``Scene/handlesExternalEvents(matching:)`` _scene_ modifier. You use + /// the view modifier to indicate that an open scene handles certain + /// events, whereas you use the scene modifier to help OpenSwiftUI choose a + /// new scene to open when no open scene handles the event. + /// + /// On platforms that support only a single scene, OpenSwiftUI ignores this + /// modifier and sends all external events to the one open scene. + /// + /// ### Matching an event + /// + /// To find an open scene that handles a particular external event, OpenSwiftUI + /// compares a property of the event against the strings that you specify + /// in the `preferring` and `allowing` sets. 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). + /// + /// For both parameter sets, 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. + /// + /// If you specify multiple instances of this view modifier inside a single + /// scene, the scene uses the union of the respective `preferring` and + /// `allowing` sets from all the modifiers. + /// + /// ### Handling a user activity in an open scene + /// + /// As an example, the following view --- which participates in Handoff + /// through the ``View/userActivity(_:isActive:_:)`` and + /// ``View/onContinueUserActivity(_:perform:)`` methods --- updates its + /// `preferring` set according to the current selection. The enclosing + /// scene prefers to handle an event for a contact that's already selected, + /// but doesn't volunteer itself as a preferred scene when no contact is + /// selected: + /// + /// private struct ContactList: View { + /// var store: ContactStore + /// @State private var selectedContact: UUID? + /// + /// var body: some View { + /// NavigationSplitView { + /// List(store.contacts, selection: $selectedContact) { contact in + /// NavigationLink(contact.name) { + /// Text(contact.name) + /// } + /// } + /// } detail: { + /// Text("Select a contact") + /// } + /// .handlesExternalEvents( + /// preferring: selectedContact == nil + /// ? [] + /// : [selectedContact!.uuidString], + /// allowing: selectedContact == nil + /// ? ["*"] + /// : [] + /// ) + /// .onContinueUserActivity(Contact.userActivityType) { activity in + /// if let identifier = activity.targetContentIdentifier { + /// selectedContact = UUID(uuidString: identifier) + /// } + /// } + /// .userActivity( + /// Contact.userActivityType, + /// isActive: selectedContact != nil + /// ) { activity in + /// activity.title = "Contact" + /// activity.targetContentIdentifier = selectedContact?.uuidString + /// activity.isEligibleForHandoff = true + /// } + /// } + /// } + /// + /// The above code also updates the `allowing` set to indicate that the + /// scene can handle any incoming event when there's no current selection, + /// but that it can't handle any event if the view already displays a + /// contact. The `preferring` set takes precedence in the special case + /// where the incoming event exactly matches the currently selected contact. + /// + /// - Parameters: + /// - preferring: A set of strings that OpenSwiftUI compares against the + /// incoming user activity or URL to see if the view's + /// scene prefers to handle the external event. + /// - allowing: A set of strings that OpenSwiftUI compares against the + /// incoming user activity or URL to see if the view's + /// scene can handle the exernal event. + /// + /// - Returns: A view whose enclosing scene --- if already open --- + /// handles incoming external events. + nonisolated public func handlesExternalEvents( + preferring: Set, + allowing: Set + ) -> some View { + transformPreference( + SceneBridge.ActivationConditionsPreferenceKey.self + ) { value in + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "TransformPreference closure for activation conditions called " + + "\(String(describing: value))" + ) + } + value = .init(( + preferring: value?.preferring.union(preferring) ?? preferring, + allowing: value?.allowing.union(allowing) ?? allowing, + )) + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "TransformPreference setting value for activation conditions" + + "\(String(describing: value))" + ) + } + } + } +} + +// MARK: - OpenURLOptions + +@_spi(Private) +@available(OpenSwiftUI_v4_0, *) +@available(macOS, unavailable) +@available(watchOS, unavailable) +public struct OpenURLOptions { + #if os(iOS) || os(visionOS) + public var uiSceneOpenURLOptions: UIScene.OpenURLOptions + #endif +} + +@_spi(Private) +@available(*, unavailable) +extension OpenURLOptions: Sendable {} + +// MARK: - OpenURLContext + +struct OpenURLContext { + var url: URL + #if os(iOS) || os(visionOS) + var options: OpenURLOptions? + #endif +} #endif From 27c4644d36b7ba300f60b090b4ce8be5ee83f8f1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Feb 2026 03:18:28 +0800 Subject: [PATCH 2/5] Implement sceneBridgePublisher --- .../OpenSwiftUI/App/Scene/SceneBridge.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift index 287490355..45c21f05f 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift @@ -141,8 +141,24 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { identifier: String, sceneBridge: SceneBridge ) -> PassthroughSubject { - // TODO - .init() + let publishers = sceneBridge.sceneBridgePublishers[AnyHashable(ObjectIdentifier(type))] + guard let publishers, + let subject = publishers[AnyHashable(identifier)] else { + let subject = PassthroughSubject() + let newPublishers: [AnyHashable: PassthroughSubject] + if var publishers { + publishers[AnyHashable(identifier)] = subject + newPublishers = publishers + } else { + newPublishers = [AnyHashable(identifier): subject] + } + sceneBridge.sceneBridgePublishers[AnyHashable(ObjectIdentifier(type))] = newPublishers + DispatchQueue.main.async { + sceneBridge.flushEnqueuedEvents(for: identifier, type: type) + } + return subject + } + return subject } init() { From 99614ef39a4c49ee8ada6094a7c2918fc6cce404 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Feb 2026 13:14:05 +0800 Subject: [PATCH 3/5] Add IdentifiedPreference --- .../ViewModifier/IdentifiedPreference.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift diff --git a/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift b/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift new file mode 100644 index 000000000..d5f03d85e --- /dev/null +++ b/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift @@ -0,0 +1,56 @@ +// +// IdentifiedPreference.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: BADADABA7CFDAF5EFDACD96BEDF6E8F3 (SwiftUI) + +import OpenAttributeGraphShims +import OpenSwiftUICore + +// MARK: - IdentifiedPreferenceTransformModifier + +struct IdentifiedPreferenceTransformModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier where Key: PreferenceKey { + var transform: (inout Key.Value, ViewIdentity) -> Void + + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + let value = modifier.value + let phase = inputs.viewPhase + var outputs = body(_Graph(), inputs) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: Key.self, + transform: Attribute( + Transform( + modifier: value, + phase: phase, + helper: .init(id: .invalid, resetSeed: 0) + ) + ) + ) + return outputs + } + + // MARK: - IdentifiedPreferenceTransformModifier.Transform + + private struct Transform: StatefulRule, AsyncAttribute { + @Attribute var modifier: IdentifiedPreferenceTransformModifier + @Attribute var phase: ViewPhase + var helper: ViewIdentity.Tracker + + typealias Value = (inout Key.Value) -> Void + + mutating func updateValue() { + let (id, _) = helper.update(for: phase) + let transform = modifier.transform + value = { value in + transform(&value, id) + } + } + } +} From 7a0d0dfc9238e5a6358334fc1abfa743b97d7187 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Feb 2026 16:08:41 +0800 Subject: [PATCH 4/5] Add UserActivityModifier --- .../OpenSwiftUI/App/Scene/SceneBridge.swift | 73 +++++++++++++++++-- .../ViewModifier/IdentifiedPreference.swift | 11 +++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift index 45c21f05f..4a89c317e 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: Complete - UserActivityModifier WIP +// Status: Complete // ID: A9714FE7FB47B9EE521B92A735A59E38 (SwiftUI) #if canImport(Darwin) @@ -20,11 +20,13 @@ import Combine @_spi(Private) import OpenSwiftUICore -// MARK: - UserActivityTrackingInfo +// MARK: - Logging @available(OpenSwiftUI_v2_0, *) public var _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled = false +// MARK: - UserActivityTrackingInfo + class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { var userActivity: NSUserActivity? var handlers: [ViewIdentity: (NSUserActivity) -> Bool] = [:] @@ -488,7 +490,7 @@ private struct SceneBridgeReader: View where V: View { } } -// MARK: - UserActivityModifier [WIP] +// MARK: - UserActivityModifier struct UserActivityModifier: ViewModifier { let activityType: String @@ -512,15 +514,72 @@ struct UserActivityModifier: ViewModifier { } func body(content: Content) -> some View { - // TODO: - // macOS: ScrapeableAttachmentViewModifier - // iOS: IdentifiedPreferenceTransformModifier - _openSwiftUIUnimplementedFailure() + SceneBridgeReader { bridge in + content + .advertiseUserActivity(activityType, isActive: isActive, sceneBridge: bridge) { activity in + guard isActive else { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log("Skipping inactive advertiseUserActivity handler") + } + return false + } + update(activity) + return true + } + .onReceive( + SceneBridge.sceneBridgePublisher( + UserActivityTrackingInfo?.self, + identifier: "UserActivityTrackingInfo", + sceneBridge: bridge + ) + ) { output in + guard let output = output as? UserActivityTrackingInfo? else { + return + } + info = output + } + .scrapeableAttachment(scrapeableAttachment) + } } } @available(OpenSwiftUI_v2_0, *) extension View { + fileprivate func advertiseUserActivity( + _ activityType: String, + isActive: Bool, + sceneBridge: SceneBridge?, + handler: @escaping (NSUserActivity) -> Bool + ) -> some View { + transformIdentifiedPreference(SceneBridge.UserActivityPreferenceKey.self) { value, identity in + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "TransformIdentifiedPreference closure for UserActivity " + + "called with value \(String(describing: value))" + ) + } + guard isActive, let sceneBridge else { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log("TransformIdentifiedPreference closure for UserActivity: inactive, leaving value alone") + } + return + } + var handlers: [ViewIdentity: (NSUserActivity) -> Bool] + if let value, value.activityType == activityType { + handlers = value.handlers + handlers[identity] = handler + } else { + handlers = [identity: handler] + } + value = .init((activityType: activityType, handlers: handlers)) + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log( + "TransformIdentifiedPreference for UserActivity setting value " + + "to \(String(describing: value))" + ) + } + } + } /// Advertises a user activity type. /// diff --git a/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift b/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift index d5f03d85e..5d4d8fa85 100644 --- a/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift +++ b/Sources/OpenSwiftUI/Modifier/ViewModifier/IdentifiedPreference.swift @@ -9,6 +9,17 @@ import OpenAttributeGraphShims import OpenSwiftUICore +// MARK: - View + transformIdentityPreference + +extension View { + func transformIdentifiedPreference( + _ key: K.Type = K.self, + _ callback: @escaping (inout K.Value, ViewIdentity) -> Void + ) -> some View where K: PreferenceKey { + modifier(IdentifiedPreferenceTransformModifier(transform: callback)) + } +} + // MARK: - IdentifiedPreferenceTransformModifier struct IdentifiedPreferenceTransformModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier where Key: PreferenceKey { From d0aadb538513f9907bfb870cce7270690350dfe0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Feb 2026 16:13:06 +0800 Subject: [PATCH 5/5] Add activityEnvironmentLog --- .../OpenSwiftUI/App/Scene/SceneBridge.swift | 226 +++++++----------- 1 file changed, 92 insertions(+), 134 deletions(-) diff --git a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift index 4a89c317e..267b5c7d5 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift @@ -5,6 +5,7 @@ // Audited for 6.5.4 // Status: Complete // ID: A9714FE7FB47B9EE521B92A735A59E38 (SwiftUI) +// TODO: Add test case and verify [Q] #if canImport(Darwin) #if os(iOS) || os(visionOS) @@ -25,6 +26,13 @@ import OpenSwiftUICore @available(OpenSwiftUI_v2_0, *) public var _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled = false +@_transparent +private func activityEnvironmentLog(_ message: @autoclosure () -> String) { + if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { + Log.log(message()) + } +} + // MARK: - UserActivityTrackingInfo class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { @@ -44,9 +52,7 @@ class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { } func userActivityWillSave(_ userActivity: NSUserActivity) { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("userActivityWillSave called for \(description)") - } + activityEnvironmentLog("userActivityWillSave called for \(description)") if Thread.isMainThread { updateUserActivity(userActivity) } else { @@ -59,16 +65,12 @@ class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { func updateUserActivity(_ userActivity: NSUserActivity) { guard let currentActivity = self.userActivity, currentActivity == userActivity else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("Mismatched UserActivity in tracking info, skipping update.") - } + activityEnvironmentLog("Mismatched UserActivity in tracking info, skipping update.") return } guard let sceneBridge else { return } let failedIDs = handlers.compactMap { identity, handler in - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("Invoking handler for \(identity)") - } + activityEnvironmentLog("Invoking handler for \(identity)") return handler(userActivity) ? nil : identity } for id in failedIDs { @@ -95,12 +97,10 @@ class UserActivityTrackingInfo: NSObject, NSUserActivityDelegate { sceneBridge.userActivityTrackingInfo = self } userActivity.needsSave = false - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "updated user activity \(String(describing: userActivity.title)) " - + "with userInfo \(String(describing: userActivity.userInfo))" - ) - } + activityEnvironmentLog( + "updated user activity \(String(describing: userActivity.title)) " + + "with userInfo \(String(describing: userActivity.userInfo))" + ) } } @@ -215,20 +215,16 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { static var defaultValue: Value { nil } static func reduce(value: inout Value, nextValue: () -> Value) { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Reducing UserActivityPreference " + - "\(String(describing: value)) " + - "with \(String(describing: nextValue()))" - ) - } + activityEnvironmentLog( + "Reducing UserActivityPreference " + + "\(String(describing: value)) " + + "with \(String(describing: nextValue()))" + ) defer { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Reduced UserActivityPreference to " + - "\(String(describing: value))" - ) - } + activityEnvironmentLog( + "Reduced UserActivityPreference to " + + "\(String(describing: value))" + ) } guard let current = value else { value = nextValue() @@ -253,19 +249,15 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { let preferenceValue = preferences[UserActivityPreferenceKey.self] if let userActivityPreferenceSeed, preferenceValue.seed.matches(userActivityPreferenceSeed) { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "UserActivity Preferences hasn't changed, skipping update for advertised NSUserActivities. " + - "Seed is \(preferenceValue.seed)" - ) - } + activityEnvironmentLog( + "UserActivity Preferences hasn't changed, skipping update for advertised NSUserActivities. " + + "Seed is \(preferenceValue.seed)" + ) } else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "UserActivityPreferences changed: " + - "\(preferenceValue)" - ) - } + activityEnvironmentLog( + "UserActivityPreferences changed: " + + "\(preferenceValue)" + ) userActivityPreferenceSeed = preferenceValue.seed guard let value = preferenceValue.value, !value.handlers.isEmpty else { userActivityTrackingInfo = nil @@ -282,9 +274,7 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { initialUserActivity = nil } #endif - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("Cleared AdvertiseUserActivity tracking info since UserActivity preferences are empty") - } + activityEnvironmentLog("Cleared AdvertiseUserActivity tracking info since UserActivity preferences are empty") return } let trackingInfo = userActivityTrackingInfo ?? UserActivityTrackingInfo( @@ -302,12 +292,10 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { if activity !== oldActivity { activity.delegate = trackingInfo } - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Initializing advertised user activity: " + - "\(String(describing: trackingInfo.userActivity))" - ) - } + activityEnvironmentLog( + "Initializing advertised user activity: " + + "\(String(describing: trackingInfo.userActivity))" + ) userActivityTrackingInfo = trackingInfo #if os(iOS) || os(visionOS) if let rootViewController { @@ -322,20 +310,16 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { initialUserActivity = trackingInfo.userActivity } #endif - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "View Advertising UserActivity, set rootViewController activity to " + - "\(String(describing: trackingInfo.userActivity))" - ) - } - } - trackingInfo.handlers = value.handlers - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Set up AdvertiseUserActivity tracking info from " + - "value in UserActivityPreferenceKey: \(trackingInfo.description)" + activityEnvironmentLog( + "View Advertising UserActivity, set rootViewController activity to " + + "\(String(describing: trackingInfo.userActivity))" ) } + trackingInfo.handlers = value.handlers + activityEnvironmentLog( + "Set up AdvertiseUserActivity tracking info from " + + "value in UserActivityPreferenceKey: \(trackingInfo.description)" + ) } } @@ -363,27 +347,21 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { let preferenceValue = preferences[ActivationConditionsPreferenceKey.self] if let activationConditionsPreferenceSeed, preferenceValue.seed.matches(activationConditionsPreferenceSeed) { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "ActivationConditions Preferences hasn't changed, skipping update for Scene ActivationConditions. " + - "Seed is \(preferenceValue.seed)" - ) - } + activityEnvironmentLog( + "ActivationConditions Preferences hasn't changed, skipping update for Scene ActivationConditions. " + + "Seed is \(preferenceValue.seed)" + ) } else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "ActivationConditionPreferences changed: " + - "\(preferenceValue)" - ) - } + activityEnvironmentLog( + "ActivationConditionPreferences changed: " + + "\(preferenceValue)" + ) activationConditionsPreferenceSeed = preferenceValue.seed setActivationConditions(preferenceValue.value) - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Set Scene ActivationConditions to " + - "\(String(describing: sceneActivationConditions))" - ) - } + activityEnvironmentLog( + "Set Scene ActivationConditions to " + + "\(String(describing: sceneActivationConditions))" + ) } } @@ -411,12 +389,10 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { newConditions.prefersToActivateForTargetContentIdentifierPredicate = existingConditions.prefersToActivateForTargetContentIdentifierPredicate newConditions.canActivateForTargetContentIdentifierPredicate = existingConditions.canActivateForTargetContentIdentifierPredicate windowScene.activationConditions = newConditions - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "Changed Scene ActivationConditions to " + - "\(windowScene.activationConditions.description)" - ) - } + activityEnvironmentLog( + "Changed Scene ActivationConditions to " + + "\(windowScene.activationConditions.description)" + ) sceneActivationConditions = conditions } #elseif os(macOS) @@ -518,9 +494,7 @@ struct UserActivityModifier: ViewModifier { content .advertiseUserActivity(activityType, isActive: isActive, sceneBridge: bridge) { activity in guard isActive else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("Skipping inactive advertiseUserActivity handler") - } + activityEnvironmentLog("Skipping inactive advertiseUserActivity handler") return false } update(activity) @@ -552,16 +526,12 @@ extension View { handler: @escaping (NSUserActivity) -> Bool ) -> some View { transformIdentifiedPreference(SceneBridge.UserActivityPreferenceKey.self) { value, identity in - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "TransformIdentifiedPreference closure for UserActivity " + - "called with value \(String(describing: value))" - ) - } + activityEnvironmentLog( + "TransformIdentifiedPreference closure for UserActivity " + + "called with value \(String(describing: value))" + ) guard isActive, let sceneBridge else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log("TransformIdentifiedPreference closure for UserActivity: inactive, leaving value alone") - } + activityEnvironmentLog("TransformIdentifiedPreference closure for UserActivity: inactive, leaving value alone") return } var handlers: [ViewIdentity: (NSUserActivity) -> Bool] @@ -572,12 +542,10 @@ extension View { handlers = [identity: handler] } value = .init((activityType: activityType, handlers: handlers)) - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "TransformIdentifiedPreference for UserActivity setting value " + - "to \(String(describing: value))" - ) - } + activityEnvironmentLog( + "TransformIdentifiedPreference for UserActivity setting value " + + "to \(String(describing: value))" + ) } } @@ -676,13 +644,11 @@ extension View { ) return self.onReceive(publisher) { output in guard let activity = output as? NSUserActivity else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "onUserActivity skipping event with " + - "identifier \(activityType), published object is not " + - "a NSUserActivity: \(output)" - ) - } + activityEnvironmentLog( + "onUserActivity skipping event with " + + "identifier \(activityType), published object is not " + + "a NSUserActivity: \(output)" + ) return } action(activity) @@ -723,12 +689,10 @@ extension View { ) return self.onReceive(publisher) { output in guard let context = output as? OpenURLContext else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "onURL skipping event for OpenURLContext, " + - "published object is not a OpenURLContext: \(output)" - ) - } + activityEnvironmentLog( + "onURL skipping event for OpenURLContext, " + + "published object is not a OpenURLContext: \(output)" + ) return } action(context.url) @@ -750,12 +714,10 @@ extension View { ) return self.onReceive(publisher) { output in guard let context = output as? OpenURLContext else { - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "onURL skipping event for OpenURLContext, " + - "published object is not a OpenURLContext: \(output)" - ) - } + activityEnvironmentLog( + "onURL skipping event for OpenURLContext, " + + "published object is not a OpenURLContext: \(output)" + ) return } action(context.url, context.options) @@ -898,22 +860,18 @@ extension View { transformPreference( SceneBridge.ActivationConditionsPreferenceKey.self ) { value in - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "TransformPreference closure for activation conditions called " + - "\(String(describing: value))" - ) - } + activityEnvironmentLog( + "TransformPreference closure for activation conditions called " + + "\(String(describing: value))" + ) value = .init(( preferring: value?.preferring.union(preferring) ?? preferring, allowing: value?.allowing.union(allowing) ?? allowing, )) - if _defaultOpenSwiftUIActivityEnvironmentLoggingEnabled { - Log.log( - "TransformPreference setting value for activation conditions" + - "\(String(describing: value))" - ) - } + activityEnvironmentLog( + "TransformPreference setting value for activation conditions" + + "\(String(describing: value))" + ) } } }