safely wrap for tvOS only

This commit is contained in:
2025-11-02 23:44:23 +01:00
parent 11a72074e7
commit d0c32ecfff

View File

@@ -1,512 +1,514 @@
import UIKit #if os(tvOS)
import UIKit
// MARK: - - Cookie + DataStore // MARK: - - Cookie + DataStore
public struct WKCookie { public struct WKCookie {
public let name: String public let name: String
public let value: String public let value: String
}
public class WKCookieStore {
fileprivate let cookieStorage = HTTPCookieStorage.shared
public func getAllCookies(_ callback: ([WKCookie]) -> Void) {
let wkCookies = cookieStorage.cookies?.map {
WKCookie(name: $0.name, value: $0.value)
} ?? []
callback(wkCookies)
} }
public func setCookie(_ cookie: WKCookie) { public class WKCookieStore {
if let httpCookie = HTTPCookie(properties: [ fileprivate let cookieStorage = HTTPCookieStorage.shared
.name: cookie.name,
.value: cookie.value public func getAllCookies(_ callback: ([WKCookie]) -> Void) {
]) { let wkCookies = cookieStorage.cookies?.map {
cookieStorage.setCookie(httpCookie) WKCookie(name: $0.name, value: $0.value)
} ?? []
callback(wkCookies)
} }
}
}
public struct WKDataStore { public func setCookie(_ cookie: WKCookie) {
public let httpCookieStore = WKCookieStore() if let httpCookie = HTTPCookie(properties: [
} .name: cookie.name,
.value: cookie.value
// MARK: - - Script Message Handler ]) {
cookieStorage.setCookie(httpCookie)
public struct WKScriptMessage {
public let name: String
public let body: AnyObject
}
public protocol WKScriptMessageHandler : NSObjectProtocol {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage)
}
// MARK: - - User Scripts + Messaging
public enum WKUserScriptInjectionTime {
case atDocumentStart
case atDocumentEnd
}
public struct WKUserScript {
public let source: String
public let injectionTime: WKUserScriptInjectionTime
public let forMainFrameOnly: Bool
public init(source: String, injectionTime: WKUserScriptInjectionTime = .atDocumentEnd, forMainFrameOnly: Bool = false) {
self.source = source
self.injectionTime = injectionTime
self.forMainFrameOnly = forMainFrameOnly
}
}
public class WKUserContentController {
fileprivate weak var owner: WKWebView?
fileprivate var scripts: [WKUserScript] = []
fileprivate var scriptMessageHandlers: [String : WKScriptMessageHandler] = [:]
public func addUserScript(_ script: WKUserScript) {
scripts.append(script)
}
public func removeScriptMessageHandler(forName name: String) {
scriptMessageHandlers.removeValue(forKey: name)
owner?._updateJSBridgeBindings()
}
public func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String) {
scriptMessageHandlers[name] = scriptMessageHandler
owner?._updateJSBridgeBindings()
}
}
// MARK: - - Configuration
public class WKWebViewConfiguration {
public init() {}
public var websiteDataStore = WKDataStore()
public var userContentController = WKUserContentController()
public var allowsInlineMediaPlayback: Bool = true {
didSet {
precondition(allowsInlineMediaPlayback == true,
"allowsInlineMediaPlayback can only be true")
}
}
public var suppressesIncrementalRendering: Bool = false {
didSet {
precondition(suppressesIncrementalRendering == false,
"suppressesIncrementalRendering can only be false")
}
}
public var allowsAirPlayForMediaPlayback: Bool = false {
didSet {
precondition(allowsAirPlayForMediaPlayback == false,
"allowsAirPlayForMediaPlayback can only be false")
}
}
public var allowsPictureInPictureMediaPlayback: Bool = false {
didSet {
precondition(allowsPictureInPictureMediaPlayback == false,
"allowsPictureInPictureMediaPlayback can only be false")
}
}
public var mediaTypesRequiringUserActionForPlayback: [Any] = [] {
didSet {
precondition(mediaTypesRequiringUserActionForPlayback.isEmpty,
"mediaTypesRequiringUserActionForPlayback must be empty")
}
}
}
// MARK: - - Navigation + Delegates
public struct WKNavigation {
@available(*, unavailable, message: "The effectiveContentMode property is not supported.")
public var effectiveContentMode: Any {
fatalError("This property is unavailable.")
}
}
public struct WKNavigationAction: Sendable {
public let request: URLRequest
@available(*, unavailable, message: "The navigationType property is not supported.")
public var navigationType: Any {
fatalError("This property is unavailable.")
}
}
public enum WKNavigationActionPolicy: Sendable {
case allow
case cancel
@available(*, unavailable, message: "The .download policy is not supported.")
case download
}
public protocol WKNavigationDelegate : NSObjectProtocol {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation)
func webView(_ webView: WKWebView, didFail navigation: WKNavigation, withError error: Error)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation
)
}
// MARK: - - WKWebView ( without WebKit )
public class WKWebView: UIView {
fileprivate var _webView: AnyObject?
fileprivate var _delegateProxy: _DelegateProxy?
public static var logger: DebugLogger?
public var customUserAgent: String? = nil { didSet { _applyCustomUserAgent() } }
public var navigationDelegate: WKNavigationDelegate? = nil
public var configuration: WKWebViewConfiguration
public init(frame: CGRect, configuration: WKWebViewConfiguration) {
WKWebView.logger?.log("Creating WKWebView")
self.configuration = configuration
super.init(frame: frame)
let className = getInternalClassName().reversed().joined()
if let webViewClass = NSClassFromString(className) as? UIView.Type {
self._webView = webViewClass.init(frame: frame)
WKWebView.logger?.log("WKWebView initialized")
self._applyCustomUserAgent()
self._applyWebViewPreferences()
self.configuration.userContentController.owner = self
self._updateJSBridgeBindings()
self._delegateProxy = _DelegateProxy(owner: self)
if let webView = _webView {
webView.setValue(self._delegateProxy, forKey: "delegate")
WKWebView.logger?.log("Delegate set successfully")
} }
}
}
if let webUIView = _webView as? UIView { public struct WKDataStore {
self.addSubview(webUIView) public let httpCookieStore = WKCookieStore()
self.isHidden = true }
webUIView.frame = self.bounds
webUIView.autoresizingMask = [.flexibleWidth, .flexibleHeight] // MARK: - - Script Message Handler
public struct WKScriptMessage {
public let name: String
public let body: AnyObject
}
public protocol WKScriptMessageHandler : NSObjectProtocol {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage)
}
// MARK: - - User Scripts + Messaging
public enum WKUserScriptInjectionTime {
case atDocumentStart
case atDocumentEnd
}
public struct WKUserScript {
public let source: String
public let injectionTime: WKUserScriptInjectionTime
public let forMainFrameOnly: Bool
public init(source: String, injectionTime: WKUserScriptInjectionTime = .atDocumentEnd, forMainFrameOnly: Bool = false) {
self.source = source
self.injectionTime = injectionTime
self.forMainFrameOnly = forMainFrameOnly
}
}
public class WKUserContentController {
fileprivate weak var owner: WKWebView?
fileprivate var scripts: [WKUserScript] = []
fileprivate var scriptMessageHandlers: [String : WKScriptMessageHandler] = [:]
public func addUserScript(_ script: WKUserScript) {
scripts.append(script)
}
public func removeScriptMessageHandler(forName name: String) {
scriptMessageHandlers.removeValue(forKey: name)
owner?._updateJSBridgeBindings()
}
public func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String) {
scriptMessageHandlers[name] = scriptMessageHandler
owner?._updateJSBridgeBindings()
}
}
// MARK: - - Configuration
public class WKWebViewConfiguration {
public init() {}
public var websiteDataStore = WKDataStore()
public var userContentController = WKUserContentController()
public var allowsInlineMediaPlayback: Bool = true {
didSet {
precondition(allowsInlineMediaPlayback == true,
"allowsInlineMediaPlayback can only be true")
}
}
public var suppressesIncrementalRendering: Bool = false {
didSet {
precondition(suppressesIncrementalRendering == false,
"suppressesIncrementalRendering can only be false")
}
}
public var allowsAirPlayForMediaPlayback: Bool = false {
didSet {
precondition(allowsAirPlayForMediaPlayback == false,
"allowsAirPlayForMediaPlayback can only be false")
}
}
public var allowsPictureInPictureMediaPlayback: Bool = false {
didSet {
precondition(allowsPictureInPictureMediaPlayback == false,
"allowsPictureInPictureMediaPlayback can only be false")
}
}
public var mediaTypesRequiringUserActionForPlayback: [Any] = [] {
didSet {
precondition(mediaTypesRequiringUserActionForPlayback.isEmpty,
"mediaTypesRequiringUserActionForPlayback must be empty")
}
}
}
// MARK: - - Navigation + Delegates
public struct WKNavigation {
@available(*, unavailable, message: "The effectiveContentMode property is not supported.")
public var effectiveContentMode: Any {
fatalError("This property is unavailable.")
}
}
public struct WKNavigationAction: Sendable {
public let request: URLRequest
@available(*, unavailable, message: "The navigationType property is not supported.")
public var navigationType: Any {
fatalError("This property is unavailable.")
}
}
public enum WKNavigationActionPolicy: Sendable {
case allow
case cancel
@available(*, unavailable, message: "The .download policy is not supported.")
case download
}
public protocol WKNavigationDelegate : NSObjectProtocol {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation)
func webView(_ webView: WKWebView, didFail navigation: WKNavigation, withError error: Error)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation
)
}
// MARK: - - WKWebView ( without WebKit )
public class WKWebView: UIView {
fileprivate var _webView: AnyObject?
fileprivate var _delegateProxy: _DelegateProxy?
public static var logger: DebugLogger?
public var customUserAgent: String? = nil { didSet { _applyCustomUserAgent() } }
public var navigationDelegate: WKNavigationDelegate? = nil
public var configuration: WKWebViewConfiguration
public init(frame: CGRect, configuration: WKWebViewConfiguration) {
WKWebView.logger?.log("Creating WKWebView")
self.configuration = configuration
super.init(frame: frame)
let className = getInternalClassName().reversed().joined()
if let webViewClass = NSClassFromString(className) as? UIView.Type {
self._webView = webViewClass.init(frame: frame)
WKWebView.logger?.log("WKWebView initialized")
self._applyCustomUserAgent()
self._applyWebViewPreferences()
self.configuration.userContentController.owner = self
self._updateJSBridgeBindings()
self._delegateProxy = _DelegateProxy(owner: self)
if let webView = _webView {
webView.setValue(self._delegateProxy, forKey: "delegate")
WKWebView.logger?.log("Delegate set successfully")
}
if let webUIView = _webView as? UIView {
self.addSubview(webUIView)
self.isHidden = true
webUIView.frame = self.bounds
webUIView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
} else {
WKWebView.logger?.log("⚠️ _webView is no UIView Subclass")
}
} else { } else {
WKWebView.logger?.log("⚠️ _webView is no UIView Subclass") WKWebView.logger?.logError("⚠️ UIWebView not found at runtime (tvOS)")
} }
} else {
WKWebView.logger?.logError("⚠️ UIWebView not found at runtime (tvOS)")
} }
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("⚠️ init(coder:) has not been implemented") fatalError("⚠️ init(coder:) has not been implemented")
} }
public var url: URL? { public var url: URL? {
get { get {
if let request = _webView?.perform(sel("request"))?.takeUnretainedValue() as? URLRequest { if let request = _webView?.perform(sel("request"))?.takeUnretainedValue() as? URLRequest {
return request.url return request.url
}
return nil
}
set {
guard let newURL = newValue else { return }
load(URLRequest(url: newURL))
} }
return nil
} }
set {
guard let newURL = newValue else { return } public func load(_ request: URLRequest) {
load(URLRequest(url: newURL)) WKWebView.logger?.log("Loading URL: \(request.url?.absoluteString ?? "nil")")
DispatchQueue.main.async {
_ = self._webView?.perform(sel("loadRequest:"), with: request)
}
}
public func stopLoading() {
WKWebView.logger?.log("Stopping load")
DispatchQueue.main.async {
_ = self._webView?.perform(sel("stopLoading"))
}
}
public func reload() {
WKWebView.logger?.log("Reloading")
DispatchQueue.main.async {
_ = self._webView?.perform(sel("reload"))
}
}
public func loadHTMLString(_ htmlContent: String, baseURL: String? = nil) {
let base = baseURL.flatMap { URL(string: $0) } as NSURL?
WKWebView.logger?.log("Loading HTML content with baseURL: \(baseURL ?? "nil")")
DispatchQueue.main.async {
let loadHTMLSel = sel("loadHTMLString:baseURL:")
if self._webView?.responds(to: loadHTMLSel) == true {
_ = self._webView?.perform(loadHTMLSel, with: htmlContent, with: base)
} else {
WKWebView.logger?.log("⚠️ Selector not found for loadHTMLString:")
}
}
}
public func evaluateJavaScript(_ script: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
WKWebView.logger?.log("Evaluating JS: \(script.prefix(80))")
DispatchQueue.main.async {
let result = self._webView?.perform(sel("stringByEvaluatingJavaScriptFromString:"), with: script)
completionHandler?(result?.takeUnretainedValue(), nil)
}
} }
} }
public func load(_ request: URLRequest) { // MARK: Internal Helper Functions
WKWebView.logger?.log("Loading URL: \(request.url?.absoluteString ?? "nil")")
DispatchQueue.main.async { public protocol DebugLogger {
_ = self._webView?.perform(sel("loadRequest:"), with: request) func log(_ message: String)
} func logError(_ message: String)
} }
public func stopLoading() { @inlinable
WKWebView.logger?.log("Stopping load") func sel(_ name: String) -> Selector { Selector(name) }
DispatchQueue.main.async { fileprivate extension WKWebView {
_ = self._webView?.perform(sel("stopLoading"))
@inline(never)
func getInternalClassName() -> [String] {
var parts: [String] = []
parts.append("WebView")
parts.append("UI")
return parts
} }
}
public func reload() { func _applyCustomUserAgent() {
WKWebView.logger?.log("Reloading") if let customUA = customUserAgent {
WKWebView.logger?.log("Applying custom user agent: \(customUA)")
DispatchQueue.main.async { UserDefaults.standard.register(defaults: ["UserAgent": customUA])
_ = self._webView?.perform(sel("reload")) if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
} _ = _webView?.perform(sel("setCustomUserAgent:"), with: customUA)
} }
public func loadHTMLString(_ htmlContent: String, baseURL: String? = nil) {
let base = baseURL.flatMap { URL(string: $0) } as NSURL?
WKWebView.logger?.log("Loading HTML content with baseURL: \(baseURL ?? "nil")")
DispatchQueue.main.async {
let loadHTMLSel = sel("loadHTMLString:baseURL:")
if self._webView?.responds(to: loadHTMLSel) == true {
_ = self._webView?.perform(loadHTMLSel, with: htmlContent, with: base)
} else { } else {
WKWebView.logger?.log("⚠️ Selector not found for loadHTMLString:") WKWebView.logger?.log("Clearing custom user agent")
}
}
}
public func evaluateJavaScript(_ script: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { UserDefaults.standard.removeObject(forKey: "UserAgent")
WKWebView.logger?.log("Evaluating JS: \(script.prefix(80))") if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
_ = _webView?.perform(sel("setCustomUserAgent:"), with: nil)
DispatchQueue.main.async { }
let result = self._webView?.perform(sel("stringByEvaluatingJavaScriptFromString:"), with: script)
completionHandler?(result?.takeUnretainedValue(), nil)
}
}
}
// MARK: Internal Helper Functions
public protocol DebugLogger {
func log(_ message: String)
func logError(_ message: String)
}
@inlinable
func sel(_ name: String) -> Selector { Selector(name) }
fileprivate extension WKWebView {
@inline(never)
func getInternalClassName() -> [String] {
var parts: [String] = []
parts.append("WebView")
parts.append("UI")
return parts
}
func _applyCustomUserAgent() {
if let customUA = customUserAgent {
WKWebView.logger?.log("Applying custom user agent: \(customUA)")
UserDefaults.standard.register(defaults: ["UserAgent": customUA])
if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
_ = _webView?.perform(sel("setCustomUserAgent:"), with: customUA)
}
} else {
WKWebView.logger?.log("Clearing custom user agent")
UserDefaults.standard.removeObject(forKey: "UserAgent")
if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
_ = _webView?.perform(sel("setCustomUserAgent:"), with: nil)
}
}
}
func _applyWebViewPreferences() {
let boolSettings: [(String, Bool)] = [
("setScalesPageToFit:", false),
("setAllowsLinkPreview:", false),
("setKeyboardDisplayRequiresUserAction:", true),
("setAllowsInlineMediaPlayback:", true),
("setMediaPlaybackRequiresUserAction:", false),
("setMediaPlaybackAllowsAirPlay:", false),
("setAllowsPictureInPictureMediaPlayback:", false)
]
for (selectorName, value) in boolSettings {
let selector = sel(selectorName)
if _webView?.responds(to: selector) == true {
_ = _webView?.perform(selector, with: value as NSNumber)
WKWebView.logger?.log("Applied \(selectorName.dropFirst(3))\(value)")
} else {
WKWebView.logger?.log("⚠️ Missing selector: \(selectorName)")
}
}
}
func _updateJSBridgeBindings() {
let handlerNames = configuration.userContentController.scriptMessageHandlers.keys
let bridgeJS = """
(function() {
window.webkit = window.webkit || {};
window.webkit.messageHandlers = {};
\(handlerNames.map { name in
"""
window.webkit.messageHandlers['\(name)'] = {
postMessage: function(data) {
window.location = 'jsbridge://\(name)?data=' + encodeURIComponent(JSON.stringify(data));
}
};
"""
}.joined(separator: "\n"))
})();
"""
evaluateJavaScript(bridgeJS)
WKWebView.logger?.log("Injected JSBridge handlers: \(handlerNames.joined(separator: ", "))")
}
func _injectUserScripts(at time: WKUserScriptInjectionTime) -> Int {
let scriptsToInject = configuration.userContentController.scripts.filter { $0.injectionTime == time }
for script in scriptsToInject {
evaluateJavaScript(script.source)
WKWebView.logger?.log("Injected script at \(time): \(script.source.prefix(60))")
}
return scriptsToInject.count
}
func _handleJSBridge(request: URLRequest) -> Bool {
guard let url = request.url, url.scheme == "jsbridge",
let handlerName = url.host else {
return false
}
var body: AnyObject = [:] as NSDictionary
if let query = url.query,
let dataPart = query.removingPercentEncoding?
.replacingOccurrences(of: "data=", with: "") {
if let jsonData = dataPart.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: jsonData) as AnyObject {
body = json
} else {
// Fallback: treat as raw string
body = dataPart as AnyObject
} }
} }
configuration.userContentController.scriptMessageHandlers[handlerName]? func _applyWebViewPreferences() {
.userContentController(configuration.userContentController, let boolSettings: [(String, Bool)] = [
didReceive: WKScriptMessage(name: handlerName, body: body)) ("setScalesPageToFit:", false),
("setAllowsLinkPreview:", false),
("setKeyboardDisplayRequiresUserAction:", true),
("setAllowsInlineMediaPlayback:", true),
("setMediaPlaybackRequiresUserAction:", false),
("setMediaPlaybackAllowsAirPlay:", false),
("setAllowsPictureInPictureMediaPlayback:", false)
]
WKWebView.logger?.log("JSBridge message received for '\(handlerName)'") for (selectorName, value) in boolSettings {
return true let selector = sel(selectorName)
} if _webView?.responds(to: selector) == true {
} _ = _webView?.perform(selector, with: value as NSNumber)
WKWebView.logger?.log("Applied \(selectorName.dropFirst(3))\(value)")
@objc } else {
fileprivate class _DelegateProxy: NSObject { WKWebView.logger?.log("⚠️ Missing selector: \(selectorName)")
fileprivate weak var owner: WKWebView? }
}
init(owner: WKWebView) {
self.owner = owner
super.init()
WKWebView.logger?.log("_DelegateProxy initialized")
}
override func responds(to aSelector: Selector!) -> Bool {
let selectorString = String(describing: aSelector)
let implementedSelectors = [
"webViewDidStartLoad:",
"webViewDidFinishLoad:",
"webView:didFailLoadWithError:",
"webView:shouldStartLoadWithRequest:navigationType:"
]
return implementedSelectors.contains(selectorString) || super.responds(to: aSelector)
}
@objc
func webViewDidStartLoad(_ webView: AnyObject) {
WKWebView.logger?.log("webViewDidStartLoad")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
} }
owner._updateJSBridgeBindings() func _updateJSBridgeBindings() {
let count = owner._injectUserScripts(at: .atDocumentStart) let handlerNames = configuration.userContentController.scriptMessageHandlers.keys
WKWebView.logger?.log("Injected \(count) user scripts at document start") let bridgeJS = """
(function() {
window.webkit = window.webkit || {};
window.webkit.messageHandlers = {};
\(handlerNames.map { name in
"""
window.webkit.messageHandlers['\(name)'] = {
postMessage: function(data) {
window.location = 'jsbridge://\(name)?data=' + encodeURIComponent(JSON.stringify(data));
}
};
"""
}.joined(separator: "\n"))
})();
"""
owner.navigationDelegate?.webView(owner, didStartProvisionalNavigation: WKNavigation()) evaluateJavaScript(bridgeJS)
} WKWebView.logger?.log("Injected JSBridge handlers: \(handlerNames.joined(separator: ", "))")
@objc
func webViewDidFinishLoad(_ webView: AnyObject) {
WKWebView.logger?.log("webViewDidFinishLoad")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
} }
owner._updateJSBridgeBindings() func _injectUserScripts(at time: WKUserScriptInjectionTime) -> Int {
let count = owner._injectUserScripts(at: .atDocumentEnd) let scriptsToInject = configuration.userContentController.scripts.filter { $0.injectionTime == time }
WKWebView.logger?.log("Injected \(count) user scripts at document end")
owner.navigationDelegate?.webView(owner, didFinish: WKNavigation()) for script in scriptsToInject {
} evaluateJavaScript(script.source)
WKWebView.logger?.log("Injected script at \(time): \(script.source.prefix(60))")
}
@objc return scriptsToInject.count
func webView(_ webView: AnyObject, didFailLoadWithError error: NSError) {
if let failingURL = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL,
failingURL.scheme == "jsbridge" {
// Suppress fake jsbridge:// URL errors
return
} }
WKWebView.logger?.logError("Error: \(error)") func _handleJSBridge(request: URLRequest) -> Bool {
guard let owner else { guard let url = request.url, url.scheme == "jsbridge",
WKWebView.logger?.log("owner has been deallocated, canceling callback") let handlerName = url.host else {
return return false
} }
owner.navigationDelegate?.webView(owner, didFail: WKNavigation(), withError: error) var body: AnyObject = [:] as NSDictionary
} if let query = url.query,
let dataPart = query.removingPercentEncoding?
.replacingOccurrences(of: "data=", with: "") {
if let jsonData = dataPart.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: jsonData) as AnyObject {
body = json
} else {
// Fallback: treat as raw string
body = dataPart as AnyObject
}
}
@objc(webView:shouldStartLoadWithRequest:navigationType:) configuration.userContentController.scriptMessageHandlers[handlerName]?
func webView_shouldStartLoadWithRequest(_ webView: AnyObject, shouldStartLoadWithRequest request: URLRequest, navigationType: Int) -> Bool { .userContentController(configuration.userContentController,
return handleShouldStartLoad(request: request, navigationType: navigationType) didReceive: WKScriptMessage(name: handlerName, body: body))
}
@objc WKWebView.logger?.log("JSBridge message received for '\(handlerName)'")
func webView(_ webView: AnyObject, shouldStartLoadWith request: URLRequest, navigationType: Int) -> Bool {
return handleShouldStartLoad(request: request, navigationType: navigationType)
}
private func handleShouldStartLoad(request: URLRequest, navigationType: Int) -> Bool {
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return true return true
} }
if owner._handleJSBridge(request: request) { return false }
WKWebView.logger?.log("Navigation request: \(request.url?.absoluteString ?? "nil") (Type: \(navigationType))")
final class DecisionBox: @unchecked Sendable {
var value: WKNavigationActionPolicy = .allow
}
let box = DecisionBox()
let semaphore = DispatchSemaphore(value: 0)
let action = WKNavigationAction(request: request)
let handler: @Sendable (WKNavigationActionPolicy) -> Void = { policy in
box.value = policy
semaphore.signal()
}
if Thread.isMainThread {
owner.navigationDelegate?.webView(owner, decidePolicyFor: action, decisionHandler: handler)
} else {
DispatchQueue.main.async {
owner.navigationDelegate?.webView(owner, decidePolicyFor: action, decisionHandler: handler)
}
}
_ = semaphore.wait(timeout: .now() + 5)
let decision = box.value
WKWebView.logger?.log("Decision: \(decision == .allow ? "ALLOW" : "CANCEL")")
return decision == .allow
} }
}
@objc
fileprivate class _DelegateProxy: NSObject {
fileprivate weak var owner: WKWebView?
init(owner: WKWebView) {
self.owner = owner
super.init()
WKWebView.logger?.log("_DelegateProxy initialized")
}
override func responds(to aSelector: Selector!) -> Bool {
let selectorString = String(describing: aSelector)
let implementedSelectors = [
"webViewDidStartLoad:",
"webViewDidFinishLoad:",
"webView:didFailLoadWithError:",
"webView:shouldStartLoadWithRequest:navigationType:"
]
return implementedSelectors.contains(selectorString) || super.responds(to: aSelector)
}
@objc
func webViewDidStartLoad(_ webView: AnyObject) {
WKWebView.logger?.log("webViewDidStartLoad")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
}
owner._updateJSBridgeBindings()
let count = owner._injectUserScripts(at: .atDocumentStart)
WKWebView.logger?.log("Injected \(count) user scripts at document start")
owner.navigationDelegate?.webView(owner, didStartProvisionalNavigation: WKNavigation())
}
@objc
func webViewDidFinishLoad(_ webView: AnyObject) {
WKWebView.logger?.log("webViewDidFinishLoad")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
}
owner._updateJSBridgeBindings()
let count = owner._injectUserScripts(at: .atDocumentEnd)
WKWebView.logger?.log("Injected \(count) user scripts at document end")
owner.navigationDelegate?.webView(owner, didFinish: WKNavigation())
}
@objc
func webView(_ webView: AnyObject, didFailLoadWithError error: NSError) {
if let failingURL = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL,
failingURL.scheme == "jsbridge" {
// Suppress fake jsbridge:// URL errors
return
}
WKWebView.logger?.logError("Error: \(error)")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
}
owner.navigationDelegate?.webView(owner, didFail: WKNavigation(), withError: error)
}
@objc(webView:shouldStartLoadWithRequest:navigationType:)
func webView_shouldStartLoadWithRequest(_ webView: AnyObject, shouldStartLoadWithRequest request: URLRequest, navigationType: Int) -> Bool {
return handleShouldStartLoad(request: request, navigationType: navigationType)
}
@objc
func webView(_ webView: AnyObject, shouldStartLoadWith request: URLRequest, navigationType: Int) -> Bool {
return handleShouldStartLoad(request: request, navigationType: navigationType)
}
private func handleShouldStartLoad(request: URLRequest, navigationType: Int) -> Bool {
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return true
}
if owner._handleJSBridge(request: request) { return false }
WKWebView.logger?.log("Navigation request: \(request.url?.absoluteString ?? "nil") (Type: \(navigationType))")
final class DecisionBox: @unchecked Sendable {
var value: WKNavigationActionPolicy = .allow
}
let box = DecisionBox()
let semaphore = DispatchSemaphore(value: 0)
let action = WKNavigationAction(request: request)
let handler: @Sendable (WKNavigationActionPolicy) -> Void = { policy in
box.value = policy
semaphore.signal()
}
if Thread.isMainThread {
owner.navigationDelegate?.webView(owner, decidePolicyFor: action, decisionHandler: handler)
} else {
DispatchQueue.main.async {
owner.navigationDelegate?.webView(owner, decidePolicyFor: action, decisionHandler: handler)
}
}
_ = semaphore.wait(timeout: .now() + 5)
let decision = box.value
WKWebView.logger?.log("Decision: \(decision == .allow ? "ALLOW" : "CANCEL")")
return decision == .allow
}
}
#endif