Files
Fluxup_PAP/node_modules/@expo/dom-webview/ios/DomWebView.swift
2026-03-10 16:18:05 +00:00

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)")
}
}
}
}