feat: Connect SwiftUI ScrollView tracking to WebView coordinator

- Added WebViewCoordinator reference storage in BookmarkDetailLegacyView
- Added updateScrollProgress() method to WebViewCoordinator with 3% threshold
- Connected onScrollPhaseChange to coordinator's updateScrollProgress
- Added onExternalScrollUpdate callback to pass coordinator reference
- Scroll progress now flows: SwiftUI ScrollView -> Coordinator -> onScroll callback

This bridges the gap between SwiftUI ScrollView (which wraps the WebView)
and the JavaScript-style scroll progress tracking with threshold.
This commit is contained in:
Ilyas Hallak 2025-10-10 15:33:20 +02:00
parent 0a53705df1
commit 5c9c00134a
2 changed files with 58 additions and 6 deletions

View File

@ -12,9 +12,11 @@ struct BookmarkDetailLegacyView: View {
@State private var showingFontSettings = false
@State private var showingLabelsSheet = false
@State private var readingProgress: Double = 0.0
@State private var currentScrollOffset: CGFloat = 0
@State private var showJumpToProgressButton: Bool = false
@State private var scrollPosition = ScrollPosition(edge: .top)
@State private var showingImageViewer = false
@State private var webViewCoordinator: WebViewCoordinator? = nil
// MARK: - Envs
@ -60,6 +62,10 @@ struct BookmarkDetailLegacyView: View {
print("🔥 BookmarkDetailLegacyView onScroll callback: \(String(format: "%.3f", scrollPercent)) (\(String(format: "%.1f", scrollPercent * 100))%)")
readingProgress = scrollPercent
viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: scrollPercent, anchor: nil)
},
onExternalScrollUpdate: { coordinator in
print("🎯 WebView coordinator ready, storing reference")
webViewCoordinator = coordinator
}
)
.frame(height: webViewHeight)
@ -100,6 +106,24 @@ struct BookmarkDetailLegacyView: View {
}
.ignoresSafeArea(edges: .top)
.scrollPosition($scrollPosition)
.onScrollGeometryChange(for: CGFloat.self) { geo in
geo.contentOffset.y
} action: { oldValue, newValue in
currentScrollOffset = newValue
}
.onScrollPhaseChange { oldPhase, newPhase in
// Only calculate when scrolling ends (interacting -> idle)
if oldPhase == .interacting && newPhase == .idle {
print("📊 Scroll ended at offset: \(currentScrollOffset)")
let offset = currentScrollOffset
let maxOffset = webViewHeight - geometry.size.height
print("🎯 Sending to WebView coordinator: offset=\(offset), maxOffset=\(maxOffset)")
// Send to WebView coordinator which handles the 3% threshold
webViewCoordinator?.updateScrollProgress(offset: offset, maxOffset: maxOffset)
}
}
}
}
.frame(maxWidth: .infinity)

View File

@ -6,6 +6,7 @@ struct WebView: UIViewRepresentable {
let settings: Settings
let onHeightChange: (CGFloat) -> Void
var onScroll: ((Double) -> Void)? = nil
var onExternalScrollUpdate: ((WebViewCoordinator) -> Void)? = nil
@Environment(\.colorScheme) private var colorScheme
func makeUIView(context: Context) -> WKWebView {
@ -22,6 +23,8 @@ struct WebView: UIViewRepresentable {
webView.isOpaque = false
webView.backgroundColor = UIColor.clear
print("🟢 WebView created with scrolling DISABLED (embedded in ScrollView)")
// Allow text selection and copying
webView.allowsBackForwardNavigationGestures = false
webView.allowsLinkPreview = true
@ -30,6 +33,10 @@ struct WebView: UIViewRepresentable {
webView.configuration.userContentController.add(context.coordinator, name: "scrollProgress")
context.coordinator.onHeightChange = onHeightChange
context.coordinator.onScroll = onScroll
context.coordinator.webView = webView
// Notify parent that coordinator is ready
onExternalScrollUpdate?(context.coordinator)
return webView
}
@ -344,6 +351,9 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
var onHeightChange: ((CGFloat) -> Void)?
var onScroll: ((Double) -> Void)?
// WebView reference
weak var webView: WKWebView?
// Height management
var lastHeight: CGFloat = 0
var pendingHeight: CGFloat = 0
@ -354,6 +364,7 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
var scrollVelocity: Double = 0
var lastScrollTime: Date = Date()
var scrollEndTimer: Timer?
var lastSentProgress: Double = 0
// Lifecycle
private var isCleanedUp = false
@ -455,6 +466,23 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
onHeightChange?(height)
}
// Method to receive scroll updates from SwiftUI ScrollView
func updateScrollProgress(offset: CGFloat, maxOffset: CGFloat) {
let progress = maxOffset > 0 ? min(max(offset / maxOffset, 0), 1) : 0
print("📊 External scroll update: offset=\(offset), maxOffset=\(maxOffset), progress=\(String(format: "%.3f", progress))")
// Only send if change >= 3%
let threshold: Double = 0.03
if abs(progress - lastSentProgress) >= threshold {
print("✅ Calling onScroll callback with: \(String(format: "%.3f", progress))")
lastSentProgress = progress
onScroll?(progress)
} else {
print("⏸️ Skipping (change < 3%): \(String(format: "%.3f", abs(progress - lastSentProgress)))")
}
}
func cleanup() {
guard !isCleanedUp else { return }
isCleanedUp = true