Swift 编译器对 ObjC API 自动应用名称简化规则(Swift 3+ SE-0044), 即使是 1.x 老 SDK header 也会被简化。ObjC selector 不变,运行时 仍调 +[WXApi getApiVersion] / +[WXApi sendReq:],但 Swift 端必须 写简化后的名字: - ObjC +(NSString *)getApiVersion → Swift WXApi.getVersion() - ObjC +(BOOL)sendReq:(BaseReq*)req → Swift WXApi.send(_:) 错误信息里 Swift 编译器明确给出:"has been renamed to 'getVersion()'" + "obsoleted in Swift 3"。 注释里同步说明 selector 与 Swift 名映射关系,避免未来维护者误以为 我们调到的是 2.x SDK 的 getVersion(实际仍是 1.8.2 的 getApiVersion)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
240 lines
9.5 KiB
Swift
240 lines
9.5 KiB
Swift
//
|
||
// WeChatManager.swift
|
||
// ylgamehall
|
||
//
|
||
// 微信 SDK 持久 delegate + authorize + share 异步包装。
|
||
// 把 WXApiDelegate 的回调 → async/await,state UUID 配对多发起方。
|
||
//
|
||
// Design §8.5;msext WXApiManager.m 等价。
|
||
//
|
||
|
||
import Foundation
|
||
import UIKit
|
||
|
||
// 微信 SDK 1.8.2(与 daoqi msext 同款)通过 Bridging Header 导入,无需 import 语句
|
||
|
||
@MainActor
|
||
public final class WeChatManager: NSObject {
|
||
|
||
public static let shared = WeChatManager()
|
||
|
||
/// 授权 scope(沿用 msext SGDefineInfo.h:104)
|
||
public static let authScope = "snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact"
|
||
|
||
/// 待 resolve 的 authorize continuation(state UUID 配对)
|
||
private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:]
|
||
/// 待 resolve 的 share continuation(FIFO 串行 — 微信回包不带配对 ID)
|
||
private var sharePending: [CheckedContinuation<Void, Error>] = []
|
||
|
||
public override init() {
|
||
super.init()
|
||
}
|
||
|
||
// MARK: - Authorize
|
||
|
||
/// 拉起微信授权(msext NewRootVC.m → WXApiRequestHandler.m 同款路径)。
|
||
/// 必须用新版 SDK 2.x 的 `sendAuthReq:viewController:delegate:completion:`:
|
||
/// - viewController = SDK 拉起微信回来时的 present 上下文(缺失则 SDK 无法弹起)
|
||
/// - delegate = SDK 弱引用,授权完成由 WXApiDelegate.onResp 回调(与 SceneDelegate
|
||
/// openURL 路径解耦,授权回包独立由 SDK 直接派给 delegate)
|
||
/// 旧版 1.x 的 `WXApi.send(req)` 已 deprecated,不传 vc/delegate 会导致 H5 调了无反应。
|
||
public func authorize() async throws -> WXAuthCodePayload {
|
||
print("[WeChat] authorize() 开始")
|
||
print("[WeChat] - WXApi.isWXAppInstalled = \(WXApi.isWXAppInstalled())")
|
||
// ObjC 端 selector 是 +getApiVersion,但 Swift 编译器自动简化为 getVersion()
|
||
// (Swift 3+ ObjC name 简化规则;运行时仍调 +[WXApi getApiVersion])
|
||
print("[WeChat] - WXApi.getVersion = \(WXApi.getVersion() ?? "nil")")
|
||
let state = UUID().uuidString
|
||
return try await withCheckedThrowingContinuation { cont in
|
||
authPending[state] = cont
|
||
let req = SendAuthReq()
|
||
req.scope = Self.authScope
|
||
req.state = state
|
||
|
||
guard let vc = Self.topmostViewController() else {
|
||
print("[WeChat] ⚠️ topmostViewController() 返回 nil,无 present 上下文")
|
||
authPending.removeValue(forKey: state)
|
||
cont.resume(throwing: WeChatError.authFailed(-3))
|
||
return
|
||
}
|
||
print("[WeChat] 调 sendAuthReq vc=\(type(of: vc)) scope=\(Self.authScope) state=\(state)")
|
||
// 1.x 的 sendAuthReq 返回 BOOL(同步知道是否成功拉起),无 completion
|
||
let success = WXApi.sendAuthReq(req, viewController: vc, delegate: self)
|
||
print("[WeChat] sendAuthReq 返回 success=\(success)")
|
||
if !success {
|
||
if let pending = authPending.removeValue(forKey: state) {
|
||
pending.resume(throwing: WeChatError.authFailed(-3))
|
||
}
|
||
}
|
||
// success=true 时等 WXApiDelegate.onResp 触发(SceneDelegate openURL 链路)
|
||
}
|
||
}
|
||
|
||
/// 取当前最顶层 ViewController(含 presented / nav.visible),供微信 SDK 作为
|
||
/// modal present 的上下文。msext 直接 self(gameController/NewRootVC),新外壳
|
||
/// AccreditLoginHandler 是 stateless enum,由本方法 resolve。
|
||
private static func topmostViewController() -> UIViewController? {
|
||
let windowScene = UIApplication.shared.connectedScenes
|
||
.compactMap { $0 as? UIWindowScene }
|
||
.first(where: { $0.activationState == .foregroundActive })
|
||
?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first
|
||
guard let root = windowScene?.windows.first(where: \.isKeyWindow)?.rootViewController
|
||
?? windowScene?.windows.first?.rootViewController else {
|
||
return nil
|
||
}
|
||
var top: UIViewController = root
|
||
while let presented = top.presentedViewController {
|
||
top = presented
|
||
}
|
||
if let nav = top as? UINavigationController, let visible = nav.visibleViewController {
|
||
return visible
|
||
}
|
||
return top
|
||
}
|
||
|
||
// MARK: - Share
|
||
|
||
public enum ShareScene {
|
||
case session // 微信好友
|
||
case timeline // 朋友圈
|
||
}
|
||
|
||
public struct ShareLink: Sendable {
|
||
public let url: String
|
||
public let title: String
|
||
public let desc: String
|
||
public let thumbnailURL: String?
|
||
|
||
public init(url: String, title: String, desc: String, thumbnailURL: String?) {
|
||
self.url = url; self.title = title; self.desc = desc; self.thumbnailURL = thumbnailURL
|
||
}
|
||
}
|
||
|
||
/// 图片分享载荷(type=2 截图 / type=3 远端图)。
|
||
/// imageData 是大图原始字节(JPEG 等),thumb 是缩略图源 — SDK
|
||
/// `setThumbImage:` 内部把 thumb 压到 32KB。
|
||
public struct ShareImage {
|
||
public let imageData: Data
|
||
public let thumb: UIImage
|
||
|
||
public init(imageData: Data, thumb: UIImage) {
|
||
self.imageData = imageData
|
||
self.thumb = thumb
|
||
}
|
||
}
|
||
|
||
/// 链接分享(FIFO 串行)。微信回包不携带配对 ID,连续调时按入队顺序消费。
|
||
public func shareLink(_ link: ShareLink, scene: ShareScene) async throws {
|
||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||
sharePending.append(cont)
|
||
|
||
let media = WXWebpageObject()
|
||
media.webpageUrl = link.url
|
||
|
||
let message = WXMediaMessage()
|
||
message.title = link.title
|
||
message.description = link.desc
|
||
message.mediaObject = media
|
||
|
||
let req = SendMessageToWXReq()
|
||
req.bText = false
|
||
req.message = message
|
||
switch scene {
|
||
case .session: req.scene = Int32(WXSceneSession.rawValue)
|
||
case .timeline: req.scene = Int32(WXSceneTimeline.rawValue)
|
||
}
|
||
// 1.x: sendReq BOOL 返回;分享回包通过 SceneDelegate.openURLContexts →
|
||
// WXApi.handleOpen(url:delegate:WeChatManager.shared) → onResp 路径到达。
|
||
// ObjC selector +sendReq:,Swift 简化为 send(_:)(去 Req 后缀)。
|
||
// 运行时仍调 +[WXApi sendReq:]
|
||
WXApi.send(req)
|
||
}
|
||
}
|
||
|
||
/// 图片分享(FIFO 串行,复用 sharePending)。msext gameController.m:637-678
|
||
/// type=2 截图 / type=其它 远端图 共用同一条 WXImageObject + setThumbImage 路径。
|
||
public func shareImage(_ img: ShareImage, scene: ShareScene) async throws {
|
||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||
sharePending.append(cont)
|
||
|
||
let media = WXImageObject()
|
||
media.imageData = img.imageData
|
||
|
||
let message = WXMediaMessage()
|
||
// 图片分享不需要 title / description(msext 字面:传 nil;这里传空串等价)
|
||
message.mediaObject = media
|
||
message.setThumbImage(img.thumb)
|
||
|
||
let req = SendMessageToWXReq()
|
||
req.bText = false
|
||
req.message = message
|
||
switch scene {
|
||
case .session: req.scene = Int32(WXSceneSession.rawValue)
|
||
case .timeline: req.scene = Int32(WXSceneTimeline.rawValue)
|
||
}
|
||
// ObjC selector +sendReq:,Swift 简化为 send(_:)(去 Req 后缀)。
|
||
// 运行时仍调 +[WXApi sendReq:]
|
||
WXApi.send(req)
|
||
}
|
||
}
|
||
|
||
// MARK: - Internal: resolve callbacks
|
||
|
||
fileprivate func resolveAuth(state: String, code: String?, errCode: Int) {
|
||
guard let cont = authPending.removeValue(forKey: state) else { return }
|
||
if errCode == 0, let code {
|
||
cont.resume(returning: WXAuthCodePayload(code: code, state: state))
|
||
} else {
|
||
cont.resume(throwing: WeChatError.authFailed(errCode))
|
||
}
|
||
}
|
||
|
||
fileprivate func resolveShare(errCode: Int) {
|
||
guard !sharePending.isEmpty else { return }
|
||
let cont = sharePending.removeFirst()
|
||
if errCode == 0 {
|
||
cont.resume(returning: ())
|
||
} else {
|
||
cont.resume(throwing: WeChatError.shareFailed(errCode))
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - WXApiDelegate
|
||
|
||
extension WeChatManager: WXApiDelegate {
|
||
|
||
public nonisolated func onReq(_ req: BaseReq) {
|
||
// 我们当前只发起请求,不接受外部 req(微信 → 本 App 的 req 用于公众号回复等场景)
|
||
}
|
||
|
||
public nonisolated func onResp(_ resp: BaseResp) {
|
||
// 主线程化:BaseResp 由 SDK 在主线程派发,但保留 MainActor.run 兜底
|
||
let errCode = Int(resp.errCode)
|
||
if let auth = resp as? SendAuthResp {
|
||
let code = auth.code
|
||
let state = auth.state ?? ""
|
||
Task { @MainActor in
|
||
Self.shared.resolveAuth(state: state, code: code, errCode: errCode)
|
||
}
|
||
} else if resp is SendMessageToWXResp {
|
||
Task { @MainActor in
|
||
Self.shared.resolveShare(errCode: errCode)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Public payload / error types
|
||
|
||
public struct WXAuthCodePayload: Sendable {
|
||
public let code: String
|
||
public let state: String
|
||
}
|
||
|
||
public enum WeChatError: Error, Sendable {
|
||
case sdkNotLinked
|
||
case authFailed(Int)
|
||
case shareFailed(Int)
|
||
}
|