import SwiftUI import WebKit // MARK: - iOS 26+ Native SwiftUI WebView Implementation // This implementation is available but not currently used // To activate: Replace WebView usage with hybrid approach using #available(iOS 26.0, *) @available(iOS 26.0, *) struct NativeWebView: View { let htmlContent: String let settings: Settings let onHeightChange: (CGFloat) -> Void var onScroll: ((Double) -> Void)? = nil var selectedAnnotationId: String? var onTextSelected: ((String, Int, Int) -> Void)? = nil @State private var webPage = WebPage() @Environment(\.colorScheme) private var colorScheme var body: some View { WebKit.WebView(webPage) .scrollDisabled(true) // Disable internal scrolling .onAppear { loadStyledContent() setupTextSelectionCallback() } .onChange(of: htmlContent) { _, _ in loadStyledContent() } .onChange(of: colorScheme) { _, _ in loadStyledContent() } .onChange(of: selectedAnnotationId) { _, _ in loadStyledContent() } .onChange(of: webPage.isLoading) { _, isLoading in if !isLoading { // Update height when content finishes loading DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { Task { await updateContentHeightWithJS() } } } } } private func setupTextSelectionCallback() { guard let onTextSelected = onTextSelected else { return } // Poll for text selection using JavaScript Task { while true { try? await Task.sleep(nanoseconds: 300_000_000) // Check every 0.3s let script = """ (function() { const selection = window.getSelection(); if (selection && selection.toString().length > 0) { const range = selection.getRangeAt(0); const selectedText = selection.toString(); const preRange = document.createRange(); preRange.selectNodeContents(document.body); preRange.setEnd(range.startContainer, range.startOffset); const startOffset = preRange.toString().length; const endOffset = startOffset + selectedText.length; return { text: selectedText, startOffset: startOffset, endOffset: endOffset }; } return null; })(); """ do { if let result = try await webPage.evaluateJavaScript(script) as? [String: Any], let text = result["text"] as? String, let startOffset = result["startOffset"] as? Int, let endOffset = result["endOffset"] as? Int { await MainActor.run { onTextSelected(text, startOffset, endOffset) } } } catch { // Silently continue polling } } } } private func updateContentHeightWithJS() async { var lastHeight: CGFloat = 0 // Similar strategy to WebView: multiple attempts with increasing delays let delays = [0.1, 0.2, 0.5, 1.0, 1.5, 2.0] // 6 attempts like WebView for (index, delay) in delays.enumerated() { let attempt = index + 1 try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) do { // Try to get height via JavaScript - use simple document.body.scrollHeight let result = try await webPage.callJavaScript("return document.body.scrollHeight") if let height = result as? Double, height > 0 { let cgHeight = CGFloat(height) // Update height if it's significantly different (> 5px like WebView) if lastHeight == 0 || abs(cgHeight - lastHeight) > 5 { print("🟢 NativeWebView - JavaScript height updated: \(height)px on attempt \(attempt)") DispatchQueue.main.async { self.onHeightChange(cgHeight) } lastHeight = cgHeight } // If height seems stable (no change in last 2 attempts), we can exit early if attempt >= 2 && lastHeight > 0 { print("🟢 NativeWebView - Height stabilized at \(lastHeight)px after \(attempt) attempts") return } } } catch { print("🟡 NativeWebView - JavaScript attempt \(attempt) failed: \(error)") } } // If no valid height was found, use fallback if lastHeight == 0 { print("🔴 NativeWebView - No valid JavaScript height found, using fallback") updateContentHeightFallback() } else { print("🟢 NativeWebView - Final height: \(lastHeight)px") } } private func updateContentHeightFallback() { // Simplified fallback calculation let fontSize = getFontSize(from: settings.fontSize ?? .extraLarge) let plainText = htmlContent.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) let characterCount = plainText.count let estimatedLines = max(1, characterCount / 80) let textHeight = CGFloat(estimatedLines) * CGFloat(fontSize) * 1.8 let finalHeight = max(400, min(textHeight + 100, 3000)) print("🟡 NativeWebView - Using fallback height: \(finalHeight)px") DispatchQueue.main.async { self.onHeightChange(finalHeight) } } private func loadStyledContent() { let isDarkMode = colorScheme == .dark let fontSize = getFontSize(from: settings.fontSize ?? .extraLarge) let fontFamily = getFontFamily(from: settings.fontFamily ?? .serif) let styledHTML = """ \(htmlContent) """ webPage.load(html: styledHTML) // Update height after content loads DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { Task { await updateContentHeightWithJS() } } } private func getFontSize(from fontSize: FontSize) -> Int { switch fontSize { case .small: return 14 case .medium: return 16 case .large: return 18 case .extraLarge: return 20 } } private func getFontFamily(from fontFamily: FontFamily) -> String { switch fontFamily { case .system: return "-apple-system, BlinkMacSystemFont, sans-serif" case .serif: return "'Times New Roman', Times, serif" case .sansSerif: return "'Helvetica Neue', Helvetica, Arial, sans-serif" case .monospace: return "'SF Mono', Menlo, Monaco, monospace" } } private func generateTextSelectionJS() -> String { // Not needed for iOS 26 - we use polling instead return "" } private func generateScrollToAnnotationJS() -> String { guard let selectedId = selectedAnnotationId else { return "" } return """ // Scroll to selected annotation and add selected class function scrollToAnnotation() { // Remove 'selected' class from all annotations document.querySelectorAll('rd-annotation.selected').forEach(el => { el.classList.remove('selected'); }); // Find and highlight selected annotation const selectedElement = document.querySelector('rd-annotation[data-annotation-id-value="\(selectedId)"]'); if (selectedElement) { selectedElement.classList.add('selected'); setTimeout(() => { selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', scrollToAnnotation); } else { setTimeout(scrollToAnnotation, 300); } """ } } // MARK: - Hybrid WebView (Not Currently Used) // This would be the implementation to use both native and legacy WebViews // Currently commented out - the app uses only the crash-resistant WebView /* struct HybridWebView: View { let htmlContent: String let settings: Settings let onHeightChange: (CGFloat) -> Void var onScroll: ((Double) -> Void)? = nil var body: some View { if #available(iOS 26.0, *) { // Use new native SwiftUI WebView on iOS 26+ NativeWebView( htmlContent: htmlContent, settings: settings, onHeightChange: onHeightChange, onScroll: onScroll ) } else { // Fallback to crash-resistant WebView for older iOS WebView( htmlContent: htmlContent, settings: settings, onHeightChange: onHeightChange, onScroll: onScroll ) } } } */