diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 6677ba91..97d90f6a 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -2390,7 +2390,28 @@ final class WindowBrowserPortal: NSObject { reason: reason ) } + func preserveVisibleDuringTransientDetach(reason: String) -> Bool { + guard entry.visibleInUI, !containerView.isHidden else { return false } + let didScheduleTransientRecovery = scheduleTransientRecoveryRetryIfNeeded( + forWebViewId: webViewId, + entry: &entry, + webView: webView, + reason: reason + ) + guard didScheduleTransientRecovery else { return false } +#if DEBUG + dlog( + "browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " + + "reason=\(reason) frame=\(browserPortalDebugFrame(containerView.frame))" + ) +#endif + containerView.setDropZoneOverlay(zone: nil) + return true + } guard let anchorView = entry.anchorView, let window else { + if preserveVisibleDuringTransientDetach(reason: "missingAnchorOrWindow") { + return + } if scheduleTransientDetachRecovery(reason: "missingAnchorOrWindow") { hideContainerView(reason: "missingAnchorOrWindow") return @@ -2415,21 +2436,15 @@ final class WindowBrowserPortal: NSObject { anchorView.window == nil && anchorView.superview != nil if isOffWindowReparent { - let didScheduleTransientRecovery = scheduleTransientRecoveryRetryIfNeeded( - forWebViewId: webViewId, - entry: &entry, - webView: webView, - reason: "anchorWindowMismatch" - ) -#if DEBUG - if didScheduleTransientRecovery && !containerView.isHidden { - dlog( - "browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " + - "reason=anchorWindowMismatch.offWindow frame=\(browserPortalDebugFrame(containerView.frame))" - ) + if preserveVisibleDuringTransientDetach(reason: "anchorWindowMismatch.offWindow") { + return } -#endif - containerView.setDropZoneOverlay(zone: nil) + if scheduleTransientDetachRecovery(reason: "anchorWindowMismatch") { + hideContainerView(reason: "anchorWindowMismatch") + return + } + } + if preserveVisibleDuringTransientDetach(reason: "anchorWindowMismatch") { return } if scheduleTransientDetachRecovery(reason: "anchorWindowMismatch") { diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index f7372a14..05dfaf2e 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -4264,6 +4264,11 @@ struct WebViewRepresentable: NSViewRepresentable { } private static func installPortalAnchorView(_ anchorView: NSView, in host: NSView) { + // SwiftUI can keep transient replacement hosts alive off-window during split + // reparenting. Never let those hosts steal the shared portal anchor, or the + // portal will bind against an anchor with no real window and WKWebView will + // fall into a hidden/unrendered state. + guard host.window != nil else { return } if anchorView.superview !== host { anchorView.removeFromSuperview() anchorView.frame = host.bounds @@ -4288,13 +4293,15 @@ struct WebViewRepresentable: NSViewRepresentable { let paneDropContext = shouldAttachWebView ? currentPaneDropContext() : nil let activeSearchOverlay = shouldAttachWebView ? searchOverlay : nil let portalAnchorView = panel.portalAnchorView - Self.installPortalAnchorView(portalAnchorView, in: host) + if host.window != nil { + Self.installPortalAnchorView(portalAnchorView, in: host) + } host.onDidMoveToWindow = { [weak host, weak webView, weak coordinator, weak portalAnchorView] in guard let host, let webView, let coordinator, let portalAnchorView else { return } guard coordinator.attachGeneration == generation else { return } - Self.installPortalAnchorView(portalAnchorView, in: host) guard host.window != nil else { return } + Self.installPortalAnchorView(portalAnchorView, in: host) BrowserWindowPortalRegistry.bind( webView: webView, to: portalAnchorView, @@ -4314,6 +4321,7 @@ struct WebViewRepresentable: NSViewRepresentable { guard let host, let webView, let coordinator, let portalAnchorView else { return } guard coordinator.attachGeneration == generation else { return } guard coordinator.lastPortalHostId == ObjectIdentifier(host) else { return } + guard host.window != nil else { return } Self.installPortalAnchorView(portalAnchorView, in: host) if host.window != nil, !BrowserWindowPortalRegistry.isWebView(webView, boundTo: portalAnchorView) {