-
Notifications
You must be signed in to change notification settings - Fork 55
Fix TabBarMinimize and prefersLargeTitle behaviour #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix TabBarMinimize and prefersLargeTitle behaviour #199
Conversation
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]: software-mansion/react-native-screens#1034
|
Nice work! Do you know if this changes anything in iOS 18 or below? |
On iOS 18 it solves the same issue with the I have updated the SceneDelegate example to also include a tab bar: import HotwireNative
import UIKit
let rootURL = URL(string: "https://hotwire-native-demo.dev")!
class CustomNavigationController: HotwireNavigationController {
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.prefersLargeTitles = true
}
}
extension HotwireTab {
static let all = [
HotwireTab(title: "Home", image: UIImage(systemName: "house")!, url: rootURL)
]
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let tabBarController = HotwireTabBarController()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Hotwire.config.defaultNavigationController = { CustomNavigationController() }
window?.rootViewController = tabBarController
tabBarController.load(HotwireTab.all)
}
}Beforeios18-before.mp4Afterios18-after.mp4 |
|
This resolved the prefersLargeTitle behaviour in our app as well (tested on both iOS 18 and 26), thank you @nbelzer! |
|
This is great, nice work @nbelzer! I'm inclined to also prefer large titles in the demo app (see diff bellow). But when I launch the app for the first time, the first tab doesn't correctly show a large title. Are you seeing the same? RocketSim_Recording_iPhone_17_Pro_6.3_2025-11-06_08.17.01.mp4diff --git a/Demo/AppDelegate.swift b/Demo/AppDelegate.swift
index a6912b9..42ad6e8 100644
--- a/Demo/AppDelegate.swift
+++ b/Demo/AppDelegate.swift
@@ -38,6 +38,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
OverflowMenuComponent.self,
])
+ // Use a custom navigation controller that prefers large titles.
+ Hotwire.config.defaultNavigationController = {
+ LargeTitleNavigationController()
+ }
+
// Set configuration options
Hotwire.config.backButtonDisplayMode = .minimal
Hotwire.config.showDoneButtonOnModals = truediff --git a/Demo/LargeTitleNavigationController.swift b/Demo/LargeTitleNavigationController.swift
new file mode 100644
index 0000000..224149a
--- /dev/null
+++ b/Demo/LargeTitleNavigationController.swift
@@ -0,0 +1,9 @@
+import HotwireNative
+import UIKit
+
+class LargeTitleNavigationController: HotwireNavigationController {
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ navigationBar.prefersLargeTitles = true
+ }
+} |
|
Found an additional reference indicating that the scrollable view should be placed at index 0 in the subview hierarchy. Regarding the issue with large title on load, I took some time to look into the issue and found the following:
import WebKit
import UIKit
let rootURL = URL(string: "https://hotwire-native-demo.dev")!
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let navigationController = UINavigationController()
navigationController.navigationBar.prefersLargeTitles = true
window?.rootViewController = navigationController
let webView = WKWebView(frame: .zero)
webView.translatesAutoresizingMaskIntoConstraints = false
let request = URLRequest(url: rootURL)
webView.load(request)
let viewController = UIViewController()
viewController.view.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
webView.topAnchor.constraint(equalTo: viewController.view.topAnchor),
webView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor)
])
viewController.title = "Home"
navigationController.pushViewController(viewController, animated: true)
}
}issue-on-plain-scene-delegate.mp4
Based on the fact that the issue with large titles appears outside the HotwireNative library, and the incorrect contentOffset is being set by internal WebKit logic, I believe the issue is a bug in iOS. I reported my findings to Apple through the Feedback Assistant. I will keep this thread updated when I hear something back. On the short-term we might want to look into a fix built into the HotwireNative library. @joemasilotti Do we want to include such a fix as part of this PR? |
|
Great work on this @nbelzer! And thanks for reporting to Apple. I'm all in favor of this change. I think we had a similar PR back before the Hotwire Native rebrand but it got lost in the transition. Also of note, I think the bug is actually fixed in iOS 26.1! Here's two simulators, iOS 26.1 on the left and iOS 26.0 on the right, running the same code you shared above. Notice how the large title works as expected right on launch in iOS 26.1.
@svara, is it possible to test this against HEY/Basecamp? Everything seems to work fine in the apps I'm currently working on but I'd love another check before merging this. |



I recently delved into an issue with the new TabBarMinimize functionality in iOS 26. For certain visits the minimize behavior would not trigger on scroll resulting in an unpredictable user experience.
Based on my findings the scrolling of the webView is not always detected by the TabBarController, therefore not triggering the minimized state of the tab bar. This also seems to extend to other functionality that triggers on scroll like the prefersLargeTitle configuration.
Based on my initial findings the issue appears to be that the webView in HotwireNative is not always the first child in the view hierarchy when the view appears. In HotwireNative we have several other views, like the
screenshotContainerViewandactivityIndicatorViewthat are added and removed depending on the loading state of the page.The proposed fix is to always place the webView as the first view in the hierarchy using insertSubview.
A better solution might be to rework the way we add views to the VisitableView such that the webview is always the first child. 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.
I have included more specific details in the commit description.
Would love to see this fixed in a future release. Also happy to discuss and look into any alternatives.
Example
At the moment I cannot share any examples from the app I am working on. However, I was able to use the demo app to re-create the issue using the following SceneDelegate:
Before
before.mp4
After
after.mp4