1

I'm working on a SwiftUI app using SwiftData, and I'm very new to both frameworks, and swift in general to be honest. I'm trying to create a webview that embeds a monaco editor that will then send the change events to swift to track the changes to the initial content in SwiftData.

This is what that struct looks like:

import SwiftData import SwiftUI import WebKit enum CodeEditorTheme: String, Codable, CaseIterable { case materialLight, solarizedLight, githubLight, aura, tokyoNightDay, dracula, tokyoNight, materialDark, tokyoNightStorm, githubDark, solarizedDark, xcodeDark, xcodeLight } func getConfig() -> WKWebViewConfiguration { let config = WKWebViewConfiguration() config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") config.setValue(true, forKey: "allowUniversalAccessFromFileURLs") return config } struct ResponsiveEditorWebView: UIViewRepresentable { let url: URL @State private var webView: WKWebView = WKWebView( frame: .zero, configuration: getConfig() ) @Environment(\.openURL) var openURL @Environment(\.modelContext) var modelContext @Binding var theme: WebViewTheme @Binding var editorTheme: CodeEditorTheme @Binding var editingNote: NoteModel? @State var haveSetInitialContent: Bool = false @Environment(\.colorScheme) var colorScheme { didSet { applyWebViewColorScheme() } } func makeUIView(context: Context) -> WKWebView { self.webView.navigationDelegate = context.coordinator self.webView.scrollView.isScrollEnabled = false self.webView.scrollView.zoomScale = 1 self.webView.scrollView.minimumZoomScale = 1 self.webView.scrollView.maximumZoomScale = 1 self.webView.allowsBackForwardNavigationGestures = false self.webView.configuration.userContentController.add( context.coordinator, name: "editor-update" ) if #available(iOS 16.4, macOS 13.3, *) { self.webView.isInspectable = true // Enable inspection } // now load the local url self.webView.loadFileURL(url, allowingReadAccessTo: url) // emitEditorThemeEvent(theme: editorTheme) // applyWebViewColorScheme() return self.webView } func updateUIView(_ uiView: WKWebView, context: Context) { if !haveSetInitialContent { // Even with these commented off the issue persists. // applyWebViewColorScheme() // emitEditorThemeEvent(theme: editorTheme) // setInitialContent() } uiView.loadFileURL(url, allowingReadAccessTo: url) } func makeCoordinator() -> Coordinator { Coordinator(self) } func setInitialContent() { if !haveSetInitialContent { print("Setting initial content") let body = editingNote?.markdown.body.replacingOccurrences( of: "`", with: "\\`" ) self.webView.evaluateJavaScript( """ window.localStorage.setItem("editor-initial-value", `\(body ?? "")`) """ ) { (result, error) in if error != nil { print("set initial value error: ", error) } else { print("Set initial value result: ", result) haveSetInitialContent = true } } } } func emitEditorThemeEvent(theme: CodeEditorTheme) { print("Changing editor theme event") self.webView.evaluateJavaScript( """ window.localStorage.setItem("editor-theme", "\(theme.rawValue)") """ ) { (result, error) in if error != nil { print("Error: ", error) } else { print("Result: ", result) } } } func applyWebViewColorScheme() { print("Applying webview color scheme") self.webView.evaluateJavaScript( """ window.localStorage.setItem("darkMode", "\(colorScheme == .dark ? "true" : "false")") """ ) { (result, error) in if error != nil { print("Error: ", error) } else { print("Result: ", result) } } } class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { var parent: ResponsiveEditorWebView init(_ parent: ResponsiveEditorWebView) { self.parent = parent } // Delegate method to decide policy for navigation func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { if let url = navigationAction.request.url { // Check if the link should open in the default browser (e.g., an external link) // You can add logic here to only open specific URLs externally // Example logic: if it's not a link to your internal website, open externally // if url.host != "www.myinternalwebsite.com" { if navigationAction.navigationType == .linkActivated && url.host != webView.url?.host { // Open the URL using the environment's openURL action parent.openURL(url) // Cancel the navigation within the web view decisionHandler(.cancel) return } } // Allow the navigation within the web view for other links decisionHandler(.allow) } func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { if message.name == "editor-update" { if parent.editingNote != nil { print("Message: \(message.body)") parent.editingNote!.markdown.body = message.body as! String } } } } } 

The problem is that when the editor changes the content and sends the messages to swift, an update occurs some seconds later (what I believe is SwiftData reading from the store?) causing the entire webview to re-render. This causes the initial events to be sent again, which isn't the end of the world, apart from that it overwrites the changes in the editor leaving the editor in this cycle of making small changes of a sentence or two before swiftdata tries to sync and overwrites those changes with the initial content leaving the user back where they started.

Any help is greatly appreciated. Like I said, I'm brand new to swift, SwiftUI and SwiftData, so if the answer is obvious, please forgive me... but this is incredibly frustrating.

5
  • Is this your own personal app? I am asking since you seems to be using old technology which is really unnecessary if,you are writing something new from scratch. Commented 16 hours ago
  • Ok, please enlighten me... like I said, I'm very new to swift. I know there's a swiftui webview if that's what you're getting at, but I was having issues with the overscroll and this is the first way I found to resolve it. Maybe not the best idea, but for somebody that couldn't write a line of swift a month ago it's a sacrifice I'm willing to make. Commented 16 hours ago
  • makeUIView must init the webview as a local and return it. Fix that first then see. Commented 16 hours ago
  • Then how can I access it outside of the makeUIView function? I know in the SwiftUI webview view you can just pass the webview as a binding, but I wasn't able to find any examples that use UIViewRepresentable and still access the webview outside of that init function? Commented 16 hours ago
  • Well if you say you must use WKWebView I can't really argue with that since I don't know that framework very well. It just looks like you are adding a lot of hurdles for someone new to the language and its frameworks. Commented 4 hours ago

1 Answer 1

0

Your WebView is stored in @State, which means SwiftUI is free to recreate it whenever the view updates. On top of that, updateUIView calls loadFileURL again, which completely reloads the page on every render pass. So any SwiftData update triggers a normal SwiftUI redraw → updateUIView runs → the file is reloaded → the WebView resets back to the initial content → the editor overwrites the user’s changes.

The correct approach is to move the WKWebView into a separate class so it becomes a stable reference type, and then pass that instance into the Representable. This prevents SwiftUI from recreating it. Also, only load the HTML inside makeUIView, not in updateUIView. And set your initial content once, after the page finishes loading, inside didFinish of the WKNavigationDelegate.

Once you do this, the WebView will stop resetting itself, and the user’s changes will no longer get overwritten.

Container for WKWebView:

final class EditorWebViewContainer { let webView: WKWebView = { let view = WKWebView(frame: .zero, configuration: getConfig()) view.scrollView.isScrollEnabled = false view.scrollView.minimumZoomScale = 1 view.scrollView.maximumZoomScale = 1 view.allowsBackForwardNavigationGestures = false return view }() } 

The rewritten UIViewRepresentable:

struct ResponsiveEditorWebView: UIViewRepresentable { let url: URL let container: EditorWebViewContainer // stable WebView @Environment(\.openURL) var openURL @Environment(\.modelContext) var modelContext @Binding var theme: WebViewTheme @Binding var editorTheme: CodeEditorTheme @Binding var editingNote: NoteModel? @Environment(\.colorScheme) var colorScheme func makeUIView(context: Context) -> WKWebView { let webView = container.webView webView.navigationDelegate = context.coordinator webView.configuration.userContentController.add(context.coordinator, name: "editor-update") // Loading the page only once webView.loadFileURL(url, allowingReadAccessTo: url) return webView } func updateUIView(_ uiView: WKWebView, context: Context) { // No reboots. No initial values. } func makeCoordinator() -> Coordinator { Coordinator(self) } } 

Initializing initial content:

extension ResponsiveEditorWebView { final class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { var parent: ResponsiveEditorWebView private var didSetInitialContent = false init(_ parent: ResponsiveEditorWebView) { self.parent = parent } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { guard !didSetInitialContent else { return } didSetInitialContent = true let body = parent.editingNote?.markdown.body .replacingOccurrences(of: "`", with: "\\`") ?? "" webView.evaluateJavaScript(""" window.localStorage.setItem("editor-initial-value", `\(body)`); """) } func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { if message.name == "editor-update", let str = message.body as? String { parent.editingNote?.markdown.body = str } } } } 

Ready-made instructions:

struct EditorScreen: View { @StateObject private var container = EditorWebViewContainer() var body: some View { ResponsiveEditorWebView( url: Bundle.main.url(forResource:"editor", withExtension:"html")!, container: container, theme: $theme, editorTheme: $editorTheme, editingNote: $editingNote ) } } 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.