feat: Implement JavaScript-based scroll tracking in BookmarkDetailLegacyView

- Added scroll progress tracking via JavaScript in WebView
- Implemented 3% threshold to reduce message frequency
- Removed SwiftUI onScrollGeometryChange and onScrollPhaseChange
- Cleaned up unused state variables (scrollViewHeight, currentScrollOffset)
- Removed Combine import (no longer needed)
- Disabled JumpButton scroll-to-position (requires JavaScript implementation)

This approach offloads scroll tracking to the WebView's JavaScript,
reducing SwiftUI state updates and improving performance.
This commit is contained in:
Ilyas Hallak 2025-10-10 14:47:56 +02:00
parent 171bf881fb
commit 32dbab400e
2 changed files with 31 additions and 52 deletions

View File

@ -1,6 +1,5 @@
import SwiftUI import SwiftUI
import SafariServices import SafariServices
import Combine
struct BookmarkDetailLegacyView: View { struct BookmarkDetailLegacyView: View {
let bookmarkId: String let bookmarkId: String
@ -13,8 +12,6 @@ struct BookmarkDetailLegacyView: View {
@State private var showingFontSettings = false @State private var showingFontSettings = false
@State private var showingLabelsSheet = false @State private var showingLabelsSheet = false
@State private var readingProgress: Double = 0.0 @State private var readingProgress: Double = 0.0
@State private var scrollViewHeight: CGFloat = 1
@State private var currentScrollOffset: CGFloat = 0
@State private var showJumpToProgressButton: Bool = false @State private var showJumpToProgressButton: Bool = false
@State private var scrollPosition = ScrollPosition(edge: .top) @State private var scrollPosition = ScrollPosition(edge: .top)
@State private var showingImageViewer = false @State private var showingImageViewer = false
@ -51,11 +48,19 @@ struct BookmarkDetailLegacyView: View {
JumpButton() JumpButton()
} }
if let settings = viewModel.settings, !viewModel.articleContent.isEmpty { if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
WebView(htmlContent: viewModel.articleContent, settings: settings, onHeightChange: { height in WebView(
if webViewHeight != height { htmlContent: viewModel.articleContent,
webViewHeight = height settings: settings,
onHeightChange: { height in
if webViewHeight != height {
webViewHeight = height
}
},
onScroll: { scrollPercent in
readingProgress = scrollPercent
viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: scrollPercent, anchor: nil)
} }
}) )
.frame(height: webViewHeight) .frame(height: webViewHeight)
.cornerRadius(14) .cornerRadius(14)
.padding(.horizontal, 4) .padding(.horizontal, 4)
@ -94,33 +99,6 @@ struct BookmarkDetailLegacyView: View {
} }
.ignoresSafeArea(edges: .top) .ignoresSafeArea(edges: .top)
.scrollPosition($scrollPosition) .scrollPosition($scrollPosition)
.onScrollGeometryChange(for: CGFloat.self) { geo in
geo.contentOffset.y
} action: { oldValue, newValue in
// Just track current offset, don't calculate yet
currentScrollOffset = newValue
}
.onScrollGeometryChange(for: CGFloat.self) { geo in
geo.containerSize.height
} action: { oldValue, newValue in
scrollViewHeight = newValue
}
.onScrollPhaseChange { oldPhase, newPhase in
// Only calculate progress when scrolling ends
if oldPhase == .interacting && newPhase == .idle {
let offset = currentScrollOffset
let maxOffset = webViewHeight - geometry.size.height
let rawProgress = offset / (maxOffset > 0 ? maxOffset : 1)
let progress = min(max(rawProgress, 0), 1)
// Only update if change is significant (> 5%)
let threshold: Double = 0.05
if abs(progress - readingProgress) > threshold {
readingProgress = progress
viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: progress, anchor: nil)
}
}
}
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -449,12 +427,10 @@ struct BookmarkDetailLegacyView: View {
@ViewBuilder @ViewBuilder
func JumpButton() -> some View { func JumpButton() -> some View {
Button(action: { Button(action: {
let maxOffset = webViewHeight - scrollViewHeight // TODO: Implement scroll-to-position via JavaScript
let offset = maxOffset * (Double(viewModel.readProgress) / 100.0) // Since we're now using JavaScript-based scroll tracking,
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { // we need to send a message to the WebView to scroll to the position
scrollPosition = ScrollPosition(y: offset) showJumpToProgressButton = false
showJumpToProgressButton = false
}
}) { }) {
Text("Jump to last read position (\(viewModel.readProgress)%)") Text("Jump to last read position (\(viewModel.readProgress)%)")
.font(.subheadline) .font(.subheadline)

View File

@ -264,21 +264,24 @@ struct WebView: UIViewRepresentable {
document.querySelectorAll('img').forEach(img => { document.querySelectorAll('img').forEach(img => {
img.addEventListener('load', debouncedHeightUpdate); img.addEventListener('load', debouncedHeightUpdate);
}); });
let lastSent = { value: 0 };
window.addEventListener('scroll', function() { window.addEventListener('scroll', function() {
isScrolling = true; isScrolling = true;
let scrollTop = window.scrollY || document.documentElement.scrollTop;
let docHeight = document.documentElement.scrollHeight - window.innerHeight;
let scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
if (Math.abs(scrollPercent - lastSent.value) >= 3) {
window.webkit.messageHandlers.scrollProgress.postMessage(scrollPercent / 100);
lastSent.value = scrollPercent;
}
clearTimeout(scrollTimeout); clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function() { scrollTimeout = setTimeout(function() {
var scrollTop = window.scrollY || document.documentElement.scrollTop; isScrolling = false;
var docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; debouncedHeightUpdate();
var progress = docHeight > 0 ? scrollTop / docHeight : 0; }, 200);
window.webkit.messageHandlers.scrollProgress.postMessage(progress);
setTimeout(function() {
isScrolling = false;
debouncedHeightUpdate();
}, 200);
}, 16);
}); });
</script> </script>
</body> </body>