Search code examples
keyboardwebkitswiftuiwkwebviewuikeyboard

Using WebKit in SwiftUI hides keyboard when view updates


I've created a minimum reproducible example of this problem I'm facing.

First, I created a WKWebView housed in a UIViewRepresentable to be used with SwiftUI. Then, I set up a WKUserContentController and a WKWebViewConfiguration so the WKWebView can send messages to native code. In this case, I have a <textarea> that sends over its value on input.

The value sent over through WebKit is assigned to a @State variable which causes the view to update. Unfortunately, the WKWebView is deselected whenever the view updates. How can I work around this? The WKWebView needs to stay selected until the user deliberately chooses to hide the keyboard.

This is a minimal example of what's happening:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let html: String
    let configuration: WKWebViewConfiguration

    func makeUIView(context: Context) -> WKWebView {
        .init(frame: .zero, configuration: configuration)
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.loadHTMLString(html, baseURL: nil)
    }
}

struct ContentView: View {
    final class Coordinator: NSObject, WKScriptMessageHandler {
        @Binding var text: String

        init(text: Binding<String>) {
            _text = text
        }

        func userContentController(
            _ userContentController: WKUserContentController,
            didReceive message: WKScriptMessage
        ) {
            guard let text = message.body as? String else { return }
            self.text = text
        }
    }

    @State var text = ""

    var body: some View {
        WebView(
            html: """
            <!DOCTYPE html>
            <html>
                <body>
                    <textarea>\(text)</textarea>
                    <script>
                        const textarea = document.querySelector('textarea')

                        textarea.oninput = () =>
                            webkit.messageHandlers.main.postMessage(textarea.value)
                    </script>
                </body>
            </html>
            """,
            configuration: {
                let userContentController = WKUserContentController()
                userContentController.add(Coordinator(text: $text), name: "main")

                let configuration = WKWebViewConfiguration()
                configuration.userContentController = userContentController

                return configuration
            }()
        )
    }
}

Solution

  • I would recommend (as I see the simplest in this scenario) to change handling event to

    textarea.onblur = () =>
        webkit.messageHandlers.main.postMessage(textarea.value)
    

    otherwise a complex refactoring is needed to keep away WK* entities from SwiftUI representable wrapper, or changing model to avoid using @State or anything resulting in view rebuild, or both of those.