Files
youle_app_ios_v2/ylgamehall/Source/SDK/WeChat/WeChatManager.swift
T
joywayer 68c668a06c 4.B 截图分享补齐 msext 字段:WeChat description / QQ 引导框
【契约影响】docs/H5-Native-Contract.md §3.1[2]type==2 分支

WeChat 截图分享之前丢了 H5 description(msext 通过
sendImageData:MessageExt: 写到 WXMediaMessage.description,接收方在
微信聊天预览里能看到)。链接分享之前完全没传 thumb(msext 用 app
icon)。统一补齐:

- WeChatManager.ShareImage 加 messageExt / mediaTagName 字段;
  shareImage 内 message.description = messageExt + message.messageExt
  = messageExt + setThumbImage(thumb) + mediaTagName
- WeChatManager.ShareLink 把 thumbnailURL: String? 替换为 thumb:
  UIImage?;shareLink 内 setThumbImage(thumb) + mediaTagName。drop
  Sendable 因 UIImage 不可 Sendable(调用都在 MainActor 上无风险)
- WechatShare 双分支统一传 appIconImage() 做 thumb;截图分支 messageExt
  = content.desc + mediaTagName "进贤聚友棋牌游戏截图分享";链接分支
  mediaTagName "进贤聚友棋牌游戏分享"(字面对齐 msext WechatShareManager.m)

QQ 截图分享原本 4-fallback URL 链不靠谱(file_type=news 给截图场景实际
无意义)。简化为 Douyin 同款模式:截图 → UIPasteboard.image → 弹"立即
打开 QQ / 稍后分享"二选一 Alert。承认 ADR-006 无 QQ SDK 退化的现实,
但 UX 不再让用户摸不着北。
2026-06-24 07:53:14 +08:00

265 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// WeChatManager.swift
// ylgamehall
//
// 微信 SDK 持久 delegate + authorize + share 异步包装。
// 把 WXApiDelegate 的回调 → async/awaitstate UUID 配对多发起方。
//
// Design §8.5msext 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 continuationstate UUID 配对)
private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:]
/// 待 resolve 的 share continuationFIFO 串行 — 微信回包不带配对 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 {
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 {
authPending.removeValue(forKey: state)
cont.resume(throwing: WeChatError.authFailed(-3))
return
}
// 1.x 的 sendAuthReq 返回 BOOL(同步知道是否成功拉起),无 completion
let success = WXApi.sendAuthReq(req, viewController: vc, delegate: self)
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 直接 selfgameController/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 // 朋友圈
}
/// 链接分享载荷。`mediaTagName` 对齐 msext WXApiRequestHandler `TagName:`
/// 参数(写入 WXMediaMessage.mediaTagName,微信内部分类用)。
/// 含 UIImage 故无法 Sendable —— 实际使用都在 MainActor 上发起,无跨 actor 风险。
public struct ShareLink {
public let url: String
public let title: String
public let desc: String
public let thumb: UIImage?
public let mediaTagName: String?
public init(
url: String, title: String, desc: String,
thumb: UIImage?, mediaTagName: String? = nil
) {
self.url = url; self.title = title; self.desc = desc
self.thumb = thumb; self.mediaTagName = mediaTagName
}
}
/// 图片分享载荷(type=2 截图)。
/// - `imageData`:大图原始字节(JPEG
/// - `thumb`:缩略图源 — SDK `setThumbImage:` 内部压到 32KB
/// - `messageExt`H5 的 description;写入 `WXMediaMessage.description`
/// 与 `.messageExt`,对齐 msext `WXApiRequestHandler sendImageData:MessageExt:`
/// - `mediaTagName`:对齐 msext `TagName:` 参数
public struct ShareImage {
public let imageData: Data
public let thumb: UIImage
public let messageExt: String
public let mediaTagName: String?
public init(
imageData: Data, thumb: UIImage,
messageExt: String = "", mediaTagName: String? = nil
) {
self.imageData = imageData
self.thumb = thumb
self.messageExt = messageExt
self.mediaTagName = mediaTagName
}
}
/// 链接分享(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
if let mediaTagName = link.mediaTagName {
message.mediaTagName = mediaTagName
}
if let thumb = link.thumb {
message.setThumbImage(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)
}
// 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 WechatShareManager
/// `sendImageData:TagName:MessageExt:Action:ThumbImage:InScene:` 的字段映射:
/// TagName → mediaTagName
/// MessageExt → description(接收方预览可见)+ messageExt(SDK 内部回包)
/// ThumbImage → 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()
message.description = img.messageExt
message.mediaObject = media
message.setThumbImage(img.thumb)
if !img.messageExt.isEmpty {
message.messageExt = img.messageExt
}
if let mediaTagName = img.mediaTagName {
message.mediaTagName = mediaTagName
}
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)
}