From 72cfce009c3c2b1b60a103e7d2a4c39b77338bd4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 00:15:56 +0800 Subject: [PATCH 1/5] fix: publish native view responders for platform views --- .../Platform/PlatformViewRepresentable.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift index 3754cc62f..2e73f8389 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift @@ -132,6 +132,11 @@ extension PlatformViewRepresentable { buffer.traceMountedProperties(to: view, fields: fields) // TODO var outputs = PlatformViewChild.Value.makeDebuggableView(view: .init(child), inputs: inputs) + if inputs.preferences.requiresViewResponders { + outputs[ViewRespondersKey.self] = Attribute( + ViewResponderFilter(inputs: inputs, view: child) + ) + } // TODO return outputs #else @@ -146,6 +151,64 @@ extension PlatformViewRepresentable { #if canImport(Darwin) +// MARK: - ViewResponderFilter + +private struct ViewResponderFilter: StatefulRule where Content: PlatformViewRepresentable { + @Attribute var view: ViewLeafView + @Attribute var position: CGPoint + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + @Attribute var layoutDirection: LayoutDirection + let responder: PlatformViewResponder + #if os(macOS) + @Attribute var evaluateDefaultFocus: EvaluateDefaultFocusAction? + @Attribute var delegatesFocusEffect: Bool + @Attribute var isEnabled: Bool + #endif + @Attribute var keyPressHandlers: [KeyPress.Handler] + + init(inputs: _ViewInputs, view: Attribute>) { + _view = view + _position = inputs.animatedPosition() + _size = inputs.animatedSize() + _transform = inputs.transform + _layoutDirection = inputs.base.layoutDirection + responder = PlatformViewResponder() + #if os(macOS) + _evaluateDefaultFocus = inputs.base.mapEnvironment(id: .evaluateDefaultFocus) { $0.evaluateDefaultFocus } + _delegatesFocusEffect = inputs.base.delegatesFocusEffect + _isEnabled = inputs.base.isEnabled + #endif + _keyPressHandlers = inputs.base.keyPressHandlers + } + + typealias Value = [ViewResponder] + + mutating func updateValue() { + responder.hostView = view.platformView + responder.representedView = Content.platformView(for: view.representedViewProvider) + responder.helper.update( + data: (TrivialContentResponder(), false), + size: $size.changedValue(), + position: $position.changedValue(), + transform: $transform.changedValue(), + parent: responder + ) + responder.keyPressHandlers = keyPressHandlers + // TODO: Resolve RepresentablePreferredFocusableViewInput. + view.platformView.responder = responder + #if os(macOS) + responder.effectiveLayoutDirection = layoutDirection + responder.delegatesFocusEffect = delegatesFocusEffect + responder.isEnabled = isEnabled + responder.evaluateDefaultFocus = evaluateDefaultFocus + #endif + if !hasValue { + value = [responder] + } + } +} + extension PlatformViewRepresentable where PlatformViewProvider: PlatformView { static func platformView(for provider: PlatformViewProvider) -> PlatformView { provider From 51b6c9a7edb548766df01ec5fdd6d665529a54c3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 00:45:05 +0800 Subject: [PATCH 2/5] fix: update platform view host environment and layout --- .../Shims/AppKit/AppKit_Private.h | 2 + .../Shims/AppKit/NSView_Private.h | 5 + .../COpenSwiftUI/Shims/UIKit/UIKit_Private.h | 2 + .../COpenSwiftUI/Shims/UIKit/UIKit_Private.m | 10 + .../Data/Environment/UIKitEnvironment.swift | 23 ++ .../Platform/PlatformViewHost.swift | 243 ++++++++++++++++-- ...atformViewRepresentableLayoutOptions.swift | 1 + 7 files changed, 265 insertions(+), 21 deletions(-) diff --git a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h index 6fc18c2f0..c5199b44e 100644 --- a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h +++ b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h @@ -103,6 +103,8 @@ typedef OPENSWIFTUI_ENUM(NSInteger, NSViewVibrantBlendingStyle) { - (void)showActiveFirstResponderIndication; - (void)hideActiveFirstResponderIndication; - (void)_primitiveSetNextKeyView:(nullable NSView *)view OPENSWIFTUI_SWIFT_NAME(_primitiveSetNextKeyView(_:)); +- (void)_primitiveSetPreviousKeyView:(nullable NSView *)view OPENSWIFTUI_SWIFT_NAME(_primitiveSetPreviousKeyView(_:)); +- (void)_setDefaultKeyViewLoop; - (nullable NSView *)_recursiveSetDefaultKeyViewLoop; - (void)_recursiveGatherAllKeyViewCandidatesInArray:(NSMutableArray *)array OPENSWIFTUI_SWIFT_NAME(_recursiveGatherAllKeyViewCandidates(in:)); @end diff --git a/Sources/COpenSwiftUI/Shims/AppKit/NSView_Private.h b/Sources/COpenSwiftUI/Shims/AppKit/NSView_Private.h index 7bdc95ba9..46e254640 100644 --- a/Sources/COpenSwiftUI/Shims/AppKit/NSView_Private.h +++ b/Sources/COpenSwiftUI/Shims/AppKit/NSView_Private.h @@ -23,6 +23,11 @@ @property (nonatomic) BOOL ignoreHitTest_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(ignoreHitTest); @property (nullable, copy) id contentStyle; @property NSUserInterfaceLayoutDirection userInterfaceLayoutDirection; +@property (readonly) BOOL _wantsConstraintBasedLayout; + +- (void)_setWantsToHostAutolayoutEngine:(BOOL)value; +- (CGSize)_layoutSizeThatFits:(CGSize)size fixedAxes:(unsigned long long)axes; +- (NSLayoutPriority)_priorityForEngineHostConstraints; - (BOOL)_userInterfaceLayoutDirectionPropagatesToDescendants; - (void)_setUserInterfaceLayoutDirectionPropagatesToDescendants:(BOOL)propagates; diff --git a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h index 621cc2bd1..92d5aad99 100644 --- a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h +++ b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h @@ -55,6 +55,7 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN @end @interface UIView (OpenSwiftUI_SPI) +- (void)_noteTraitsDidChangeRecursively_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_noteTraitsDidChangeRecursively()); @property(nonatomic, readonly, nullable) UIResponder *firstResponder_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(firstResponder); - (nullable id)_largeContentViewerItemAtPoint:(CGPoint)point OPENSWIFTUI_SWIFT_NAME(_largeContentViewerItem(at:)); - (instancetype)_initWithLayer:(CALayer *)layer; @@ -93,6 +94,7 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN #endif @interface UIViewController (OpenSwiftUI_SPI) +- (void)_noWarn_setOverrideTraitCollection_openswiftui_safe_wrapper:(UITraitCollection *)traits forChild:(UIViewController *)child OPENSWIFTUI_SWIFT_NAME(_noWarn_setOverrideTraitCollection(_:forChild:)); + (nullable UIViewController *)viewControllerForView_openswiftui_safe_wrapper:(UIView *)view OPENSWIFTUI_SWIFT_NAME(viewController(for:)); @property (nonatomic, readonly) BOOL _shouldIgnoreChildFocusRegions_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_shouldIgnoreChildFocusRegions); @property (nonatomic, readonly) BOOL _canShowWhileLocked_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_canShowWhileLocked); diff --git a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m index 97194a264..170886a7f 100644 --- a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m +++ b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m @@ -56,6 +56,11 @@ - (void)_saveRestorationUserActivityStateForScene_openswiftui_safe_wrapper:(UISc @end @implementation UIView (OpenSwiftUI_SPI) +- (void)_noteTraitsDidChangeRecursively_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(void, @"_noteTraitsDidChangeRecursively", ); + func(self, selector); +} + - (UIResponder *)firstResponder_openswiftui_safe_wrapper { OPENSWIFTUI_SAFE_WRAPPER_IMP(UIResponder *, @"firstResponder", nil); return func(self, selector); @@ -93,6 +98,11 @@ + (NSInteger)_currentAnimationCurve_openswiftui_safe_wrapper { @end @implementation UIViewController (OpenSwiftUI_SPI) +- (void)_noWarn_setOverrideTraitCollection_openswiftui_safe_wrapper:(UITraitCollection *)traits forChild:(UIViewController *)child { + OPENSWIFTUI_SAFE_WRAPPER_IMP(void, @"_noWarn_setOverrideTraitCollection:forChild:", , UITraitCollection *, UIViewController *); + func(self, selector, traits, child); +} + + (UIViewController *)viewControllerForView_openswiftui_safe_wrapper:(UIView *)view { OPENSWIFTUI_SAFE_WRAPPER_IMP(UIViewController *, @"viewControllerForView:", nil, UIView *); return func(self, selector, view); diff --git a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift index fa00d67d5..88b0e2a13 100644 --- a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift +++ b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift @@ -243,4 +243,27 @@ extension EnvironmentValues { } } +extension UIView { + func adoptEnvironment(_ environment: EnvironmentValues, hostedSubview: UIView) { + let isEnabled = environment.isEnabled + Update.enqueueAction(reason: nil) { + if self.isUserInteractionEnabled != isEnabled { + self.isUserInteractionEnabled = isEnabled + } + if let control = hostedSubview as? UIControl, control.isEnabled != isEnabled { + control.isEnabled = isEnabled + } + } + let semanticContentAttribute: UISemanticContentAttribute = environment.layoutDirection == .rightToLeft + ? .forceRightToLeft : .forceLeftToRight + if hostedSubview.semanticContentAttribute != semanticContentAttribute { + hostedSubview.semanticContentAttribute = semanticContentAttribute + } + if let tint = Semantics.UIKitEnvironmentPrefersTint.isEnabled + ? environment.tintColor ?? environment.accentColor + : environment.accentColor { + tintColor = unsafeDowncast(tint.resolve(in: environment).kitColor, to: UIColor.self) + } + } +} #endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift index 70e87f160..0fd324608 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift @@ -44,7 +44,11 @@ where Content: PlatformViewRepresentable { var viewHierarchyMode: ViewControllerParentingMode? - var focusedValues: FocusedValues + var focusedValues: FocusedValues { + didSet { + updateNestedHosts(.focusedValues, colorSchemeChanged: false) + } + } weak var responder: PlatformViewResponder? @@ -60,6 +64,8 @@ where Content: PlatformViewRepresentable { #endif #if os(macOS) + @NSManaged var _safeAreaInsets: PlatformEdgeInsets + var recursiveIgnoreHitTest: Bool = false var customAcceptsFirstMouse: Bool? @@ -75,7 +81,6 @@ where Content: PlatformViewRepresentable { var cachedLayoutTraits: _LayoutTraits? - // FIXME: macOS init( _ provider: Content.PlatformViewProvider, host: ViewRendererHost?, @@ -90,14 +95,21 @@ where Content: PlatformViewRepresentable { self.focusedValues = .init() self.importer = importer super.init(hostedView: nil) + #if os(iOS) || os(visionOS) if Content.isViewController { viewHierarchyMode = isLinkedOnOrAfter(.v6) ? .willMoveToSuperview : .didMoveToWindow } - #if os(iOS) || os(visionOS) if isLinkedOnOrAfter(.v6) { layer.allowsGroupOpacity = false layer.allowsGroupBlending = false } + #elseif os(macOS) + if isLinkedOnOrAfter(.v6) { + layer?.allowsGroupOpacity = false + layer?.allowsGroupBlending = false + } + clipsToBounds = false + _setWants(toHostAutolayoutEngine: true) #endif if !Content.isViewController { hostedView = Content.platformView(for: representedViewProvider) @@ -206,6 +218,12 @@ where Content: PlatformViewRepresentable { func layoutTraits() -> _LayoutTraits { guard let cachedLayoutTraits else { + #if os(macOS) + let invalidator = layoutInvalidator + layoutInvalidator = nil + updateConstraintsForSubtreeIfNeeded() + layoutInvalidator = invalidator + #endif let traits = intrinsicLayoutTraits() cachedLayoutTraits = traits return traits @@ -214,16 +232,59 @@ where Content: PlatformViewRepresentable { } func updateEnvironment(_ environment: EnvironmentValues, viewPhase: _GraphInputs.Phase) { - _openSwiftUIUnimplementedWarning() + let colorSchemeChanged = self.environment.colorScheme != environment.colorScheme + self.environment = environment + self.viewPhase = viewPhase + updateNestedHosts(.environment, colorSchemeChanged: colorSchemeChanged) + adoptEnvironment(environment, hostedSubview: Content.platformView(for: representedViewProvider)) } func updateSafeAreaInsets(_ insets: PlatformEdgeInsets?) { safeAreaHelper.updateSafeAreaInsets(insets, delegate: self) } + private func updateNestedHosts(_ properties: ViewRendererHostProperties, colorSchemeChanged: Bool) { + #if os(iOS) || os(visionOS) + guard let viewController = representedViewProvider as? UIViewController else { + _noteTraitsDidChangeRecursively() + return + } + guard let parentController = host?.as(UIViewControllerProvider.self)?.uiViewController else { + return + } + let traits = traitCollection.byOverriding( + with: environment, + viewPhase: viewPhase, + focusedValues: focusedValues + ) + Update.enqueueAction(reason: nil) { [weak parentController] in + parentController?._noWarn_setOverrideTraitCollection(traits, forChild: viewController) + viewController.updateTraitsIfNeeded() + if colorSchemeChanged { + parentController?.setNeedsStatusBarAppearanceUpdate() + } + } + #elseif os(macOS) + var views = subviews + while !views.isEmpty { + let view = views.removeFirst() + if let host = view as? ViewRendererHost { + host.invalidateProperties(properties) + } else { + views.append(contentsOf: view.subviews) + } + } + #endif + } + override var hostedView: PlatformView? { get { super.hostedView } - set { super.hostedView = newValue } + set { + super.hostedView = newValue + #if os(macOS) + cachedImplementsFittingSize = nil + #endif + } } #if os(iOS) || os(visionOS) @@ -280,14 +341,14 @@ where Content: PlatformViewRepresentable { #else override func viewWillMove(toSuperview newSuperview: NSView?) { defer { super.viewWillMove(toSuperview: newSuperview) } - guard let newSuperview else { + guard newSuperview != nil else { return } guard let viewController = representedViewProvider as? PlatformViewController else { return } let view = viewController.view - guard view.superview != newSuperview else { + guard view.superview != self else { return } hostedView = view @@ -296,8 +357,18 @@ where Content: PlatformViewRepresentable { override func viewDidMoveToSuperview() { defer { super.viewDidMoveToSuperview() } - // TODO - // updateConstraintsForSubtreeIfNeeded() + guard superview != nil else { return } + _setDefaultKeyViewLoop() + _primitiveSetPreviousKeyView(host as? NSView) + _primitiveSetNextKeyView(host as? NSView) + updateConstraintsForSubtreeIfNeeded() + } + + override func removeFromSuperview() { + if let viewController = representedViewProvider as? NSViewController { + viewController.view.removeFromSuperview() + } + super.removeFromSuperview() } #endif @@ -359,6 +430,125 @@ where Content: PlatformViewRepresentable { override func layout() { updateHostedViewBounds() } + + private func layoutHostedView() { + guard let hostedView, + !hostedView._wantsConstraintBasedLayout, + bounds.width != .zero, bounds.height != .zero else { + return + } + hostedView.frame = hostedView.frame(forAlignmentRect: bounds) + } + + override func resizeSubviews(withOldSize oldSize: CGSize) { + super.resizeSubviews(withOldSize: oldSize) + layoutHostedView() + } + + override var fittingSize: CGSize { + if Content.isViewController { + return super.fittingSize + } else { + return Content.platformView(for: representedViewProvider).fittingSize + } + } + + private var implementsFittingSize: Bool { + if let cachedImplementsFittingSize { + return cachedImplementsFittingSize + } + guard let hostedView else { return false } + let implementation = hostedView.method(for: fittingSizeSelector).map { + unsafeBitCast($0, to: OpaquePointer.self) + } + let result = implementation != defaultFittingSizeIMP + cachedImplementsFittingSize = result + return result + } + + override func _layoutSizeThatFits(_ size: CGSize, fixedAxes: UInt64) -> CGSize { + let wasInLayoutSizeThatFits = inLayoutSizeThatFits + inLayoutSizeThatFits = true + defer { inLayoutSizeThatFits = wasInLayoutSizeThatFits } + guard let hostedView else { return size } + let wantsConstraints = hostedView._wantsConstraintBasedLayout + let boundsSize = hostedView.bounds.size + var result: CGSize + if let control = hostedView as? NSControl { + result = control._layoutSizeThatFits(size, fixedAxes: fixedAxes) + } else { + result = super._layoutSizeThatFits(size, fixedAxes: fixedAxes) + } + if (result.width == boundsSize.width || result.height == boundsSize.height), + !wantsConstraints, !implementsFittingSize { + let intrinsicSize = hostedView.intrinsicContentSize + if result.width == boundsSize.width { + result.width = intrinsicSize.width == NSView.noIntrinsicMetric ? size.width : intrinsicSize.width + } + if result.height == boundsSize.height { + result.height = intrinsicSize.height == NSView.noIntrinsicMetric ? size.height : intrinsicSize.height + } + } + if !wantsConstraints, !implementsFittingSize { + if result.width < size.width, + hostedView.contentHuggingPriority(for: .horizontal) < .defaultHigh { + result.width = size.width + } else if result.width > size.width, + hostedView.contentCompressionResistancePriority(for: .horizontal) < .defaultHigh { + result.width = size.width + } + if result.height < size.height, + hostedView.contentHuggingPriority(for: .vertical) < .defaultHigh { + result.height = size.height + } else if result.height > size.height, + hostedView.contentCompressionResistancePriority(for: .vertical) < .defaultHigh { + result.height = size.height + } + } + return result + } + + func invalidateLayout() { + cachedLayoutTraits = nil + if let layoutInvalidator, let graphHost = layoutInvalidator.graphHost { + graphHost.asyncTransaction(invalidating: layoutInvalidator.layoutComputer) + } + invalidationPending = false + } + + func enqueueLayoutInvalidation() { + guard layoutInvalidator != nil else { + cachedLayoutTraits = nil + return + } + guard !invalidationPending, superview != nil else { return } + invalidationPending = true + Update.enqueueAction(reason: nil) { [weak self] in + self?.invalidateLayout() + } + } + + override func _layoutMetricsInvalidatedForHostedView() { + super._layoutMetricsInvalidatedForHostedView() + if Content.layoutOptions(representedViewProvider).contains(._0), + !inLayoutSizeThatFits, hostedView != nil { + enqueueLayoutInvalidation() + } + } + + override var alignmentRectInsets: NSEdgeInsets { .zero } + + override func updateConstraints() { + _ = enableUnifiedLayout() + if let hostedView, hostedView._wantsConstraintBasedLayout { + hostedView.translatesAutoresizingMaskIntoConstraints = false + } + super.updateConstraints() + } + + override func _priorityForEngineHostConstraints() -> NSLayoutConstraint.Priority { + .init(500) + } #endif #if os(iOS) || os(visionOS) @@ -404,6 +594,19 @@ where Content: PlatformViewRepresentable { override var computedSafeAreaInsets: PlatformEdgeInsets { safeAreaHelper.resolvedSafeAreaInsets(delegate: self) } + + @objc static func automaticallyTracksDependenciesOn_safeAreaInsets() -> Bool { + true + } + + @objc static func default_safeAreaInsets() -> NSEdgeInsets { + .init( + top: .greatestFiniteMagnitude, + left: .greatestFiniteMagnitude, + bottom: .greatestFiniteMagnitude, + right: .greatestFiniteMagnitude + ) + } #endif override var intrinsicContentSize: CGSize { @@ -426,18 +629,6 @@ extension PlatformViewHost: PlatformFocusViewProvider { } extension PlatformViewHost: SafeAreaHelperDelegate { - #if os(macOS) - var _safeAreaInsets: PlatformEdgeInsets { - get { - _openSwiftUIUnimplementedWarning() - return .zero - } - set { - _openSwiftUIUnimplementedWarning() - } - } - #endif - var defaultSafeAreaInsets: PlatformEdgeInsets { #if os(iOS) || os(visionOS) super.safeAreaInsets @@ -506,6 +697,16 @@ extension PlatformViewHost: AcceptsFirstMouseCustomizing {} extension PlatformViewHost: RecursiveIgnoreHitTestCustomizing {} +#if os(macOS) +extension PlatformViewHost: PlatformLayoutContainer {} + +private let fittingSizeSelector = #selector(getter: NSView.fittingSize) + +private let defaultFittingSizeIMP = NSView.instanceMethod(for: fittingSizeSelector).map { + unsafeBitCast($0, to: OpaquePointer.self) +} +#endif + #endif func enableUnifiedLayout() -> Bool { diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableLayoutOptions.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableLayoutOptions.swift index 0cf2e7eee..dd228380f 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableLayoutOptions.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableLayoutOptions.swift @@ -11,6 +11,7 @@ public struct _PlatformViewRepresentableLayoutOptions: OptionSet { public let rawValue: Int + static var _0: _PlatformViewRepresentableLayoutOptions { .init(rawValue: 1 << 0) } static var _1: _PlatformViewRepresentableLayoutOptions { .init(rawValue: 1 << 1) } @_spi(Private) From 33a6a31b6426c7a7997d7774b9746783c9774068 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 00:45:11 +0800 Subject: [PATCH 3/5] test: cover toggle interaction and disabled state --- .../View/ToggleInteractionTests.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Example/Tests/OpenSwiftUIInteractionTests/View/ToggleInteractionTests.swift diff --git a/Example/Tests/OpenSwiftUIInteractionTests/View/ToggleInteractionTests.swift b/Example/Tests/OpenSwiftUIInteractionTests/View/ToggleInteractionTests.swift new file mode 100644 index 000000000..f8124512f --- /dev/null +++ b/Example/Tests/OpenSwiftUIInteractionTests/View/ToggleInteractionTests.swift @@ -0,0 +1,66 @@ +// +// ToggleInteractionTests.swift +// OpenSwiftUIInteractionTests + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif +import Testing + +@MainActor +@Suite(.serialized, .tags(.interaction)) +struct ToggleInteractionTests { + @Test + func repeatedTapsUpdateBinding() async throws { + var values: [Bool] = [] + let content = StatefulToggle { values.append($0) } + + try await withInteractionTestHost(of: content) { host in + #expect(values.isEmpty) + for count in 1...3 { + try await host.tap() + try await host.waitUntil(values.count >= count) + // Let the native switch finish its animation before the next tap. + try await Task.sleep(for: .milliseconds(600)) + } + #expect(values == [true, false, true]) + } + } + + @Test + func disabledToggleDoesNotUpdateBinding() async throws { + var values: [Bool] = [] + let content = StatefulToggle { values.append($0) } + .disabled(true) + + try await withInteractionTestHost(of: content) { host in + for _ in 0..<2 { + try await host.tap() + try await Task.sleep(for: .milliseconds(600)) + } + #expect(values.isEmpty) + } + } + + private struct StatefulToggle: View { + var onChange: (Bool) -> Void + @State private var reset = false + + var body: some View { + Toggle("Reset", isOn: Binding( + get: { reset }, + set: { + reset = $0 + onChange($0) + } + )) + #if os(macOS) + .toggleStyle(.switch) + #endif + .labelsHidden() + .fixedSize() + } + } +} From 31508d8449640d98c3e81a41ac90440bdeae42f8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 00:57:49 +0800 Subject: [PATCH 4/5] fix: remove redundant fitting size IMP casts --- .../Representable/Platform/PlatformViewHost.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift index 0fd324608..f6610c24e 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift @@ -458,9 +458,7 @@ where Content: PlatformViewRepresentable { return cachedImplementsFittingSize } guard let hostedView else { return false } - let implementation = hostedView.method(for: fittingSizeSelector).map { - unsafeBitCast($0, to: OpaquePointer.self) - } + let implementation = hostedView.method(for: fittingSizeSelector) let result = implementation != defaultFittingSizeIMP cachedImplementsFittingSize = result return result @@ -702,9 +700,7 @@ extension PlatformViewHost: PlatformLayoutContainer {} private let fittingSizeSelector = #selector(getter: NSView.fittingSize) -private let defaultFittingSizeIMP = NSView.instanceMethod(for: fittingSizeSelector).map { - unsafeBitCast($0, to: OpaquePointer.self) -} +private let defaultFittingSizeIMP = NSView.instanceMethod(for: fittingSizeSelector) #endif #endif From 459f58d0aa63439c0429087cc15de6bb70e3d9c5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 01:49:20 +0800 Subject: [PATCH 5/5] build: update DarwinPrivateFrameworks for visionOS UIKit overlay --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index ecdd45903..6f5b1b73b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "f9bbd5918debf9bf10f6dae0b116587a742d2bcb" + "revision" : "ebf635d498be7e26ddb308ea5d17ca28a6754dd6" } }, {