Replace onScrollGeometryChange/onScrollPhaseChange with ContentHeightPreferenceKey
approach for improved scroll performance and accurate read progress tracking.
Changes:
- Add ScrollOffsetPreferenceKey and ContentHeightPreferenceKey for scroll tracking
- Track content end position dynamically as WebView loads
- Calculate progress from scroll offset relative to total scrollable content
- Implement 3% threshold for progress updates to reduce API calls
- Add progress locking at 100% to prevent pixel-variation regressions
- Guarantee 100% update when reaching end of content
- Apply to both BookmarkDetailLegacyView and BookmarkDetailView2
Technical approach:
- Place invisible marker at end of content to measure position in scrollView
- Update initialContentEndPosition as content grows during WebView loading
- Progress = (initialPosition - currentPosition) / (initialPosition - containerHeight)
- Lock progress once 100% reached to avoid 100% -> 99% fluctuations
Added ContentHeightPreferenceKey to track the total ScrollView content height.
The bug: Progress was calculated using only webViewHeight - containerHeight,
which ignores the header, title, and other content above the webview.
The fix: Use total content height (header + title + webview + archive section)
instead of just webViewHeight for accurate progress calculation.
Changes:
- Added ContentHeightPreferenceKey preference key
- Added contentHeight state variable
- Added background GeometryReader to VStack to measure total content height
- Changed progress calculation: contentHeight - containerHeight (not webViewHeight)
Applied to both BookmarkDetailLegacyView and BookmarkDetailView2.
Changed headerView from var to func with width parameter, matching LegacyView.
Added .frame(width: width, height: headerHeight) to constrain header image width.
This was the root cause of content overflow - without explicit width on the
header image, the entire ZStack and its children (including title and webview)
could grow beyond viewport width. Now matches LegacyView implementation exactly.
Added width: geometry.size.width to the spacer Color.clear.frame()
to constrain the VStack width, matching the LegacyView implementation.
This prevents NativeWebView content from overflowing the screen width.
The explicit width on the spacer propagates to the parent VStack,
which then constrains all child views including NativeWebView.
Removed .frame(maxWidth: .infinity) from NativeWebView which was causing
the content to be wider than the viewport. The NativeWebView now respects
the parent container's width constraints set by .padding(.horizontal, 4).
- Implemented ScrollOffsetPreferenceKey for BookmarkDetailView2
- Replaced onScrollGeometryChange + onScrollPhaseChange with onPreferenceChange
- Removed currentScrollOffset and scrollViewHeight state variables
- Changed jumpButton from var to func with containerHeight parameter
- Fixed excessive spacing between header and content by using ZStack layout
Layout fix: Header image is now in ZStack background with content in foreground,
eliminating double spacing that occurred with separate VStacks.
Performance: Same PreferenceKey benefits as LegacyView - more efficient scroll tracking.
- Created BookmarkDetailView2 with native SwiftUI WebView (iOS 26+)
- Refactored BookmarkDetailView as version router
- Renamed original implementation to BookmarkDetailLegacyView
- Moved Archive/Favorite buttons to bottom toolbar using ToolbarItemGroup
- Added toggle button to switch between native and legacy views
- Implemented onScrollPhaseChange for optimized reading progress tracking
- Added NativeWebView component with improved JavaScript height detection
- All changes preserve existing functionality while adding modern alternatives