import SwiftUI import WebKit // iOS 26+ Native SwiftUI WebView Implementation @available(iOS 26.0, *) struct NativeWebView: View { let htmlContent: String let settings: Settings let onHeightChange: (CGFloat) -> Void var onScroll: ((Double) -> 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() } .onChange(of: htmlContent) { _, _ in loadStyledContent() } .onChange(of: colorScheme) { _, _ 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 updateContentHeightWithJS() async { var lastHeight: CGFloat = 0 // More frequent attempts with shorter delays let delays = [0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0] // 9 attempts 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 let result = try await webPage.callJavaScript("getContentHeight()") if let height = result as? Double, height > 0 { let cgHeight = CGFloat(height) // Update height if it's significantly different or this is the first valid measurement if lastHeight == 0 || abs(cgHeight - lastHeight) > 10 { print("JavaScript height updated: \(height)px on attempt \(attempt)") DispatchQueue.main.async { self.onHeightChange(cgHeight) } lastHeight = cgHeight } // If height seems stable (no change in last few attempts), we can exit early if attempt >= 3 && lastHeight > 0 { print("Height stabilized at \(lastHeight)px") return } } } catch { print("JavaScript attempt \(attempt) failed: \(error)") } } // If no valid height was found, use fallback if lastHeight == 0 { print("No valid JavaScript height found, using fallback") updateContentHeightFallback() } } 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)) 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" } } } // Main WebView - automatically chooses best implementation struct WebView: 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 WKWebView wrapper for older iOS LegacyWebView( htmlContent: htmlContent, settings: settings, onHeightChange: onHeightChange, onScroll: onScroll ) } } } // Fallback: Original WKWebView Implementation struct LegacyWebView: UIViewRepresentable { let htmlContent: String let settings: Settings let onHeightChange: (CGFloat) -> Void var onScroll: ((Double) -> Void)? = nil @Environment(\.colorScheme) private var colorScheme func makeUIView(context: Context) -> WKWebView { let configuration = WKWebViewConfiguration() // Enable text selection and copy functionality let preferences = WKWebpagePreferences() preferences.allowsContentJavaScript = true configuration.defaultWebpagePreferences = preferences let webView = WKWebView(frame: .zero, configuration: configuration) webView.navigationDelegate = context.coordinator webView.scrollView.isScrollEnabled = false webView.isOpaque = false webView.backgroundColor = UIColor.clear // Allow text selection and copying webView.allowsBackForwardNavigationGestures = false webView.allowsLinkPreview = true // Message Handler für Height und Scroll Updates webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate") webView.configuration.userContentController.add(context.coordinator, name: "scrollProgress") context.coordinator.onHeightChange = onHeightChange context.coordinator.onScroll = onScroll return webView } func updateUIView(_ webView: WKWebView, context: Context) { // Update callbacks context.coordinator.onHeightChange = onHeightChange context.coordinator.onScroll = onScroll let isDarkMode = colorScheme == .dark // Font Settings aus Settings-Objekt let fontSize = getFontSize(from: settings.fontSize ?? .extraLarge) let fontFamily = getFontFamily(from: settings.fontFamily ?? .serif) let styledHTML = """ \(htmlContent) """ webView.loadHTMLString(styledHTML, baseURL: nil) } func makeCoordinator() -> WebViewCoordinator { WebViewCoordinator() } 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, 'Segoe UI', Roboto, sans-serif" case .serif: return "'Times New Roman', Times, 'Liberation Serif', serif" case .sansSerif: return "'Helvetica Neue', Helvetica, Arial, sans-serif" case .monospace: return "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace" } } } class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { var onHeightChange: ((CGFloat) -> Void)? var onScroll: ((Double) -> Void)? var hasHeightUpdate: Bool = false var isScrolling: Bool = false var scrollEndTimer: Timer? deinit { scrollEndTimer?.invalidate() scrollEndTimer = nil } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { if let url = navigationAction.request.url { UIApplication.shared.open(url) decisionHandler(.cancel) return } } decisionHandler(.allow) } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { switch message.name { case "heightUpdate": guard let height = message.body as? CGFloat else { return } DispatchQueue.main.async { // Block height updates during active scrolling to prevent flicker if !self.isScrolling && !self.hasHeightUpdate { self.onHeightChange?(height) self.hasHeightUpdate = true } } case "scrollProgress": guard let progress = message.body as? Double else { return } DispatchQueue.main.async { // Track scrolling state self.isScrolling = true // Reset scrolling state after scroll ends self.scrollEndTimer?.invalidate() self.scrollEndTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in self?.isScrolling = false } self.onScroll?(progress) } default: print("Unknown message: \(message.name)") } } }