feat: iOS XWebView 增强 - 添加 StandardHandlers
新增功能: - XWebViewStandardHandlers 类 - xuqm.getDeviceInfo - 获取设备信息 - xuqm.getToken - 获取 Token - xuqm.getUserInfo - 获取用户信息 - xuqm.closeWebView - 关闭 WebView - 更新 XWebViewView.swift 集成 StandardHandlers - 自动注入 StandardHandlers JS 代码 Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
父节点
71c4a9f2d1
当前提交
07fcd64b1a
@ -0,0 +1,176 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import XuqmCoreSDK
|
||||||
|
|
||||||
|
/// JSBridge 标准处理器
|
||||||
|
/// 提供 xuqm.* 命名空间的标准桥接方法
|
||||||
|
public final class XWebViewStandardHandlers {
|
||||||
|
|
||||||
|
private weak var viewController: UIViewController?
|
||||||
|
private var onClose: (() -> Void)?
|
||||||
|
|
||||||
|
public init(viewController: UIViewController? = nil, onClose: (() -> Void)? = nil) {
|
||||||
|
self.viewController = viewController
|
||||||
|
self.onClose = onClose
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 JS 消息
|
||||||
|
/// - Returns: 是否已处理
|
||||||
|
public func handleMessage(_ message: [String: Any]) -> Bool {
|
||||||
|
guard let action = message["action"] as? String else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let callbackId = message["callbackId"] as? String
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "xuqm.getDeviceInfo":
|
||||||
|
handleGetDeviceInfo(callbackId: callbackId)
|
||||||
|
return true
|
||||||
|
case "xuqm.getToken":
|
||||||
|
handleGetToken(callbackId: callbackId)
|
||||||
|
return true
|
||||||
|
case "xuqm.getUserInfo":
|
||||||
|
handleGetUserInfo(callbackId: callbackId)
|
||||||
|
return true
|
||||||
|
case "xuqm.closeWebView":
|
||||||
|
handleCloseWebView()
|
||||||
|
return true
|
||||||
|
case "xuqm.showToast":
|
||||||
|
handleShowToast(message: message)
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Handlers
|
||||||
|
|
||||||
|
private func handleGetDeviceInfo(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
|
||||||
|
let device = UIDevice.current
|
||||||
|
let model = device.model
|
||||||
|
let osVersion = "iOS \(device.systemVersion)"
|
||||||
|
let platform = "IOS"
|
||||||
|
let deviceId = device.identifierForVendor?.uuidString ?? ""
|
||||||
|
|
||||||
|
callCallback(callbackId, data: [
|
||||||
|
"model": model,
|
||||||
|
"osVersion": osVersion,
|
||||||
|
"platform": platform,
|
||||||
|
"deviceId": deviceId,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGetToken(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
let token = XuqmSDK.shared.token ?? ""
|
||||||
|
callCallback(callbackId, data: ["token": token])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGetUserInfo(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
let userId = XuqmSDK.shared.currentUserId ?? ""
|
||||||
|
callCallback(callbackId, data: ["userId": userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleCloseWebView() {
|
||||||
|
onClose?()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleShowToast(message: [String: Any]) {
|
||||||
|
guard let msg = message["message"] as? String else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let vc = self?.viewController else { return }
|
||||||
|
let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
|
||||||
|
vc.present(alert, animated: true)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
alert.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Callback
|
||||||
|
|
||||||
|
private func callCallback(_ callbackId: String, data: [String: Any]) {
|
||||||
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let js = "window[\"\(callbackId)\"](\(jsonString))"
|
||||||
|
// The WebView will execute this JS through its controller
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .xWebViewCallJs,
|
||||||
|
object: nil,
|
||||||
|
userInfo: ["js": js]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成注入的 JS 代码
|
||||||
|
public static var injectedJavaScript: String {
|
||||||
|
return """
|
||||||
|
(function() {
|
||||||
|
window.xuqm = window.xuqm || {};
|
||||||
|
|
||||||
|
window.xuqm.getDeviceInfo = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getDeviceInfo',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.getToken = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data.token);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getToken',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.getUserInfo = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getUserInfo',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.closeWebView = function() {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.closeWebView'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.showToast = function(message) {
|
||||||
|
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.showToast',
|
||||||
|
message: message
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
true;
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification Name
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let xWebViewCallJs = Notification.Name("xWebViewCallJs")
|
||||||
|
}
|
||||||
@ -77,7 +77,7 @@ true;
|
|||||||
"""#
|
"""#
|
||||||
|
|
||||||
private func buildInjectedJs(_ config: XWebViewConfig) -> String {
|
private func buildInjectedJs(_ config: XWebViewConfig) -> String {
|
||||||
dialogOverrideJs + "\n" + (config.injectedJavaScript ?? "")
|
dialogOverrideJs + "\n" + XWebViewStandardHandlers.injectedJavaScript + "\n" + (config.injectedJavaScript ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
@ -187,6 +187,13 @@ public struct XWebViewView: UIViewRepresentable {
|
|||||||
config.onMessage?(raw)
|
config.onMessage?(raw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle StandardHandlers (xuqm.* actions)
|
||||||
|
if let action = json["action"] as? String, action.hasPrefix("xuqm.") {
|
||||||
|
handleStandardAction(action, message: json)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard let xwv = json["__xwv"] as? String else {
|
guard let xwv = json["__xwv"] as? String else {
|
||||||
config.onMessage?(raw)
|
config.onMessage?(raw)
|
||||||
return
|
return
|
||||||
@ -208,6 +215,60 @@ public struct XWebViewView: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理 StandardHandlers 动作
|
||||||
|
private func handleStandardAction(_ action: String, message: [String: Any]) {
|
||||||
|
let callbackId = message["callbackId"] as? String
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "xuqm.getDeviceInfo":
|
||||||
|
handleGetDeviceInfo(callbackId: callbackId)
|
||||||
|
case "xuqm.getToken":
|
||||||
|
handleGetToken(callbackId: callbackId)
|
||||||
|
case "xuqm.getUserInfo":
|
||||||
|
handleGetUserInfo(callbackId: callbackId)
|
||||||
|
case "xuqm.closeWebView":
|
||||||
|
config.onClose?()
|
||||||
|
case "xuqm.showToast":
|
||||||
|
// iOS 不支持原生 Toast,可以使用 alert
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGetDeviceInfo(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
let device = UIDevice.current
|
||||||
|
let data: [String: Any] = [
|
||||||
|
"model": device.model,
|
||||||
|
"osVersion": "iOS \(device.systemVersion)",
|
||||||
|
"platform": "IOS",
|
||||||
|
"deviceId": device.identifierForVendor?.uuidString ?? "",
|
||||||
|
]
|
||||||
|
callCallback(callbackId, data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGetToken(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
let token = XuqmSDK.shared.token ?? ""
|
||||||
|
callCallback(callbackId, data: ["token": token])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGetUserInfo(callbackId: String?) {
|
||||||
|
guard let callbackId = callbackId else { return }
|
||||||
|
let userId = XuqmSDK.shared.currentUserId ?? ""
|
||||||
|
callCallback(callbackId, data: ["userId": userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func callCallback(_ callbackId: String, data: [String: Any]) {
|
||||||
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let js = "window[\"\(callbackId)\"](\(jsonString))"
|
||||||
|
webView?.evaluateJavaScript(js, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
private func dispatchDownloadEvent(_ name: String, url: String, extra: String = "") {
|
private func dispatchDownloadEvent(_ name: String, url: String, extra: String = "") {
|
||||||
let escaped = url.jsEscaped
|
let escaped = url.jsEscaped
|
||||||
let js = "window.dispatchEvent(new CustomEvent('\(name)',{detail:{url:'\(escaped)'\(extra)}}));"
|
let js = "window.dispatchEvent(new CustomEvent('\(name)',{detail:{url:'\(escaped)'\(extra)}}));"
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户