226 lines
7.1 KiB
Swift
226 lines
7.1 KiB
Swift
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
|
|
import ExpoModulesCore
|
|
import WebKit
|
|
|
|
internal final class DomWebView: ExpoView, UIScrollViewDelegate, WKUIDelegate, WKScriptMessageHandler {
|
|
// swiftlint:disable implicitly_unwrapped_optional
|
|
private(set) var webView: WKWebView!
|
|
private(set) var id: WebViewId!
|
|
// swiftlint:enable implicitly_unwrapped_optional
|
|
|
|
private var source: DomWebViewSource?
|
|
private var injectedJSBeforeContentLoaded: WKUserScript?
|
|
var webviewDebuggingEnabled = false
|
|
var decelerationRate: UIScrollView.DecelerationRate = .normal
|
|
|
|
internal typealias SyncCompletionHandler = (String?) -> Void
|
|
|
|
private var needsResetupScripts = false
|
|
|
|
private static let EVAL_PROMPT_HEADER = "__EXPO_DOM_WEBVIEW_JS_EVAL__"
|
|
private static let POST_MESSAGE_HANDLER_NAME = "ReactNativeWebView"
|
|
|
|
private let onMessage = EventDispatcher()
|
|
|
|
required init(appContext: AppContext? = nil) {
|
|
super.init(appContext: appContext)
|
|
super.backgroundColor = .clear
|
|
self.id = DomWebViewRegistry.shared.add(webView: self)
|
|
webView = createWebView()
|
|
resetupScripts()
|
|
addSubview(webView)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
webView.frame = bounds
|
|
}
|
|
|
|
override var backgroundColor: UIColor? {
|
|
get { webView.backgroundColor }
|
|
set {
|
|
let isOpaque = (newValue ?? UIColor.clear).cgColor.alpha == 1.0
|
|
self.isOpaque = isOpaque
|
|
webView.isOpaque = isOpaque
|
|
webView.scrollView.backgroundColor = newValue
|
|
webView.backgroundColor = newValue
|
|
}
|
|
}
|
|
|
|
override func removeFromSuperview() {
|
|
webView.removeFromSuperview()
|
|
webView = nil
|
|
DomWebViewRegistry.shared.remove(webViewId: self.id)
|
|
super.removeFromSuperview()
|
|
}
|
|
|
|
// MARK: - Public methods
|
|
|
|
func reload() {
|
|
if #available(iOS 16.4, *) {
|
|
webView.isInspectable = webviewDebuggingEnabled
|
|
}
|
|
|
|
if needsResetupScripts {
|
|
resetupScripts()
|
|
needsResetupScripts = false
|
|
}
|
|
|
|
if let source,
|
|
let request = RCTConvert.nsurlRequest(source.toDictionary(appContext: appContext)),
|
|
webView.url?.absoluteURL != request.url {
|
|
webView.load(request)
|
|
}
|
|
}
|
|
|
|
func scrollTo(offset: CGPoint, animated: Bool) {
|
|
webView.scrollView.setContentOffset(offset, animated: animated)
|
|
}
|
|
|
|
func injectJavaScript(_ script: String) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.webView.evaluateJavaScript(script)
|
|
}
|
|
}
|
|
|
|
func setSource(_ source: DomWebViewSource) {
|
|
self.source = source
|
|
}
|
|
|
|
func setInjectedJSBeforeContentLoaded(_ script: String?) {
|
|
if let script, !script.isEmpty {
|
|
injectedJSBeforeContentLoaded = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
|
} else {
|
|
injectedJSBeforeContentLoaded = nil
|
|
}
|
|
needsResetupScripts = true
|
|
}
|
|
|
|
// MARK: - UIScrollViewDelegate implementations
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
scrollView.decelerationRate = decelerationRate
|
|
}
|
|
|
|
// MARK: - WKUIDelegate implementations
|
|
|
|
func webView(
|
|
_ webView: WKWebView,
|
|
runJavaScriptTextInputPanelWithPrompt prompt: String,
|
|
defaultText: String?,
|
|
initiatedByFrame frame: WKFrameInfo,
|
|
completionHandler: @escaping SyncCompletionHandler
|
|
) {
|
|
if !prompt.hasPrefix(Self.EVAL_PROMPT_HEADER) {
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
let script = String(prompt.dropFirst(Self.EVAL_PROMPT_HEADER.count))
|
|
if let data = script.data(using: .utf8),
|
|
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
|
let deferredId = json["deferredId"] as? Int,
|
|
let source = json["source"] as? String {
|
|
nativeJsiEvalSync(deferredId: deferredId, source: source, completionHandler: completionHandler)
|
|
} else {
|
|
completionHandler("Invalid parameters for nativeJsiEvalSync")
|
|
}
|
|
}
|
|
|
|
// MARK: - WKScriptMessageHandler implementations
|
|
|
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
if message.name == Self.POST_MESSAGE_HANDLER_NAME {
|
|
var payload = createBaseEventPayload()
|
|
payload["data"] = message.body
|
|
onMessage(payload)
|
|
return
|
|
}
|
|
}
|
|
|
|
// MARK: - Internals
|
|
|
|
private func createWebView() -> WKWebView {
|
|
let config = WKWebViewConfiguration()
|
|
config.userContentController = WKUserContentController()
|
|
let webView = WKWebView(frame: .zero, configuration: config)
|
|
webView.uiDelegate = self
|
|
webView.backgroundColor = .clear
|
|
webView.scrollView.delegate = self
|
|
return webView
|
|
}
|
|
|
|
private func createBaseEventPayload() -> [String: Any] {
|
|
return [
|
|
"url": webView.url?.absoluteString ?? "",
|
|
"title": webView.title ?? ""
|
|
]
|
|
}
|
|
|
|
private func resetupScripts() {
|
|
let userContentController = webView.configuration.userContentController
|
|
userContentController.removeAllUserScripts()
|
|
userContentController.removeAllScriptMessageHandlers()
|
|
|
|
userContentController.add(self, name: Self.POST_MESSAGE_HANDLER_NAME)
|
|
|
|
if let injectedJSBeforeContentLoaded {
|
|
userContentController.addUserScript(injectedJSBeforeContentLoaded)
|
|
}
|
|
|
|
let addDomWebViewBridgeScript = """
|
|
window.ExpoDomWebViewBridge = {
|
|
eval: function eval(params) {
|
|
return window.prompt('\(Self.EVAL_PROMPT_HEADER)' + params);
|
|
},
|
|
};
|
|
true;
|
|
"""
|
|
userContentController.addUserScript(WKUserScript(source: addDomWebViewBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
|
|
|
|
let addRNWObjectScript = """
|
|
window.ReactNativeWebView ||= {};
|
|
window.ReactNativeWebView.postMessage = function postMessage(data) {
|
|
window.webkit.messageHandlers.\(Self.POST_MESSAGE_HANDLER_NAME).postMessage(String(data));
|
|
};
|
|
true;
|
|
"""
|
|
userContentController.addUserScript(WKUserScript(source: addRNWObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
|
|
|
|
guard let webViewId = self.id else {
|
|
return
|
|
}
|
|
|
|
let addExpoDomWebViewObjectScript = "\(INSTALL_GLOBALS_SCRIPT);true;"
|
|
.replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
|
|
userContentController.addUserScript(WKUserScript(source: addExpoDomWebViewObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
|
|
}
|
|
|
|
private func nativeJsiEvalSync(deferredId: Int, source: String, completionHandler: @escaping SyncCompletionHandler) {
|
|
guard let appContext else {
|
|
completionHandler("Missing AppContext")
|
|
return
|
|
}
|
|
guard let webViewId = self.id else {
|
|
completionHandler("Missing webViewId")
|
|
return
|
|
}
|
|
guard let runtime = try? appContext.runtime else {
|
|
completionHandler("Missing JS Runtime")
|
|
return
|
|
}
|
|
try? appContext.runtime.schedule {
|
|
let wrappedSource = NATIVE_EVAL_WRAPPER_SCRIPT
|
|
.replacingOccurrences(of: "\"%%DEFERRED_ID%%\"", with: String(deferredId))
|
|
.replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
|
|
.replacingOccurrences(of: "\"%%SOURCE%%\"", with: source)
|
|
do {
|
|
let result = try runtime.eval(wrappedSource)
|
|
completionHandler(result.getString())
|
|
} catch {
|
|
completionHandler("\(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|