按 msext gameController.m:637-678 字面行为,WechatShare 按 content.type 分发: - type=1 → shareLink(已有) - type=2 → captureScreenshot → JPEG 0.6 + thumb 0.4x → shareImage - type=其它 → downloadRemote(URL) → JPEG 0.9 + thumb 0.4x → shareImage 新增: - ImageProvider.scaledThumb(_:scale:):等价 msext FuncPublic imageByScalingAndCroppingForSize 0.4x 缩放;微信 SDK setThumbImage: 内部再压到 32KB - WeChatManager.ShareImage 结构 + shareImage(_:scene:) 异步包装 (WXImageObject + WXMediaMessage.setThumbImage,FIFO 串行复用 sharePending) 契约:与 type=1 一致,调成功 → sharesuccess 反向 callback(success/cancelled 都发);errCode=-2 → cancelled;其它 errCode → failed。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
203 lines
6.6 KiB
Swift
203 lines
6.6 KiB
Swift
//
|
||
// WeChatManager.swift
|
||
// ylgamehall
|
||
//
|
||
// 微信 SDK 持久 delegate + authorize + share 异步包装。
|
||
// 把 WXApiDelegate 的回调 → async/await,state UUID 配对多发起方。
|
||
//
|
||
// Design §8.5;msext WXApiManager.m 等价。
|
||
//
|
||
|
||
import Foundation
|
||
import UIKit
|
||
|
||
#if canImport(WechatOpenSDK)
|
||
import WechatOpenSDK
|
||
#endif
|
||
|
||
@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 行为:state = "wechat_sdk" + scope = 4 项)
|
||
/// 返回的 code 可用 WeChatAuth.exchangeForUser 拿 7 字段用户信息。
|
||
public func authorize() async throws -> WXAuthCodePayload {
|
||
#if canImport(WechatOpenSDK)
|
||
let state = UUID().uuidString
|
||
return try await withCheckedThrowingContinuation { cont in
|
||
authPending[state] = cont
|
||
let req = SendAuthReq()
|
||
req.scope = Self.authScope
|
||
req.state = state
|
||
WXApi.send(req)
|
||
}
|
||
#else
|
||
throw WeChatError.sdkNotLinked
|
||
#endif
|
||
}
|
||
|
||
// 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 {
|
||
#if canImport(WechatOpenSDK)
|
||
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)
|
||
}
|
||
WXApi.send(req)
|
||
}
|
||
#else
|
||
throw WeChatError.sdkNotLinked
|
||
#endif
|
||
}
|
||
|
||
/// 图片分享(FIFO 串行,复用 sharePending)。msext gameController.m:637-678
|
||
/// type=2 截图 / type=其它 远端图 共用同一条 WXImageObject + setThumbImage 路径。
|
||
public func shareImage(_ img: ShareImage, scene: ShareScene) async throws {
|
||
#if canImport(WechatOpenSDK)
|
||
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)
|
||
}
|
||
WXApi.send(req)
|
||
}
|
||
#else
|
||
throw WeChatError.sdkNotLinked
|
||
#endif
|
||
}
|
||
|
||
// 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
|
||
|
||
#if canImport(WechatOpenSDK)
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// MARK: - Public payload / error types(无 SDK 依赖,可在 canImport 之外使用)
|
||
|
||
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)
|
||
}
|