微信分享去sdk

This commit is contained in:
joywayer
2026-06-30 17:41:10 +08:00
parent c1e51d2183
commit 113b4d4960
7 changed files with 227 additions and 171 deletions
+170
View File
@@ -0,0 +1,170 @@
# ylgamehall
进贤聚友棋牌 iOS 新外壳(greenfield 重写)。原生外壳 + 内置 H5(`gamehall.zip` 解压到沙盒)+ WebViewJavascriptBridge 桥接,企业签 / 超级签 / TF 渠道分发,**不上架 App Store**。
> 同仓库 `../daoqi/` 是已上线多年的旧外壳 `msext`Objective-C / MRC / iOS 9.0+),作为本项目的"工作参照实现"。本项目按现代 iOS 标准重写,但 **H5 端零修改** 是不可妥协的第一准则。
---
## 技术栈
| 项 | 值 |
|---|---|
| 语言 | Swift 6.0`SWIFT_APPROACHABLE_CONCURRENCY=YES``SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor` |
| UI | SwiftUI + UIKit 互操作(按需) |
| 最低 iOS | 15.6 |
| 设备 | iPhone + iPad,仅横屏,`UIRequiresFullScreen=true` |
| Xcode | 26.5+`objectVersion=77`File System Synchronized Group |
| 工程入口 | `ylgamehall.xcodeproj`**无 CocoaPods**,不使用 workspace |
| 依赖管理 | SPM(首选)+ Vendor `.xcframework`(闭源 SDK |
| Bundle ID | `com.skyapp.ylgamehall` |
| 并发模型 | Swift async/await + actor 隔离(**不使用 Combine** |
### SPM 依赖
- `ZIPFoundation` — H5 zip 解压
- `HappyDNS` — 七牛 SDK 配套
- `Qiniu``qiniu/objc-sdk` v8.9.x — 七牛对象存储
### Vendor 闭源 SDK
放在 `Vendor/<SDKName>/`,每个目录附 `README.md` 记录版本来源:
- `Vendor/WechatSDK/` — 微信开放平台
- `Vendor/AMap/` — 高德定位
- `Vendor/OpenCoreAMR/` — AMR 编解码(`libopencore-amrnb.a` / `libopencore-amrwb.a`
---
## 目录结构
```
ylgamehall/
├── CLAUDE.md # AI 助手协作约定(第一权威)
├── README.md # 本文件
├── ylgamehall.xcodeproj # Xcode 工程
├── ylgamehall/ # App target
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ ├── Info.plist
│ ├── ylgamehall-Bridging-Header.h
│ ├── Assets.xcassets
│ ├── Base.lproj/ # LaunchScreen.storyboard
│ ├── Resources/
│ │ ├── gamehall.zip # 内置 H5 包
│ │ ├── ChannelConfig.plist # 渠道配置(gameid / channel / agent / market 等)
│ │ ├── AppSecrets.plist # SDK 凭证(微信 AppID / 高德 Key / 七牛 等)
│ │ └── JS/
│ │ └── WebViewJavascriptBridge.js
│ └── Source/
│ ├── Audio/ # AMR 编解码、录音 / 播放、录音浮窗
│ ├── Bridge/ # WebViewJavascriptBridge + 各 Handler
│ │ └── Handlers/ # 39 项 H5↔Native 桥接 handler
│ ├── Coordinator/ # AppCoordinator 启动流水线
│ ├── Location/ # 高德定位封装
│ ├── Login/ # 微信登录
│ ├── Network/ # RemoteConfig、版本解析、七牛上传
│ ├── Resource/ # 沙盒路径、H5 zip 升级、解压、子游戏下载
│ ├── SDK/ # 微信 / 高德 SDK 包装
│ ├── Share/ # 微信 / QQ / 抖音分享 + 分享面板
│ └── WebView/ # WKWebView 容器、Splash、子游戏 VC
├── Vendor/ # 闭源 SDK
│ ├── WechatSDK/
│ ├── AMap/
│ └── OpenCoreAMR/
└── docs/ # 项目文档(见下节)
└── res/ # 维护者私人素材池(项目代码不感知)
```
---
## 文档体系
所有架构决策、契约定义、执行计划、验收清单都在 `docs/` 下。**任何代码修改前先读对应文档**。
| 文档 | 角色 | 何时读 |
|------|------|--------|
| [`CLAUDE.md`](./CLAUDE.md) | 项目协作约定、契约边界声明、两条核心原则 | **每次对话先读** |
| [`docs/H5-Native-Contract.md`](./docs/H5-Native-Contract.md) | H5 ↔ 原生 桥接**契约**(黑盒可观察行为) | 涉及桥接接口必读 |
| [`docs/H5-Native-Implementation-Design.md`](./docs/H5-Native-Implementation-Design.md) | 新外壳实施蓝图(架构、模块、代码骨架) | 实现新外壳必读 |
| [`docs/Development-Plan.md`](./docs/Development-Plan.md) | 开发执行计划(Phase 0–10、依赖图、进度追踪) | 排期 / 进度更新必读 |
| [`docs/Verification-Checklist.md`](./docs/Verification-Checklist.md) | 功能验证清单(统一验收的操作手册) | 阶段验收时勾选 |
| [`docs/SDK-Integration-Guide.md`](./docs/SDK-Integration-Guide.md) | 微信 / 高德 / opencore-amr / 七牛 接入手册 | 接 SDK 时按章节落地 |
| [`docs/H5-Debug-Guide.md`](./docs/H5-Debug-Guide.md) | H5 端调试(Safari Web Inspector | 调试 H5 console / Network / 断点时 |
| `../daoqi/` | 原项目 `msext` 工作参照实现 | 遇到行为差异 / 渠道注入 / SDK 时序等不确定问题时优先参考 |
> 文档优先级冲突时:**Contract > Design > Plan**。
---
## 核心原则(两条,原则 A 是第一准则)
### 原则 A:H5 端零修改(不可妥协)
H5 端 **一行代码、一个字符、一个文件名、一个调用时机都不许改**。遇到契约边界冲突时,**回到原 msext 老路**、由原生 Swift 端硬扛,哪怕代码看起来"不现代"。
必须 1:1 与现网一致的五类内容:
1. 桥接接口名(handler 字符串,含历史驼峰 / 下划线大小写)
2. 参数字段名(含 `"title "` 末尾空格、`Province` 大写 P 等历史包袱)
3. 参数数据结构(类型、嵌套、fallback)
4. H5 端注册方式(`WebViewJavascriptBridge` JS 协议 / 弹层 `window.settings.xxx`
5. 空实现接口也必须保留注册(`opensaoma``getGameplay` 等)
详见 [`docs/H5-Native-Contract.md`](./docs/H5-Native-Contract.md)。
### 原则 B:原生内部自由重构
原生内部一切自由——技术栈、模块结构、状态管理、SDK 选型、并发模型按现代 iOS 标准重新设计,不照搬旧项目实现细节。
**冲突时无条件选 A**:原生多一层适配 / 一段"看似过时"的代码可接受;契约破裂不可接受。
---
## 构建
### 前置
- macOS + Xcode 26.5+
- 项目方提供的 SDK 凭证(按 [`docs/SDK-Integration-Guide.md`](./docs/SDK-Integration-Guide.md) 填到 `Resources/AppSecrets.plist`
- Vendor 闭源 SDK 二进制(按 `Vendor/<SDK>/README.md` 放置)
### 构建步骤
```bash
open ylgamehall.xcodeproj
```
Xcode → Product → Run(或 ⌘R)。
> **不要**新建 `Podfile` / `.xcworkspace`。本项目通过 SPM + Vendor 两层管理依赖,禁止引入 CocoaPodsADR-006)。
### 渠道配置
`ylgamehall/Resources/ChannelConfig.plist` 是渠道注入入口(替代 msext 时代的"空目录名注入"机制):
| key | 说明 |
|---|---|
| `gameid` | 游戏 ID |
| `channel` / `gamedir` | 渠道标识 / H5 解压子目录名 |
| `gamestart` | H5 启动入口目录名(默认 `gamehall` |
| `gameconfig` | RemoteConfig 拉取地址 |
| `market` | 市场标识 |
| `agent` | 代理标识 |
| `appversion` | 当前外壳版本号(用于 RemoteConfig 升级判断) |
---
## 提交与协作
- commit message 使用中文短句,描述"做了什么 + 为什么"
- 改动通过最小验证(BuildProject / 关键路径手测 / 单测)后可直接 commit,不必每次确认
- 涉及桥接接口名 / 参数字段名 / 数据结构的改动必须在 commit message 中明示 **"契约影响"** 并附 [`docs/H5-Native-Contract.md`](./docs/H5-Native-Contract.md) 对应章节
- 每完成一个 Phase 子项立即同步勾选 [`docs/Development-Plan.md`](./docs/Development-Plan.md) §5 / §8
- 不在未授权情况下执行 `git push --force` / `git reset --hard` / `git rebase` 等不可逆操作
---
## 验收
新外壳完成后,**在不修改 H5 任何一行代码** 的前提下,逐项跑 [`docs/H5-Native-Contract.md`](./docs/H5-Native-Contract.md) §10 的 26 项验收清单。任意一项 H5 表现与现网(`../daoqi/msext`)不一致即视为契约违反。
+18 -3
View File
@@ -379,9 +379,9 @@ window.rootViewController = NavgationController
**流程** **流程**
1. 下载 AMR 到本地 1. 下载 AMR 到本地
2. `VoiceConverter ConvertAmrToWav` 转 WAV 2. `VoiceConverter ConvertAmrToWav` 转 WAV
3. `AVAudioPlayer` 播放 3. `AVAudioPlayer` 播放(若当前已有语音在播,先停旧、给 H5 发 `gameui_stop_voice(被打断 user)` 再起新播放)
4. 开始播放时 callback H5`gameui_play_voice(user)` (见 §3.2 7]) 4. 开始播放时 callback H5`gameui_play_voice(user)` (见 §3.2 7])
5. 播放结束 callback H5`gameui_stop_voice(user)` (见 §3.2 8]) 5. 播放结束 / 被打断 callback H5`gameui_stop_voice(user)` (见 §3.2 8])
##### 【6】 `voicePlaying` — 语音播放总开关 ##### 【6】 `voicePlaying` — 语音播放总开关
@@ -563,7 +563,7 @@ window.rootViewController = NavgationController
| 5 | `shakeEnd` | motionEnded 摇一摇结束 | `nil` | ✓ | ✓ | | 5 | `shakeEnd` | motionEnded 摇一摇结束 | `nil` | ✓ | ✓ |
| 6 | `getlocationinfo` | 定位成功/失败 | 见下方表 B | ✓ | ✓ | | 6 | `getlocationinfo` | 定位成功/失败 | 见下方表 B | ✓ | ✓ |
| 7 | `gameui_play_voice` | mediaTypeAudio 开始播放(且 voicePlaying=1 时)| 字符串 user_id | ✓ | ✓ | | 7 | `gameui_play_voice` | mediaTypeAudio 开始播放(且 voicePlaying=1 时)| 字符串 user_id | ✓ | ✓ |
| 8 | `gameui_stop_voice` | mediaTypeAudio AVAudioPlayer 播放结束 | 字符串 user_id(最近一次 mediaTypeAudio 的 user 字段| ✓ | ✓ | | 8 | `gameui_stop_voice` | mediaTypeAudio AVAudioPlayer 播放结束 / 被新一次 mediaTypeAudio 打断 / 解码失败 | 字符串 user_id(**该次结束 / 被打断的语音**所绑定的 user,不是入参里最新的 user — 与 daoqi 旧实现语义不同,详见下方 `gameui_stop_voice` 行为说明| ✓ | ✓ |
| 9 | `getaudiourl` | 录音 + 上传成功(七牛或旧 PostFile)| `{ "audiourl": "<七牛 URL>", "time": "<秒数字符串>" }` | ✓ | ✓ | | 9 | `getaudiourl` | 录音 + 上传成功(七牛或旧 PostFile)| `{ "audiourl": "<七牛 URL>", "time": "<秒数字符串>" }` | ✓ | ✓ |
| 10 | `sharelogin` | 微信授权 OAuth 拿到 userinfo 后 | 见下方表 C | ✓ | ✓ | | 10 | `sharelogin` | 微信授权 OAuth 拿到 userinfo 后 | 见下方表 C | ✓ | ✓ |
| 11 | `sharesuccess` | 分享完成(微信 SendMessageToWXResp / 闲聊 finishBlock| `{ "success": "2", "type": "<sharefriend 原值如 '1' 或 '2'>" }` | ✓ | ✓ | | 11 | `sharesuccess` | 分享完成(微信 SendMessageToWXResp / 闲聊 finishBlock| `{ "success": "2", "type": "<sharefriend 原值如 '1' 或 '2'>" }` | ✓ | ✓ |
@@ -586,6 +586,21 @@ H5 端 N 年来收到的实际语义就是:**`"2"` = 进入后台、`"1"` =
(顺带:原 msext 还有一条 iOS<9 UIWebView 路径 `RootVC.m:516-543` / `fourviewVC.m:1254-1282`,发的值反过来:进入后台 → `"1"` / 回到前台 → `"2"`。该路径在新外壳最低 iOS 15.6 下完全不可达,**不能作为对齐依据**,请忽略。) (顺带:原 msext 还有一条 iOS<9 UIWebView 路径 `RootVC.m:516-543` / `fourviewVC.m:1254-1282`,发的值反过来:进入后台 → `"1"` / 回到前台 → `"2"`。该路径在新外壳最低 iOS 15.6 下完全不可达,**不能作为对齐依据**,请忽略。)
**`gameui_stop_voice` 行为说明**(与 daoqi 旧实现的有意差异,用户反馈驱动):
daoqi `NewRootVC.m:1736-1748` / `gameController.m:1288-1308``voicePlayer` / `self.User_id` 都作为 VC 成员变量。覆盖式播放(前一条还在播,第二条到来)的代码路径:
1. `self.User_id = userid;` 先覆盖最新 user
2. `voicePlayer = [[AVAudioPlayer alloc]...]` 直接换 ivarMRC 下旧 player 实际泄漏继续在播)
3. 旧 player 自然播完触发 `audioPlayerDidFinishPlaying:` → 调 `stopradio``callHandler:@"gameui_stop_voice" data:self.User_id`
结果:旧消息对应的 `gameui_stop_voice` 发出去的 user_id **实际是新消息的 user_id**;H5 收不到旧消息的停止动画信号,旧动画一直转,新消息那条反而停了。
新外壳实现(`VoicePlayer.swift`)把 user 与 player 一一绑定(`currentUser`):
- 覆盖式 play 时**主动**停旧 player,并用旧 player 绑定的 user 触发一次 `gameui_stop_voice`,再起新播放 + `gameui_play_voice(新 user)`
- 自然结束 / 解码失败:用结束那个 player 绑定的 user 发 `gameui_stop_voice`,不再读"最近一次入参"
H5 端无需任何改动:仍然按照"收到 `gameui_stop_voice(user)` 就停这个 user 的动画"处理即可,只是信号现在会更准。验证场景:H5 连发两条 `mediaTypeAudio`user A → user BA 还没播完 B 就来),现在 H5 会先收 `gameui_stop_voice(A)` 再收 `gameui_play_voice(B)`B 播完再收 `gameui_stop_voice(B)`。daoqi 旧路径下 A 那次停止信号永远收不到。
**表 A — `getphoneinfo`** **表 A — `getphoneinfo`**
```json ```json
{ {
+5 -5
View File
@@ -78,7 +78,7 @@
| `AppDelegate.swift` | `AMapWrapper.bootstrap()` → 真实调隐私合规 3 步 + apiKey | | `AppDelegate.swift` | `AMapWrapper.bootstrap()` → 真实调隐私合规 3 步 + apiKey |
| `SceneDelegate.swift` | `openURLContexts` → 真实调 `WXApi.handleOpen(url, delegate:)` | | `SceneDelegate.swift` | `openURLContexts` → 真实调 `WXApi.handleOpen(url, delegate:)` |
| `AccreditLoginHandler` | `accreditlogin` → 真实拉起微信授权 → 反向 callback `sharelogin` 7 字段(Province 大写 P | | `AccreditLoginHandler` | `accreditlogin` → 真实拉起微信授权 → 反向 callback `sharelogin` 7 字段(Province 大写 P |
| `WechatShare` | `share()` → 真实调 `WXApi.send(SendMessageToWXReq)` | | `WechatShare` | `share()` / `shareLobby()` → 剪贴板 + 引导框 + `weixin://` 跳转(不依赖 SDK 的分享 API,与 QQ / 抖音同款交互) |
| `StartLocationHandler` | `startlocation` → 真实调 `AMapLocationManager.requestLocation` → 反向 callback `getlocationinfo` 9 字段(latitude/longitude string, province 小写 p | | `StartLocationHandler` | `startlocation` → 真实调 `AMapLocationManager.requestLocation` → 反向 callback `getlocationinfo` 9 字段(latitude/longitude string, province 小写 p |
| `BackGameDataHandler` | 子游戏 backgameData 退出时自动清 `WXApi.delegate = nil` + `LocationService.stop()` | | `BackGameDataHandler` | 子游戏 backgameData 退出时自动清 `WXApi.delegate = nil` + `LocationService.stop()` |
@@ -236,10 +236,10 @@ bridge.register("prepareaudio") { _, callback in
|---|---|---| |---|---|---|
| `Vendor/WechatSDK/WechatOpenSDK.xcframework` | ⏳ 等用户放置 | 项目方从腾讯开放平台下 `OpenSDK2.0.5+_NoPay.zip` 解压 → 放进 `Vendor/WechatSDK/` → 按 §A.1 Embed & Sign | | `Vendor/WechatSDK/WechatOpenSDK.xcframework` | ⏳ 等用户放置 | 项目方从腾讯开放平台下 `OpenSDK2.0.5+_NoPay.zip` 解压 → 放进 `Vendor/WechatSDK/` → 按 §A.1 Embed & Sign |
| `Source/SDK/WeChat/WeChatSDK.swift` | ✅ 已写 | `register()` + `handleOpenURL` 包 canImport | | `Source/SDK/WeChat/WeChatSDK.swift` | ✅ 已写 | `register()` + `handleOpenURL` 包 canImport |
| `Source/SDK/WeChat/WeChatManager.swift` | ✅ 已写 | authorize / shareLink async wrapper + WXApiDelegate | | `Source/SDK/WeChat/WeChatManager.swift` | ✅ 已写 | authorize async wrapper + WXApiDelegate(分享路径已剥离,走剪贴板见 `WechatShare` |
| `Source/Login/WeChatAuth.swift` | ✅ 已写 | OAuth2 客户端直拼 sns/oauth2 + userinfo + 7 字段 | | `Source/Login/WeChatAuth.swift` | ✅ 已写 | OAuth2 客户端直拼 sns/oauth2 + userinfo + 7 字段 |
| `Source/Bridge/Handlers/AccreditLoginHandler.swift` | ✅ 已升级 | 真实拉起授权 + sharelogin 反向 callbackProvince 大写 P | | `Source/Bridge/Handlers/AccreditLoginHandler.swift` | ✅ 已升级 | 真实拉起授权 + sharelogin 反向 callbackProvince 大写 P |
| `Source/Share/WechatShare.swift` | ✅ 已升级 | 真实链接分享(type=2/3 截图待 Phase 4.F | | `Source/Share/WechatShare.swift` | ✅ 已改造 | 剪贴板 + 引导框 + `weixin://` 跳转(与 QQ / 抖音一致,不依赖微信 SDK 分享 API |
| `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `WXApi.delegate = nil` 子游戏 pop 时清理 | | `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `WXApi.delegate = nil` 子游戏 pop 时清理 |
| `AppDelegate.swift` | ✅ 已加注册 | `WeChatSDK.register()` | | `AppDelegate.swift` | ✅ 已加注册 | `WeChatSDK.register()` |
| `SceneDelegate.swift` | ✅ 已加 openURL | `WeChatSDK.handleOpenURL(ctx.url)` | | `SceneDelegate.swift` | ✅ 已加 openURL | `WeChatSDK.handleOpenURL(ctx.url)` |
@@ -248,8 +248,8 @@ bridge.register("prepareaudio") { _, callback in
### 1.4 验收(用户 Xcode 加完 framework 后跑) ### 1.4 验收(用户 Xcode 加完 framework 后跑)
- [ ] H5 调 `bridge.callHandler('accreditlogin')` → 拉起微信 → 同意授权 → 大厅 H5 收 `sharelogin({openid,...,Province})`**Province 大写 P** - [ ] H5 调 `bridge.callHandler('accreditlogin')` → 拉起微信 → 同意授权 → 大厅 H5 收 `sharelogin({openid,...,Province})`**Province 大写 P**
- [ ] H5 调 `bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})` → 拉起微信朋友圈 - [ ] H5 调 `bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})` → 写剪贴板 + 弹引导框 → 点"立即打开微信"跳转到微信首页(用户自行粘贴到朋友圈
- [ ] H5 调 `sharefriend:"1"` → SharePanel 三按钮,点微信 → 拉起微信好友选择 - [ ] H5 调 `sharefriend:"1"` → SharePanel 三按钮,点微信 → 写剪贴板 + 弹引导框 → 跳微信首页(用户自行粘贴到对话框)
- [ ] **关键回归**:截图分享 / QQ 分享 / 抖音分享路径不受影响 - [ ] **关键回归**:截图分享 / QQ 分享 / 抖音分享路径不受影响
### 1.5 风险 ### 1.5 风险
@@ -261,12 +261,11 @@ public enum RemoteAudioHandler {
return // cb daoqi file return return // cb daoqi file return
} }
// 2. lastUserId daoqi // 2. VoicePlayer.onFinish mediaTypeAudio bridge
// `self.User_id = userid;` // [weak bridge] bridge onFinish
VoicePlayer.shared.recordIncomingUser(user) // onFinish / / play
// payload `finishedUser` / user
// 3. VoicePlayer.onFinish mediaTypeAudio bridge // VoicePlayer currentUser user
// [weak bridge] bridge onFinish
VoicePlayer.shared.onFinish = { [weak bridge] finishedUser in VoicePlayer.shared.onFinish = { [weak bridge] finishedUser in
Task { @MainActor in Task { @MainActor in
bridge?.call( bridge?.call(
@@ -277,7 +276,7 @@ public enum RemoteAudioHandler {
} }
} }
// 4. AMR // 3. AMR
let amrData: Data let amrData: Data
do { do {
let (d, _) = try await URLSession.shared.data(from: downloadURL) let (d, _) = try await URLSession.shared.data(from: downloadURL)
@@ -288,7 +287,7 @@ public enum RemoteAudioHandler {
// `if (![[NSFileManager defaultManager] fileExistsAtPath:filename]) return;` // `if (![[NSFileManager defaultManager] fileExistsAtPath:filename]) return;`
} }
// 5. .amr // 4. .amr
let tempName = VoiceRecorder.currentTimeString() let tempName = VoiceRecorder.currentTimeString()
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let amrLocal = docs.appendingPathComponent(tempName).appendingPathExtension("amr") let amrLocal = docs.appendingPathComponent(tempName).appendingPathExtension("amr")
@@ -300,7 +299,7 @@ public enum RemoteAudioHandler {
return return
} }
// 6. AMR WAV // 5. AMR WAV
// daoqi tempName + "_AmrToWav.wav"NewRootVC.m:341 // daoqi tempName + "_AmrToWav.wav"NewRootVC.m:341
let wavLocal = docs let wavLocal = docs
.appendingPathComponent("\(tempName)_AmrToWav") .appendingPathComponent("\(tempName)_AmrToWav")
@@ -317,14 +316,16 @@ public enum RemoteAudioHandler {
transcoded = false transcoded = false
} }
// 7. voicePlaying + + gameui_play_voice // 6. voicePlaying + + gameui_play_voice
// + enabled daoqi // + enabled daoqi
// NewRootVC.m:343-353 if (canbofang) { play + callHandler } // NewRootVC.m:343-353 if (canbofang) { play + callHandler }
// playerVoicePlayer.play player
// onFinish gameui_stop_voice H5
let canPlay = await VoiceCenter.shared.isEnabled let canPlay = await VoiceCenter.shared.isEnabled
log.debug("mediaTypeAudio user=\(user, privacy: .public) transcoded=\(transcoded, privacy: .public) canPlay=\(canPlay, privacy: .public)") log.debug("mediaTypeAudio user=\(user, privacy: .public) transcoded=\(transcoded, privacy: .public) canPlay=\(canPlay, privacy: .public)")
if transcoded, canPlay { if transcoded, canPlay {
do { do {
try VoicePlayer.shared.play(wavLocal) try VoicePlayer.shared.play(wavLocal, user: user)
// gameui_play_voice user dict // gameui_play_voice user dict
bridge.call( bridge.call(
"gameui_play_voice", "gameui_play_voice",
@@ -337,7 +338,7 @@ public enum RemoteAudioHandler {
} }
} }
// 8. mediaTypeAudio cb daoqi NewRootVC.m:357**** // 7. mediaTypeAudio cb daoqi NewRootVC.m:357****
// 使 / canbofang=NO return // 使 / canbofang=NO return
callback?(.string("Response from mediaTypeAudio")) callback?(.string("Response from mediaTypeAudio"))
} }
@@ -2,10 +2,11 @@
// WeChatManager.swift // WeChatManager.swift
// ylgamehall // ylgamehall
// //
// SDK delegate + authorize + share // SDK delegate + authorize
// WXApiDelegate async/awaitstate UUID // WXApiDelegate onResp async/awaitstate UUID
// //
// Design §8.5msext WXApiManager.m // + WechatShare.swift
// SDK SendMessageToWXReq OAuth
// //
import Foundation import Foundation
@@ -23,8 +24,6 @@ public final class WeChatManager: NSObject {
/// resolve authorize continuationstate UUID /// resolve authorize continuationstate UUID
private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:] private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:]
/// resolve share continuationFIFO ID
private var sharePending: [CheckedContinuation<Void, Error>] = []
public override init() { public override init() {
super.init() super.init()
@@ -84,125 +83,6 @@ public final class WeChatManager: NSObject {
return top 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+ messageExtSDK
/// 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 // MARK: - Internal: resolve callbacks
fileprivate func resolveAuth(state: String, code: String?, errCode: Int) { fileprivate func resolveAuth(state: String, code: String?, errCode: Int) {
@@ -213,16 +93,6 @@ public final class WeChatManager: NSObject {
cont.resume(throwing: WeChatError.authFailed(errCode)) 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 // MARK: - WXApiDelegate
@@ -242,11 +112,8 @@ extension WeChatManager: WXApiDelegate {
Task { @MainActor in Task { @MainActor in
Self.shared.resolveAuth(state: state, code: code, errCode: errCode) Self.shared.resolveAuth(state: state, code: code, errCode: errCode)
} }
} else if resp is SendMessageToWXResp {
Task { @MainActor in
Self.shared.resolveShare(errCode: errCode)
}
} }
// WechatShare SendMessageToWXResp
} }
} }
@@ -260,5 +127,4 @@ public struct WXAuthCodePayload: Sendable {
public enum WeChatError: Error, Sendable { public enum WeChatError: Error, Sendable {
case sdkNotLinked case sdkNotLinked
case authFailed(Int) case authFailed(Int)
case shareFailed(Int)
} }
+1 -1
View File
@@ -22,7 +22,7 @@ public final class SharePanel: UIView {
private static let log = Logger(subsystem: "ylgamehall", category: "SharePanel") private static let log = Logger(subsystem: "ylgamehall", category: "SharePanel")
private static let panelHeight: CGFloat = 200 private static let panelHeight: CGFloat = 150
private static let buttonSize: CGFloat = 60 private static let buttonSize: CGFloat = 60
private static let animationDuration: TimeInterval = 0.25 private static let animationDuration: TimeInterval = 0.25
+14 -10
View File
@@ -104,16 +104,20 @@ public final class WechatShare: SharePlatform {
// MARK: - // MARK: -
/// title + desc + url UIPasteboard.string /// title + "\n\n" + desc UIPasteboard.string
/// "" DouyinShare /// DouyinShare 1:1 url
/// ""
private func shareLink(_ content: ShareContent) { private func shareLink(_ content: ShareContent) {
var parts: [String] = [] var shareText = ""
if !content.title.isEmpty { parts.append(content.title) } if !content.title.isEmpty {
if !content.desc.isEmpty { parts.append(content.desc) } shareText = content.title
if !content.webpageUrl.isEmpty { parts.append(content.webpageUrl) } }
let shareText = parts.isEmpty if !content.desc.isEmpty {
? "来自进贤聚友棋牌的精彩内容分享" shareText = shareText.isEmpty ? content.desc : "\(shareText)\n\n\(content.desc)"
: parts.joined(separator: "\n") }
if shareText.isEmpty {
shareText = "来自进贤聚友棋牌的精彩内容分享"
}
UIPasteboard.general.string = shareText UIPasteboard.general.string = shareText
Self.log.debug("WechatShare 链接已复制到剪贴板") Self.log.debug("WechatShare 链接已复制到剪贴板")
@@ -121,7 +125,7 @@ public final class WechatShare: SharePlatform {
✅ 内容已复制到剪贴板 ✅ 内容已复制到剪贴板
🎯 分享到好友、群聊或朋友圈步骤: 🎯 分享到好友、群聊或朋友圈步骤:
1️⃣ 打开微信,选择好友、群聊或朋友圈 1️⃣ 打开微信选择好友、群聊或朋友圈
2️⃣ 在输入框长按粘贴内容 2️⃣ 在输入框长按粘贴内容
3️⃣ 点击发送即可分享 3️⃣ 点击发送即可分享
""" """