From 8aa5c7ee43c2851def09053e8d3cf56d43c22941 Mon Sep 17 00:00:00 2001 From: Nick Belzer Date: Fri, 24 Oct 2025 16:18:23 +0200 Subject: [PATCH 1/2] Fix TabBarMinimize behaviour on visit When we perform a visit we add several views to the VisitableView in the order: 1. activityIndicatorView 2. screenshotContainerView 3. webView Whenever a visits starts we move the activityIndicator to the bottom of the hierarchy (using `bringSubviewToFront`) and before displaying the view we remove the screenshotContainerView (using `removeFromSuperview`). As a result the webView ends up being the first view in the hierarchy when inspecting the application at any point after loading the Visitable. When starting the application and performing the initial load this process causes no issues. However, with subsequent visits this results in behaviour like the UINavigationBar.prefersLargeTitles or TabBarMinimize functionality in iOS 26 not working as expected. It appears like scrolling through the webview is not detected, therefore therefore not triggering these specific behaviours. While this is undocumented by Apple I found several sources[^1][^2] that indicate that behaviour like UINavigationBar.prefersLargeTitles requires that the scollable view is the first child in the view hierarchy. While our webview is eventually the first view in the hierachy, the fact that it isn't during loading seems to cause the issues that I experienced. This led me to experiment with insertSubview to ensure the webView is always the first child in the view hierarchy. As a result I have seen no more issues with the behaviours mentioned above. A better solution might be to rework the way we add views to the VisitableView such that the webview always ends up in the initial position. While I attempted such a solution I found that using insertSubview is more explicit and less likely to break by future changes to this file. [^1]: https://swiftsenpai.com/development/large-title-uinavigationbar-glitches/ [^2]: https://github.com/software-mansion/react-native-screens/issues/1034 --- Source/Turbo/Visitable/VisitableView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Turbo/Visitable/VisitableView.swift b/Source/Turbo/Visitable/VisitableView.swift index f0adc362..162469c1 100644 --- a/Source/Turbo/Visitable/VisitableView.swift +++ b/Source/Turbo/Visitable/VisitableView.swift @@ -24,7 +24,10 @@ open class VisitableView: UIView { open func activateWebView(_ webView: WKWebView, forVisitable visitable: Visitable) { self.webView = webView self.visitable = visitable - addSubview(webView) + // For behaviour like UINavigationBar.prefersLargeTitles or + // TabBarMinimize to work the scrollable view shoud be the + // first child in the hierarchy from the moment it is added. + insertSubview(webView, at: 0) addFillConstraints(for: webView) installRefreshControl() showOrHideWebView() From ecf33138853f14d615092f8b901bf10aabd48133 Mon Sep 17 00:00:00 2001 From: Nick Belzer Date: Sun, 2 Nov 2025 10:04:29 +0100 Subject: [PATCH 2/2] Add VisitableView subview ordering tests --- .../Turbo/VisitableViewControllerTests.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Tests/Turbo/VisitableViewControllerTests.swift b/Tests/Turbo/VisitableViewControllerTests.swift index a3bcb3ca..d971d234 100644 --- a/Tests/Turbo/VisitableViewControllerTests.swift +++ b/Tests/Turbo/VisitableViewControllerTests.swift @@ -47,6 +47,30 @@ class VisitableViewControllerTests: XCTestCase { XCTAssertEqual(viewController.initialVisitableURL, originalURL) XCTAssertEqual(viewController.currentVisitableURL, overriddenURL) } + + func test_webview_is_first_child_during_loading() { + XCTAssertEqual(viewController.visitableView.subviews.first, viewController.visitableView.activityIndicatorView) + + viewController.visitableView.activateWebView(webView, forVisitable: viewController) + + XCTAssertEqual(viewController.visitableView.subviews.first, webView) + XCTAssertEqual(viewController.visitableView.subviews.last, viewController.visitableView.activityIndicatorView) + } + + func test_webview_is_first_child_on_restore() { + XCTAssertEqual(viewController.visitableView.subviews.first, viewController.visitableView.activityIndicatorView) + + viewController.showVisitableScreenshot() + viewController.visitableView.activateWebView(webView, forVisitable: viewController) + + XCTAssertEqual(viewController.visitableView.subviews.count, 3) + XCTAssertEqual(viewController.visitableView.subviews.first, viewController.visitableView.webView) + + viewController.hideVisitableScreenshot() + + XCTAssertEqual(viewController.visitableView.subviews.count, 2) + XCTAssertEqual(viewController.visitableView.subviews.first, viewController.visitableView.webView) + } } final class WebViewSpy: WKWebView {