4.B 分享逻辑对齐 daoqi msext:调用即触发 sharesuccess
【契约影响】docs/H5-Native-Contract.md §3.1[2]/§3.2[11]
H5 端 sharesuccess 时机改为"调用 friendsShare handler 即立刻发",与
原 msext 行为等价(live path 完全不等 SDK / URL Scheme 回包)。用户
明示有意为之:URL Scheme + 剪贴板路径本就没有可靠回包,乐观策略避免
H5 业务卡住。
3 平台分支严格 1:1 对齐 daoqi msext:
WechatShare(对齐 WechatShareManager shareWithContent)
- type=="2" → 截图 JPEG 0.6 + sendImageData + app icon thumb
- else → 链接分享 sendLinkURL + app icon thumb
- 移除原"远端图"分支(原项目无此分支)
- 新增 appIconImage helper(读 Info.plist CFBundleIcons,对齐
[self getAppIconImage])
QQShare(对齐 QQShareManager shareWithContent + simpleShareToQQFriend)
- type=="2" → 截图 → UIPasteboard.image → 4 URL Scheme fallback
- else → mqqapi://share/to_{fri,qzone}?req_type=1&url=...
&title=...&description=...(webpageUrl 空时 req_type=0)
- 不依赖 QQ SDK(CLAUDE.md ADR-006)
DouyinShare(对齐 DouyinShareManager shareWithContent)
- type=="2" → 截图 → UIPasteboard.image → "立即打开抖音"引导框
- else → title+"\n\n"+desc → UIPasteboard.string → 引导框
双侧空时兜底 "来自进贤聚友棋牌的精彩内容分享"
- 放弃之前的"存相册 + 自动拉起"激进路径,回到原项目"用户主动点"路径
架构调整:
- SharePlatform.share 改 sync fire-and-forget(去 ShareResult 返回)
- ShareCenter.dispatch / SharePanel.show 去 completion 参数
- FriendsShareHandler:解析后先发 responseCallback("sharefriend")
与 sharesuccess({success:"2", type:<sharefriend>}),再异步 dispatch
This commit is contained in:
@@ -2,19 +2,15 @@
|
||||
// DouyinShare.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// 抖音分享 SharePlatform 实现(Phase 4.D 完整)。
|
||||
// 抖音分享 SharePlatform 实现,1:1 对齐 daoqi msext DouyinShareManager
|
||||
// `shareWithContent:completion:`(按 type 分两个分支):
|
||||
// - type == "2" → 截图分享:截图 → UIPasteboard → 弹"立即打开抖音"引导框
|
||||
// - else → 文字分享:title + "\n\n" + desc → UIPasteboard → 弹引导框
|
||||
//
|
||||
// 移植 msext DouyinShareManager.m:201-269 shareImageToDouyin 流程:
|
||||
// 1. 拿图片源(type=2 → ImageProvider.captureScreenshot;其它 →
|
||||
// ImageProvider.downloadRemote);type=1 链接 → 抖音不原生支持纯链接,
|
||||
// 退化为截屏分享(msext 行为:所有 H5 分享到抖音都走图片)
|
||||
// 2. PhotoLibrarySaver.save 保存到相册(addOnly 权限)
|
||||
// 3. UIPasteboard 兜底放图片数据,供抖音读取
|
||||
// 4. 按顺序尝试 snssdk1128://camera / publish / share/image / 基础 scheme,
|
||||
// canOpenURL 命中即调起
|
||||
// 5. 调起即视为 success(URL Scheme 单向无返回)
|
||||
// **不依赖抖音 SDK**(CLAUDE.md ADR-006):抖音的图片 / 文本分享原本就是
|
||||
// 剪贴板 + 引导用户在抖音内粘贴的非 SDK 路径,新外壳照搬。
|
||||
//
|
||||
// **不依赖抖音 SDK**。
|
||||
// fire-and-forget:sharesuccess 已在 FriendsShareHandler 立即触发。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@@ -27,68 +23,113 @@ public final class DouyinShare: SharePlatform {
|
||||
|
||||
private static let log = Logger(subsystem: "ylgamehall", category: "DouyinShare")
|
||||
|
||||
private static let douyinScheme = "snssdk1128://"
|
||||
|
||||
public let name = "Douyin"
|
||||
|
||||
public var isInstalled: Bool {
|
||||
guard let url = URL(string: "snssdk1128://") else { return false }
|
||||
guard let url = URL(string: Self.douyinScheme) else { return false }
|
||||
return UIApplication.shared.canOpenURL(url)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult {
|
||||
guard isInstalled else { return .notInstalled(platform: name) }
|
||||
public func share(_ content: ShareContent, scene: ShareScene) {
|
||||
guard isInstalled else { return }
|
||||
|
||||
// 1. 拿图片源
|
||||
let image: UIImage
|
||||
if content.type == "2" {
|
||||
shareScreenshot()
|
||||
} else {
|
||||
shareText(content)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 截图分享
|
||||
|
||||
/// 对齐 msext DouyinShareManager.m:139-162 isScreenshotShare 分支:
|
||||
/// 截图 → pasteboard.image → 弹引导框(不自动拉起抖音,等用户点"立即打开")。
|
||||
private func shareScreenshot() {
|
||||
do {
|
||||
switch content.type {
|
||||
case "2":
|
||||
image = try ImageProvider.captureScreenshot()
|
||||
case "1":
|
||||
// 抖音不支持纯链接分享,退化为当前屏截屏(msext 行为)
|
||||
Self.log.debug("DouyinShare: type=1 链接,退化为截屏分享")
|
||||
image = try ImageProvider.captureScreenshot()
|
||||
default:
|
||||
// type == 其它 → 远端图片 URL
|
||||
image = try await ImageProvider.downloadRemote(content.webpageUrl)
|
||||
let shot = try ImageProvider.captureScreenshot()
|
||||
UIPasteboard.general.image = shot
|
||||
Self.log.debug("DouyinShare 截图已复制到剪贴板")
|
||||
} catch {
|
||||
Self.log.error("DouyinShare 截图失败 \(error.localizedDescription, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
let message = """
|
||||
✅ 图片已复制到剪贴板
|
||||
|
||||
🎯 分享到好友或群聊步骤:
|
||||
1️⃣ 打开抖音,点击右下角「消息」
|
||||
2️⃣ 选择好友或群聊进入聊天
|
||||
3️⃣ 在输入框长按粘贴图片
|
||||
4️⃣ 点击发送即可分享
|
||||
"""
|
||||
showGuidanceAlert(title: "抖音分享准备完成", message: message)
|
||||
}
|
||||
|
||||
// MARK: - 文字分享
|
||||
|
||||
/// 对齐 msext DouyinShareManager.m:163-196 else 分支:
|
||||
/// title + "\n\n" + desc 拼字符串 → pasteboard.string → 弹引导框。
|
||||
/// 双侧都空时用 msext 同款兜底文案"来自进贤聚友棋牌的精彩内容分享"。
|
||||
private func shareText(_ content: ShareContent) {
|
||||
var shareText = ""
|
||||
if !content.title.isEmpty {
|
||||
shareText = content.title
|
||||
}
|
||||
if !content.desc.isEmpty {
|
||||
shareText = shareText.isEmpty ? content.desc : "\(shareText)\n\n\(content.desc)"
|
||||
}
|
||||
if shareText.isEmpty {
|
||||
shareText = "来自进贤聚友棋牌的精彩内容分享"
|
||||
}
|
||||
UIPasteboard.general.string = shareText
|
||||
Self.log.debug("DouyinShare 文字已复制到剪贴板")
|
||||
|
||||
let message = """
|
||||
✅ 内容已复制到剪贴板
|
||||
|
||||
🎯 分享到好友或群聊步骤:
|
||||
1️⃣ 打开抖音,点击右下角「消息」
|
||||
2️⃣ 选择好友或群聊进入聊天
|
||||
3️⃣ 在输入框长按粘贴内容
|
||||
4️⃣ 点击发送即可分享
|
||||
"""
|
||||
showGuidanceAlert(title: "抖音分享准备完成", message: message)
|
||||
}
|
||||
|
||||
// MARK: - 引导弹窗
|
||||
|
||||
/// 弹"立即打开抖音 / 稍后分享"二选一 Alert,对齐 msext
|
||||
/// `showGuidanceAlertWithTitle:message:`。
|
||||
private func showGuidanceAlert(title: String, message: String) {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "立即打开抖音", style: .default) { _ in
|
||||
if let url = URL(string: Self.douyinScheme),
|
||||
UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
} catch {
|
||||
Self.log.error("DouyinShare: 获取图片失败 \(error.localizedDescription, privacy: .public)")
|
||||
return .failed(reason: "获取图片失败: \(error.localizedDescription)")
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "稍后分享", style: .cancel))
|
||||
|
||||
guard let topVC = Self.topViewController() else { return }
|
||||
topVC.present(alert, animated: true)
|
||||
}
|
||||
|
||||
private static func topViewController() -> UIViewController? {
|
||||
let scene = UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first(where: { $0.activationState == .foregroundActive })
|
||||
?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first
|
||||
guard var top = scene?.windows.first(where: \.isKeyWindow)?.rootViewController
|
||||
?? scene?.windows.first?.rootViewController else { return nil }
|
||||
while let presented = top.presentedViewController { top = presented }
|
||||
if let nav = top as? UINavigationController, let visible = nav.visibleViewController {
|
||||
return visible
|
||||
}
|
||||
|
||||
// 2. 保存到相册(含权限检查)
|
||||
do {
|
||||
try await PhotoLibrarySaver.save(image)
|
||||
} catch PhotoLibrarySaverError.permissionDenied {
|
||||
Self.log.warning("DouyinShare: 相册写入权限被拒")
|
||||
return .failed(reason: "相册写入权限被拒")
|
||||
} catch {
|
||||
Self.log.error("DouyinShare: 保存相册失败 \(error.localizedDescription, privacy: .public)")
|
||||
return .failed(reason: "保存相册失败: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
// 3. 同时放剪贴板兜底(msext 行为:抖音从剪贴板读图)
|
||||
UIPasteboard.general.image = image
|
||||
|
||||
// 4. 按 msext 顺序尝试多个 URL Scheme
|
||||
let schemes = [
|
||||
"snssdk1128://camera", // 抖音拍摄界面
|
||||
"snssdk1128://publish", // 发布界面
|
||||
"snssdk1128://share/image", // 图片分享(可能已废弃但保留)
|
||||
"snssdk1128://" // 兜底基础 scheme
|
||||
]
|
||||
|
||||
for schemeString in schemes {
|
||||
guard let url = URL(string: schemeString),
|
||||
UIApplication.shared.canOpenURL(url)
|
||||
else { continue }
|
||||
Self.log.debug("DouyinShare open: \(schemeString, privacy: .public)")
|
||||
let opened = await UIApplication.shared.open(url)
|
||||
if opened { return .success }
|
||||
}
|
||||
|
||||
return .failed(reason: "所有抖音 URL Scheme 都调起失败")
|
||||
return top
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user