From cec3221b7c8448b726a8a80b219d38cb5b5f7764 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Sep 2026 01:29:07 +0800 Subject: [PATCH 1/4] feat(hosting): track foreign UIKit subviews --- Sources/COpenSwiftUI/Util/AppleInternal.c | 29 ++++++++++++++ Sources/COpenSwiftUI/Util/AppleInternal.h | 20 ++++++++++ .../Hosting/UIKit/View/UIHostingView.swift | 39 +++++++++++++++++-- 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 Sources/COpenSwiftUI/Util/AppleInternal.c create mode 100644 Sources/COpenSwiftUI/Util/AppleInternal.h diff --git a/Sources/COpenSwiftUI/Util/AppleInternal.c b/Sources/COpenSwiftUI/Util/AppleInternal.c new file mode 100644 index 000000000..28944b10f --- /dev/null +++ b/Sources/COpenSwiftUI/Util/AppleInternal.c @@ -0,0 +1,29 @@ +// +// AppleInternal.c +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#include "AppleInternal.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN +#include + +extern bool os_variant_allows_internal_security_policies(const char *subsystem); +extern bool os_variant_has_internal_content(const char *subsystem); +#endif + +bool _OpenSwiftUIIsAppleInternalBuild(void) { + #if OPENSWIFTUI_TARGET_OS_DARWIN + static bool isInternal; + static dispatch_once_t once; + dispatch_once(&once, ^{ + isInternal = os_variant_allows_internal_security_policies("org.OpenSwiftUIProject.OpenSwiftUI") + && os_variant_has_internal_content("org.OpenSwiftUIProject.OpenSwiftUI"); + }); + return isInternal; + #else + return false; + #endif +} diff --git a/Sources/COpenSwiftUI/Util/AppleInternal.h b/Sources/COpenSwiftUI/Util/AppleInternal.h new file mode 100644 index 000000000..606150278 --- /dev/null +++ b/Sources/COpenSwiftUI/Util/AppleInternal.h @@ -0,0 +1,20 @@ +// +// AppleInternal.h +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AppleInternal_h +#define AppleInternal_h + +#include "OpenSwiftUIBase.h" + +OPENSWIFTUI_EXTERN_C_BEGIN + +OPENSWIFTUI_EXPORT +bool _OpenSwiftUIIsAppleInternalBuild(void); + +OPENSWIFTUI_EXTERN_C_END + +#endif /* AppleInternal_h */ diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift index ce74e87ae..64293daea 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift @@ -413,9 +413,38 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con } } - private lazy var foreignSubviews: NSHashTable? = NSHashTable.weakObjects() + private lazy var foreignSubviews: NSHashTable = NSHashTable.weakObjects() private var isInsertingRenderedSubview: Bool = false + + // Audited for 6.5.4 + override dynamic open func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + guard !isInsertingRenderedSubview else { return } + foreignSubviews.add(subview) + if isLinkedOnOrAfter(.v7) { + let hostName = if _OpenSwiftUIIsAppleInternalBuild(), viewController == nil { + "_UIHostingView" + } else { + "UIHostingController.view" + } + Log.runtimeIssues( + "Adding '%s' as a subview of %s is not supported and may result in a broken view hierarchy. Add your view above %s in a common superview or insert it into your OpenSwiftUI content in a UIViewRepresentable instead.", + [ + "\(type(of: subview))", + hostName, + hostName, + ] + ) + } + } + + // Audited for 6.5.4 + override dynamic open func willRemoveSubview(_ subview: UIView) { + super.willRemoveSubview(subview) + foreignSubviews.remove(subview) + } + /// The UIKit notion of the safe area insets. open override var safeAreaInsets: UIEdgeInsets { @@ -486,8 +515,12 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con } } - @objc(swiftui_insertRenderedSubview:atIndex:) // FIXME: ViewUpdater -> CoreViewAddSubview - private func openswiftui_insertRenderedSubview(_ view: UIView, at index: Int) { + @_spi(ForOpenSwiftUIOnly) + #if OPENSWIFTUI_SWIFTUI_RENDERER + @objc(swiftui_insertRenderedSubview:atIndex:) + #endif + override public func openswiftui_insertRenderedSubview(_ subview: Any, at index: Int) { + let view = subview as! UIView isInsertingRenderedSubview = true insertSubview(view, at: index) isInsertingRenderedSubview = false From b6c9750b8f1ec9a52e7cecbfc0d1c1f27c00efd8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Sep 2026 01:42:14 +0800 Subject: [PATCH 2/4] feat: update NSHostingView for didAddSubview --- .../Hosting/AppKit/View/NSHostingView.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index 312cec16a..8f9df1243 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -539,6 +539,34 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont private var isInsertingRenderedSubview: Bool = false + // Audited for 6.5.4 + override dynamic open func didAddSubview(_ subview: NSView) { + super.didAddSubview(subview) + guard !isInsertingRenderedSubview else { return } + foreignSubviews.add(subview) + if isLinkedOnOrAfter(.v7) { + let hostName = if viewController == nil { + "NSHostingView" + } else { + "NSHostingController.view" + } + Log.runtimeIssues( + "Adding '%s' as a subview of %s is not supported and may result in a broken view hierarchy. Add your view above %s in a common superview or insert it into your OpenSwiftUI content in a NSViewRepresentable instead.", + [ + "\(type(of: subview))", + hostName, + hostName, + ] + ) + } + } + + // Audited for 6.5.4 + override dynamic open func willRemoveSubview(_ subview: NSView) { + super.willRemoveSubview(subview) + foreignSubviews.remove(subview) + } + private var sizeConstraints: SizeConstraints? private struct SizeConstraints { From ebd1ab9a2a0c3919978c7c9f737f64feec968ec4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Sep 2026 01:43:01 +0800 Subject: [PATCH 3/4] feat: update containsRuntimeIssue API --- Sources/OpenSwiftUICore/Log/Logging.swift | 4 ---- .../Testing/RuntimeIssueHandlingTrait.swift | 16 ++++++++++------ .../DynamicPropertyCacheTests.swift | 1 + .../Data/State/StoredLocationTests.swift | 1 + .../Util/MainActorUtilsTests.swift | 1 + 5 files changed, 13 insertions(+), 10 deletions(-) rename {Tests/OpenSwiftUICoreTests => Sources/OpenSwiftUITestsSupport}/Testing/RuntimeIssueHandlingTrait.swift (74%) diff --git a/Sources/OpenSwiftUICore/Log/Logging.swift b/Sources/OpenSwiftUICore/Log/Logging.swift index f50e5e7c3..d2c41ee9d 100644 --- a/Sources/OpenSwiftUICore/Log/Logging.swift +++ b/Sources/OpenSwiftUICore/Log/Logging.swift @@ -193,11 +193,7 @@ package enum Log { #if OPENSWIFTUI_LINK_TESTING if Test.current != nil { let comment: Comment = #"[Runtime Issue] message: \#(message.description) args: \#(args)"# - #if swift(>=6.3) Issue.record(comment, severity: .warning) - #else - Issue.record(comment) - #endif } #endif diff --git a/Tests/OpenSwiftUICoreTests/Testing/RuntimeIssueHandlingTrait.swift b/Sources/OpenSwiftUITestsSupport/Testing/RuntimeIssueHandlingTrait.swift similarity index 74% rename from Tests/OpenSwiftUICoreTests/Testing/RuntimeIssueHandlingTrait.swift rename to Sources/OpenSwiftUITestsSupport/Testing/RuntimeIssueHandlingTrait.swift index b72aa7f9b..cc319730e 100644 --- a/Tests/OpenSwiftUICoreTests/Testing/RuntimeIssueHandlingTrait.swift +++ b/Sources/OpenSwiftUITestsSupport/Testing/RuntimeIssueHandlingTrait.swift @@ -1,20 +1,20 @@ // // RuntimeIssueHandlingTrait.swift -// OpenSwiftUICoreTests +// OpenSwiftUITestsSupport import Foundation -import Testing +package import Testing -func containsRuntimeIssue(_ message: String) -> ContainsRuntimeIssueTrait { +package func containsRuntimeIssue(_ message: String) -> ContainsRuntimeIssueTrait { ContainsRuntimeIssueTrait(message: message) } -struct ContainsRuntimeIssueTrait: TestTrait, TestScoping { - typealias TestScopeProvider = ContainsRuntimeIssueTrait +package struct ContainsRuntimeIssueTrait: TestTrait, TestScoping { + package typealias TestScopeProvider = ContainsRuntimeIssueTrait var message: String - func provideScope( + package func provideScope( for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void @@ -29,12 +29,16 @@ struct ContainsRuntimeIssueTrait: TestTrait, TestScoping { } } try await issueHandler.provideScope(for: test, testCase: testCase, performing: function) + #if !OPENSWIFTUI_LINK_TESTING + Issue.record("ContainsRuntimeIssueTrait requires OPENSWIFTUI_LINK_TESTING to be set", severity: .warning) + #else if !state.hasMatch { Issue.record( #"Expected runtime issue was not recorded: "\#(message)""#, sourceLocation: test.sourceLocation ) } + #endif } } diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift index ef0a8696e..8f2f4c22d 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift @@ -4,6 +4,7 @@ import OpenAttributeGraphShims @testable import OpenSwiftUICore +import OpenSwiftUITestsSupport import Testing // FIXME: Remove after we implement forEachField diff --git a/Tests/OpenSwiftUICoreTests/Data/State/StoredLocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/State/StoredLocationTests.swift index f91c49345..07059702d 100644 --- a/Tests/OpenSwiftUICoreTests/Data/State/StoredLocationTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/State/StoredLocationTests.swift @@ -5,6 +5,7 @@ import Foundation import OpenAttributeGraphShims @_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore +import OpenSwiftUITestsSupport import Testing @MainActor diff --git a/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift b/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift index ce1d9840a..3f335f588 100644 --- a/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/MainActorUtilsTests.swift @@ -3,6 +3,7 @@ // OpenSwiftUICoreTests import OpenSwiftUICore +import OpenSwiftUITestsSupport import Testing struct MainActorUtilsTests { From c6a4b0749e603040d9413a138e17aaa2bd6506cc Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Sep 2026 02:05:58 +0800 Subject: [PATCH 4/4] test(hosting): verify foreign subview runtime issues --- .../AppKit/View/NSHostingViewTests.swift | 26 +++++++++++++++++++ .../UIKit/View/UIHostingViewTests.swift | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 Tests/OpenSwiftUITests/Integration/Hosting/AppKit/View/NSHostingViewTests.swift create mode 100644 Tests/OpenSwiftUITests/Integration/Hosting/UIKit/View/UIHostingViewTests.swift diff --git a/Tests/OpenSwiftUITests/Integration/Hosting/AppKit/View/NSHostingViewTests.swift b/Tests/OpenSwiftUITests/Integration/Hosting/AppKit/View/NSHostingViewTests.swift new file mode 100644 index 000000000..ef810a796 --- /dev/null +++ b/Tests/OpenSwiftUITests/Integration/Hosting/AppKit/View/NSHostingViewTests.swift @@ -0,0 +1,26 @@ +// +// NSHostingViewTests.swift +// OpenSwiftUITests + +#if os(macOS) +import AppKit +import OpenSwiftUI +import OpenSwiftUICore +import OpenSwiftUITestsSupport +import Testing + +@MainActor +struct NSHostingViewTests { + @Test(containsRuntimeIssue("Adding '%s' as a subview of %s is not supported and may result in a broken view hierarchy. Add your view above %s in a common superview or insert it into your OpenSwiftUI content in a NSViewRepresentable instead.")) + func didAddSubviewRecordsRuntimeIssue() { + let hostingView = NSHostingView(rootView: EmptyView()) + let subview = NSView() + + Semantics.v7.test { + hostingView.addSubview(subview) + } + + #expect(subview.superview === hostingView) + } +} +#endif diff --git a/Tests/OpenSwiftUITests/Integration/Hosting/UIKit/View/UIHostingViewTests.swift b/Tests/OpenSwiftUITests/Integration/Hosting/UIKit/View/UIHostingViewTests.swift new file mode 100644 index 000000000..54f78399e --- /dev/null +++ b/Tests/OpenSwiftUITests/Integration/Hosting/UIKit/View/UIHostingViewTests.swift @@ -0,0 +1,26 @@ +// +// UIHostingViewTests.swift +// OpenSwiftUITests + +#if os(iOS) || os(visionOS) +import OpenSwiftUI +import OpenSwiftUICore +import OpenSwiftUITestsSupport +import Testing +import UIKit + +@MainActor +struct UIHostingViewTests { + @Test(containsRuntimeIssue("Adding '%s' as a subview of %s is not supported and may result in a broken view hierarchy. Add your view above %s in a common superview or insert it into your OpenSwiftUI content in a UIViewRepresentable instead.")) + func didAddSubviewRecordsRuntimeIssue() { + let hostingView = _UIHostingView(rootView: EmptyView()) + let subview = UIView() + + Semantics.v7.test { + hostingView.addSubview(subview) + } + + #expect(subview.superview === hostingView) + } +} +#endif