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 let name: 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 struct WKCookie {
public let name: String
public let value: String
}
public func setCookie(_ cookie: WKCookie) {
if let httpCookie = HTTPCookie(properties: [
.name: cookie.name,
.value: cookie.value
]) {
cookieStorage.setCookie(httpCookie)
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 struct WKDataStore {
public let httpCookieStore = WKCookieStore()
}
// 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")
public func setCookie(_ cookie: WKCookie) {
if let httpCookie = HTTPCookie(properties: [
.name: cookie.name,
.value: cookie.value
]) {
cookieStorage.setCookie(httpCookie)
}
}
}
if let webUIView = _webView as? UIView {
self.addSubview(webUIView)
self.isHidden = true
webUIView.frame = self.bounds
webUIView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
public struct WKDataStore {
public let httpCookieStore = WKCookieStore()
}
// 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 {
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) {
fatalError("⚠️ init(coder:) has not been implemented")
}
required init?(coder: NSCoder) {
fatalError("⚠️ init(coder:) has not been implemented")
}
public var url: URL? {
get {
if let request = _webView?.perform(sel("request"))?.takeUnretainedValue() as? URLRequest {
return request.url
public var url: URL? {
get {
if let request = _webView?.perform(sel("request"))?.takeUnretainedValue() as? URLRequest {
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 }
load(URLRequest(url: newURL))
public func load(_ request: URLRequest) {
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) {
WKWebView.logger?.log("Loading URL: \(request.url?.absoluteString ?? "nil")")
// MARK: Internal Helper Functions
DispatchQueue.main.async {
_ = self._webView?.perform(sel("loadRequest:"), with: request)
}
public protocol DebugLogger {
func log(_ message: String)
func logError(_ message: String)
}
public func stopLoading() {
WKWebView.logger?.log("Stopping load")
@inlinable
func sel(_ name: String) -> Selector { Selector(name) }
DispatchQueue.main.async {
_ = self._webView?.perform(sel("stopLoading"))
fileprivate extension WKWebView {
@inline(never)
func getInternalClassName() -> [String] {
var parts: [String] = []
parts.append("WebView")
parts.append("UI")
return parts
}
}
public func reload() {
WKWebView.logger?.log("Reloading")
func _applyCustomUserAgent() {
if let customUA = customUserAgent {
WKWebView.logger?.log("Applying custom user agent: \(customUA)")
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)
UserDefaults.standard.register(defaults: ["UserAgent": customUA])
if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
_ = _webView?.perform(sel("setCustomUserAgent:"), with: customUA)
}
} 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) {
WKWebView.logger?.log("Evaluating JS: \(script.prefix(80))")
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
UserDefaults.standard.removeObject(forKey: "UserAgent")
if _webView?.responds(to: sel("setCustomUserAgent:")) == true {
_ = _webView?.perform(sel("setCustomUserAgent:"), with: nil)
}
}
}
configuration.userContentController.scriptMessageHandlers[handlerName]?
.userContentController(configuration.userContentController,
didReceive: WKScriptMessage(name: handlerName, body: body))
func _applyWebViewPreferences() {
let boolSettings: [(String, Bool)] = [
("setScalesPageToFit:", false),
("setAllowsLinkPreview:", false),
("setKeyboardDisplayRequiresUserAction:", true),
("setAllowsInlineMediaPlayback:", true),
("setMediaPlaybackRequiresUserAction:", false),
("setMediaPlaybackAllowsAirPlay:", false),
("setAllowsPictureInPictureMediaPlayback:", false)
]
WKWebView.logger?.log("JSBridge message received for '\(handlerName)'")
return true
}
}
@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
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)")
}
}
}
owner._updateJSBridgeBindings()
let count = owner._injectUserScripts(at: .atDocumentStart)
WKWebView.logger?.log("Injected \(count) user scripts at document start")
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"))
})();
"""
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
evaluateJavaScript(bridgeJS)
WKWebView.logger?.log("Injected JSBridge handlers: \(handlerNames.joined(separator: ", "))")
}
owner._updateJSBridgeBindings()
let count = owner._injectUserScripts(at: .atDocumentEnd)
WKWebView.logger?.log("Injected \(count) user scripts at document end")
func _injectUserScripts(at time: WKUserScriptInjectionTime) -> Int {
let scriptsToInject = configuration.userContentController.scripts.filter { $0.injectionTime == time }
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
func webView(_ webView: AnyObject, didFailLoadWithError error: NSError) {
if let failingURL = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL,
failingURL.scheme == "jsbridge" {
// Suppress fake jsbridge:// URL errors
return
return scriptsToInject.count
}
WKWebView.logger?.logError("Error: \(error)")
guard let owner else {
WKWebView.logger?.log("owner has been deallocated, canceling callback")
return
}
func _handleJSBridge(request: URLRequest) -> Bool {
guard let url = request.url, url.scheme == "jsbridge",
let handlerName = url.host else {
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:)
func webView_shouldStartLoadWithRequest(_ webView: AnyObject, shouldStartLoadWithRequest request: URLRequest, navigationType: Int) -> Bool {
return handleShouldStartLoad(request: request, navigationType: navigationType)
}
configuration.userContentController.scriptMessageHandlers[handlerName]?
.userContentController(configuration.userContentController,
didReceive: WKScriptMessage(name: handlerName, body: body))
@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")
WKWebView.logger?.log("JSBridge message received for '\(handlerName)'")
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