YwxMobileApp-iOS/Shared/WebMessageHandler.swift
徐勤民 8b2796709b feat: YwxMobileApp-iOS 初始提交
三 Flavor(Shijiaobao / Clinical / Yiwangxin)iOS 项目

架构
- Shared/   全部共用层:AppHomeView、WebMessageHandler(JSBridge)、DevUrlSettings
- *Flavor/  各 flavor 入口:AppConfig、*App(@main)、AppDelegate(Yiwangxin 含 Push)
- project.yml + xcodegen 管理项目结构

SDK 集成
- 引用 Gitea SPM 包(branch: main,SNAPSHOT 开发模式)
- sdk.properties 追踪当前 SDK_REF,scripts/use-sdk.sh 支持 main/local/x.y.z 一键切换
- 含各 flavor xuqm/*.xuqmconfig 配置文件

JSBridge 协议
- window.webkit.messageHandlers.SZYX_APP.postMessage(json)
- 回调 window.SZYX_YWX_WebViewBridge.onMessage(json)
- 实现 setTitle/getDeviceInfo/login/setUserInfo/getAppVersion/checkUpdate
         setStorage/getStorage/removeStorage/clearStorage/setOrientation/apiError

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 11:29:49 +08:00

271 行
11 KiB
Swift

import Foundation
import UIKit
import XuqmCoreSDK
import XuqmWebViewSDK
import XuqmBugCollectSDK
/// H5 UserDefaults suite Update SDK
private let H5StorageSuite = "szyx_h5_storage"
/// H5 App API
private let APP_API_VERSION = 4
// MARK: - Message Dispatcher
@MainActor
func handleWebMessage(
data: String,
onStatusBarColorChanged: ((UIColor) -> Void)? = nil,
onNavBarChanged: ((String, UIColor?, UIColor?, [UIColor]?, Float, String?) -> Void)? = nil,
onCheckUpdateRequested: (() -> Void)? = nil,
onOrientationChanged: ((String) -> Void)? = nil
) {
do {
guard let jsonData = data.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { return }
let model = json["model"] as? String ?? ""
let functionId = json["functionId"] as? String ?? ""
switch model {
case "setTitle":
handleSetTitle(json: json, functionId: functionId,
onStatusBarColorChanged: onStatusBarColorChanged,
onNavBarChanged: onNavBarChanged)
case "isWechatInstalled":
// WeChat is never installed on iOS (App Store policy)
respondToWeb(functionId: functionId, model: model, status: "0",
message: "isWechatInstalled:ok",
value: ["isWechatInstalled": false])
case "wechatPay":
// WeChat Pay is not supported in this build
respondToWeb(functionId: functionId, model: model,
status: "E50001", message: "当前App不支持微信支付")
case "getDeviceInfo":
let device = UIDevice.current
let deviceInfo: [String: Any] = [
"osName": "iOS",
"osVersion": "iOS \(device.systemVersion)",
"baseVersion": device.systemVersion,
"brand": "Apple",
"manufacturer": "Apple",
"model": device.model,
"marketName": device.name,
"deviceId": device.identifierForVendor?.uuidString ?? "",
]
respondToWeb(functionId: functionId, model: model, status: "0",
message: "getDeviceInfo:ok", value: ["deviceInfo": deviceInfo])
case "login":
let userId = json["userId"] as? String ?? ""
if !userId.isEmpty {
let userSig = (json["userSig"] as? String)?.nonEmpty
let name = (json["name"] as? String)?.nonEmpty
let avatar = (json["avatar"] as? String)?.nonEmpty
XuqmSDK.shared.setUserInfo(userId: userId, nickname: name, avatar: avatar)
if let userSig {
Task { await XuqmSDK.shared.login(userId: userId, userSig: userSig) }
}
BugCollectSDK.shared.addBreadcrumb(category: "auth", message: "login userId=\(userId)")
}
respondOk(functionId: functionId, model: model, message: "login:ok")
case "setUserInfo":
let userId = ((json["userId"] as? String)?.nonEmpty ?? (json["id"] as? String)?.nonEmpty) ?? ""
if !userId.isEmpty {
let userSig = (json["userSig"] as? String)?.nonEmpty
let name = (json["name"] as? String)?.nonEmpty
let avatar = (json["avatar"] as? String)?.nonEmpty
XuqmSDK.shared.setUserInfo(userId: userId, nickname: name, avatar: avatar)
if let userSig {
Task { await XuqmSDK.shared.login(userId: userId, userSig: userSig) }
}
}
respondOk(functionId: functionId, model: model, message: "setUserInfo:ok")
case "getAppVersion":
let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
respondToWeb(functionId: functionId, model: model, status: "0",
message: "getAppVersion:ok",
value: ["version": APP_API_VERSION, "appVersion": versionName])
case "checkUpdate":
onCheckUpdateRequested?()
respondOk(functionId: functionId, model: model, message: "checkUpdate:ok")
case "setStorage":
let storage = json["storage"] as? [String: Any]
let key = (storage?["key"] as? String) ?? ""
let value = (storage?["value"] as? String) ?? ""
if !key.isEmpty {
let defaults = UserDefaults(suiteName: H5StorageSuite)
defaults?.set(value, forKey: key)
}
respondOk(functionId: functionId, model: model, message: "setStorage:ok")
case "getStorage":
let storage = json["storage"] as? [String: Any]
let key = (storage?["key"] as? String) ?? ""
let defaultValue = (storage?["defaultValue"] as? String) ?? ""
let stored: String
if !key.isEmpty, let v = UserDefaults(suiteName: H5StorageSuite)?.string(forKey: key) {
stored = v
} else {
stored = defaultValue
}
respondToWeb(functionId: functionId, model: model, status: "0",
message: "getStorage:ok",
value: ["storage": ["key": key, "value": stored]])
case "removeStorage":
let key = (json["storage"] as? [String: Any])?["key"] as? String ?? ""
if !key.isEmpty { UserDefaults(suiteName: H5StorageSuite)?.removeObject(forKey: key) }
respondOk(functionId: functionId, model: model, message: "removeStorage:ok")
case "clearStorage":
if let d = UserDefaults(suiteName: H5StorageSuite) {
d.dictionaryRepresentation().keys.forEach { d.removeObject(forKey: $0) }
}
respondOk(functionId: functionId, model: model, message: "clearStorage:ok")
case "setOrientation":
let orientation = json["orientation"] as? String ?? ""
onOrientationChanged?(orientation)
respondOk(functionId: functionId, model: model, message: "setOrientation:ok")
case "apiError":
let d = (json["data"] as? [String: Any]) ?? json
let url = d["url"] as? String ?? ""
let method = d["method"] as? String ?? ""
let status = d["status"] as? Int ?? 0
let statusText = d["statusText"] as? String ?? ""
let body = d["response"] as? String ?? ""
BugCollectSDK.shared.captureError(
ApiCallError(method: method, url: url, status: status, statusText: statusText, body: body),
tags: ["context": "webViewApi", "url": url, "method": method, "status": status]
)
default:
respondToWeb(functionId: functionId, model: model,
status: "E40001", message: "当前App版本不支持该接口")
}
} catch {
BugCollectSDK.shared.captureError(error, tags: ["context": "handleWebMessage"])
}
}
// MARK: - setTitle helpers
private func handleSetTitle(
json: [String: Any],
functionId: String,
onStatusBarColorChanged: ((UIColor) -> Void)?,
onNavBarChanged: ((String, UIColor?, UIColor?, [UIColor]?, Float, String?) -> Void)?
) {
let titleObj = json["title"] as? [String: Any]
let titleText = titleObj?["titleText"] as? String ?? ""
let textColor = parseHexColor(titleObj?["titleTextColor"] as? String)
let bgImage = (titleObj?["titleBackgroundImage"] as? String)?.nonEmpty
let gradientArr = titleObj?["titleBackgroundGradient"] as? [String]
let bgGradient: [UIColor]?
let bgColor: UIColor?
if let gradientArr, gradientArr.count >= 2 {
bgGradient = gradientArr.compactMap { parseHexColor($0) }
bgColor = bgGradient?.first
} else {
bgGradient = nil
bgColor = parseHexColor(titleObj?["titleBackgroundColor"] as? String)
}
let gradientAngle = (titleObj?["titleBackgroundGradientAngle"] as? Double).map { Float($0) } ?? 0
if let bgColor { onStatusBarColorChanged?(bgColor) }
onNavBarChanged?(titleText, textColor, bgColor, bgGradient, gradientAngle, bgImage)
respondOk(functionId: functionId, model: "setTitle", message: "setTitle:ok")
}
// MARK: - Response helpers
private func respondOk(functionId: String, model: String, message: String) {
respondToWeb(functionId: functionId, model: model, status: "0", message: message, value: [:])
}
private func respondToWeb(
functionId: String,
model: String,
status: String = "0",
message: String,
value: [String: Any] = [:]
) {
var response: [String: Any] = [
"functionId": functionId,
"model": model,
"status": status,
"message": message,
"value": value,
]
guard let data = try? JSONSerialization.data(withJSONObject: response),
let json = String(data: data, encoding: .utf8) else { return }
let js = "(function(){window.SZYX_YWX_WebViewBridge&&window.SZYX_YWX_WebViewBridge.onMessage(\(json));})()"
postJSToWebView(js)
}
// MARK: - Color helpers
func parseHexColor(_ hex: String?) -> UIColor? {
guard var h = hex?.trimmingCharacters(in: .whitespaces), !h.isEmpty else { return nil }
if h.hasPrefix("#") { h = String(h.dropFirst()) }
guard h.count == 6 || h.count == 8 else { return nil }
var value: UInt64 = 0
Scanner(string: h).scanHexInt64(&value)
if h.count == 6 {
return UIColor(
red: CGFloat((value >> 16) & 0xFF) / 255,
green: CGFloat((value >> 8) & 0xFF) / 255,
blue: CGFloat( value & 0xFF) / 255,
alpha: 1
)
} else {
return UIColor(
red: CGFloat((value >> 24) & 0xFF) / 255,
green: CGFloat((value >> 16) & 0xFF) / 255,
blue: CGFloat((value >> 8) & 0xFF) / 255,
alpha: CGFloat( value & 0xFF) / 255
)
}
}
func isLightColor(_ color: UIColor) -> Bool {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: nil)
return 0.299 * r + 0.587 * g + 0.114 * b > 0.5
}
// MARK: - JS posting
func postJSToWebView(_ js: String) {
// Forward via XWebViewBridge to the active WKWebView
Task { @MainActor in
XWebViewBridge.shared.postMessageToWeb(js)
}
}
// MARK: - Supporting types
private struct ApiCallError: Error {
let method: String
let url: String
let status: Int
let statusText: String
let body: String
var localizedDescription: String { "API \(method) \(url)\(status) \(statusText)" }
}
private extension String {
var nonEmpty: String? { isEmpty ? nil : self }
}