Fix large titles rendering collapsed on initial display#256
Closed
pardeyke wants to merge 3 commits into
Closed
Conversation
A web view attached after its view controller is already on screen misses UIKit's automatic initial content-offset adjustment, leaving it at contentOffset 0 instead of -adjustedContentInset.top. Since hotwired#199 explicitly designates the web view's scroll view as the tracked content scroll view, the navigation bar treats the page as scrolled down and renders large titles (prefersLargeTitles + largeTitleDisplayMode = .always) collapsed on initial display, only expanding after the user scrolls to the top. Align the scroll offset to the adjusted top after activation, once the pending layout pass has produced the final insets. Restored scroll positions (contentOffset.y > 0) are left untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turbo scrolls to the top after rendering, which WKWebView applies against the collapsed-bar geometry. UIKit interprets that as an upward scroll and collapses a large title — and a collapsed bar resting at the top never re-expands without a user drag. Screens configured for large titles could therefore appear expanded during the push transition and then collapse the moment the page finished rendering, with the outcome depending on render timing. After visitableDidRender, re-align the scroll offset to the adjusted top and re-measure the navigation bar, leaving restored scroll positions below the top untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field testing against a real app showed two things the previous commit missed. First, the web view activates before its view controller is even pushed, and Turbo's cache-restore render scrolls it against the outgoing page's geometry — so UIKit derives the incoming bar state from a stale offset and reliably locks large titles collapsed on every page-cache revisit. Second, neither navigationBar.sizeToFit() nor detaching the content scroll view re-expands a collapsed bar at rest (the subview-order fallback keeps the scroll view tracked regardless); the only input UIKit responds to is tracked scroll offset changes. Sample the adjusted top inset whenever an expanded large-title bar is observed, and after every appearance/render — once transitions have settled and only for the topmost, on-screen controller — re-align a web view resting at the top and, if the bar is stuck collapsed, drive the tracked offset to the sampled expanded top so the bar reopens. Restored scroll positions below the top are left untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
|
Closing this — broader testing showed the approach doesn't hold up reliably. Filed the full analysis as #257 instead. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Since #199, the web view's scroll view is explicitly designated as the navigation bar's tracked content scroll view (and made the first subview, so UIKit's ordering heuristic finds it too). That fixed large titles never collapsing on scroll (#169), but it exposed a new issue: with
navigationBar.prefersLargeTitles = trueandnavigationItem.largeTitleDisplayMode = .always, screens frequently show a collapsed (small) title when they first appear, only expanding after the user drags down. Which screens are affected feels random — the same screen can work on one navigation and fail on the next.Root cause (verified by instrumenting a real app)
I logged
contentOffset/adjustedContentInset/ bar height / transition state through the visitable lifecycle in a production Hotwire Native app:navigationControlleris still nil at activation), so nav-bar tracking begins while the outgoing page's geometry is active.-116instead of the incoming expanded top-168./clientsscreen (page-cache hit — deterministic reproduction):First visits usually survive (no cached snapshot to render early); revisits reliably fail. Slow first renders can also collapse the bar seconds after a successful appearance. That mix is why the symptom looks random.
Two negative results worth recording, both verified in the same setup:
navigationBar.sizeToFit()does not re-expand a collapsed bar resting at the top.setContentScrollView(nil, for: .all)doesn't help either — the first-subview ordering keeps the scroll view tracked through UIKit's fallback heuristic.The only input UIKit responds to is tracked scroll offset changes (which is also what collapsed the bar in the first place).
The fix
Visitable.activateVisitableWebView): align the scroll offset to the adjusted top on the next run-loop tick, only when the scroll view sits between its unadjusted and adjusted top — a restored scroll position is left untouched.VisitableViewController): once any transition has settled, and only for the topmost on-screen controller, sample the adjusted top inset whenever an expanded large-title bar is observed; if the bar is instead stuck collapsed with the page resting at the top, drive the tracked offset to the sampled expanded top — the bar and insets follow, restoring the expanded large title. Pages scrolled below the top are left alone, and screens withlargeTitleDisplayMode == .never(or.automatic, which may legitimately collapse) are never forced.Steps to reproduce (on main / 1.3.0)
navigationBar.prefersLargeTitles = trueviaHotwire.config.defaultNavigationController.HotwireWebViewControllersubclass, setnavigationItem.largeTitleDisplayMode = .alwaysinviewWillAppearfor some screens.With this change, large titles render expanded from the settled first frame and stay expanded after Turbo finishes rendering — verified end-to-end in a real app on the iOS simulator across cold tab loads, pushes from small-title screens, page-cache revisits, and pops back to small-title screens (no lingering large title).
🤖 Generated with Claude Code