- 新增 WechatShareManager:封装微信分享逻辑,支持ShareContent对象 - 增强 DouyinShareManager:新增ShareContent支持和分享引导功能 - 优化 QQShareManager:支持截图/纯文本分享类型自动识别 - 重构 SharePanel:简化为UI调度层,移除具体分享实现 - 实现职责分离:各Manager专注自己的平台分享逻辑 - 提升可维护性:修改某平台不影响其他平台 - 增强可扩展性:新增分享平台更容易实现
596 lines
22 KiB
Objective-C
596 lines
22 KiB
Objective-C
//
|
||
// DouyinShareManager.m
|
||
// msext
|
||
//
|
||
// Created on 2025/06/15.
|
||
// Copyright © 2025年. All rights reserved.
|
||
//
|
||
|
||
#import "DouyinShareManager.h"
|
||
#import "FuncPublic.h" // 用于截图功能
|
||
#import <Photos/Photos.h>
|
||
#import <MobileCoreServices/MobileCoreServices.h>
|
||
|
||
// 抖音URL Schemes
|
||
#define kDouyinScheme @"snssdk1128://"
|
||
#define kDouyinShareScheme @"snssdk1128://share/video"
|
||
#define kDouyinImageShareScheme @"snssdk1128://share/image"
|
||
#define kDouyinHashtagScheme @"snssdk1128://challenge/detail/"
|
||
#define kDouyinUserProfileScheme @"snssdk1128://user/profile/"
|
||
|
||
// 抖音应用回调的处理
|
||
static void(^DouyinShareCompletion)(BOOL) = nil;
|
||
|
||
@implementation DouyinShareManager
|
||
|
||
#pragma mark - Public Methods
|
||
|
||
+ (BOOL)isDouyinInstalled {
|
||
return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:kDouyinScheme]];
|
||
}
|
||
|
||
+ (void)shareToDouyin:(DouyinShareType)type
|
||
hashtagName:(NSString *)hashtagName
|
||
image:(UIImage *)image
|
||
videoUrl:(NSURL *)videoUrl
|
||
userId:(NSString *)userId
|
||
completion:(void (^)(BOOL))completion {
|
||
|
||
// 检查抖音是否已安装
|
||
if (![self isDouyinInstalled]) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
[self showAppNotInstalledAlert:@"抖音"];
|
||
return;
|
||
}
|
||
|
||
// 保存回调
|
||
DouyinShareCompletion = completion;
|
||
|
||
switch (type) {
|
||
case DouyinShareTypeImage:
|
||
[self shareImageToDouyin:image completion:completion];
|
||
break;
|
||
|
||
case DouyinShareTypeVideo:
|
||
[self shareVideoToDouyin:videoUrl completion:completion];
|
||
break;
|
||
|
||
case DouyinShareTypeHashtag:
|
||
[self shareHashtagToDouyin:hashtagName completion:completion];
|
||
break;
|
||
|
||
case DouyinShareTypeOpenProfile:
|
||
[self openDouyinUserProfile:userId completion:completion];
|
||
break;
|
||
}
|
||
}
|
||
|
||
+ (BOOL)handleOpenURL:(NSURL *)url {
|
||
// 判断是否是抖音返回的URL
|
||
NSString *urlString = [url absoluteString];
|
||
if ([urlString hasPrefix:@"msext://douyin_share_result"]) {
|
||
// 根据返回的URL解析结果
|
||
NSString *result = [self getUrlParam:urlString paramName:@"result"];
|
||
BOOL success = result && [result isEqualToString:@"success"];
|
||
|
||
// 执行回调
|
||
if (DouyinShareCompletion) {
|
||
DouyinShareCompletion(success);
|
||
DouyinShareCompletion = nil;
|
||
}
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
+ (void)shareWithContent:(id)shareContent
|
||
completion:(void (^ _Nullable)(BOOL success))completion {
|
||
NSLog(@"🔍 [DouyinShareManager] 开始抖音分享");
|
||
|
||
// 检查抖音是否已安装
|
||
if (![self isDouyinInstalled]) {
|
||
NSLog(@"❌ [DouyinShareManager] 抖音未安装");
|
||
[self showAppNotInstalledAlert:@"抖音"];
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 检查ShareContent对象
|
||
if (!shareContent) {
|
||
NSLog(@"❌ [DouyinShareManager] ShareContent对象为空");
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 通过KVC从ShareContent对象中提取参数
|
||
NSString *title = nil;
|
||
NSString *description = nil;
|
||
NSString *type = nil;
|
||
|
||
@try {
|
||
if ([shareContent respondsToSelector:@selector(valueForKey:)]) {
|
||
title = [shareContent valueForKey:@"title"];
|
||
description = [shareContent valueForKey:@"desc"];
|
||
type = [shareContent valueForKey:@"type"];
|
||
}
|
||
|
||
NSLog(@"🔍 [DouyinShareManager] 从ShareContent提取参数:");
|
||
NSLog(@"🔍 - 标题: %@", title ?: @"(无)");
|
||
NSLog(@"🔍 - 描述: %@", description ?: @"(无)");
|
||
NSLog(@"🔍 - 类型: %@", type ?: @"(无)");
|
||
|
||
} @catch (NSException *exception) {
|
||
NSLog(@"❌ [DouyinShareManager] 解析ShareContent时发生异常: %@", exception.reason);
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 根据分享类型选择分享方式
|
||
BOOL isScreenshotShare = [type intValue] == 2;
|
||
|
||
if (isScreenshotShare) {
|
||
// 截图分享模式
|
||
NSLog(@"🔍 [DouyinShareManager] 执行截图分享");
|
||
|
||
UIImage *screenImage = [FuncPublic getImageWithFullScreenshot];
|
||
if (screenImage) {
|
||
// 复制图片到剪贴板
|
||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||
pasteboard.image = screenImage;
|
||
|
||
NSLog(@"✅ [DouyinShareManager] 截图已复制到剪贴板");
|
||
|
||
// 显示分享引导弹窗
|
||
[self showImageShareGuidance];
|
||
|
||
if (completion) {
|
||
completion(YES);
|
||
}
|
||
} else {
|
||
NSLog(@"❌ [DouyinShareManager] 截图获取失败");
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
}
|
||
} else {
|
||
// 文本分享模式
|
||
NSLog(@"🔍 [DouyinShareManager] 执行文本分享");
|
||
|
||
// 组合分享内容
|
||
NSString *shareText = @"";
|
||
if (title && title.length > 0) {
|
||
shareText = title;
|
||
}
|
||
if (description && description.length > 0) {
|
||
if (shareText.length > 0) {
|
||
shareText = [NSString stringWithFormat:@"%@\n\n%@", shareText, description];
|
||
} else {
|
||
shareText = description;
|
||
}
|
||
}
|
||
|
||
if (shareText.length == 0) {
|
||
shareText = @"来自进贤聚友棋牌的精彩内容分享";
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||
[pasteboard setString:shareText];
|
||
|
||
NSLog(@"✅ [DouyinShareManager] 文本已复制到剪贴板: %@", shareText);
|
||
|
||
// 显示分享引导弹窗
|
||
[self showTextShareGuidance];
|
||
|
||
if (completion) {
|
||
completion(YES);
|
||
}
|
||
}
|
||
}
|
||
|
||
#pragma mark - Private Methods
|
||
|
||
+ (void)shareImageToDouyin:(UIImage *)image completion:(void (^)(BOOL))completion {
|
||
if (!image) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 方案1:先保存图片到相册,然后使用UIPasteboard传递图片数据,最后打开抖音
|
||
[self requestPhotoLibraryAuthorizationWithCompletion:^(BOOL granted) {
|
||
if (!granted) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
[self showPhotoLibraryPermissionAlert];
|
||
return;
|
||
}
|
||
|
||
// 将图片保存到相册
|
||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
|
||
} completionHandler:^(BOOL success, NSError * _Nullable error) {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
if (success) {
|
||
// 同时将图片数据放到剪贴板,供抖音读取
|
||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||
[pasteboard setImage:image];
|
||
|
||
// 尝试多种URL方案打开抖音分享
|
||
NSArray *urlSchemes = @[
|
||
@"snssdk1128://camera", // 打开抖音拍摄界面
|
||
@"snssdk1128://publish", // 打开发布界面
|
||
@"snssdk1128://share/image", // 图片分享(可能已废弃)
|
||
@"snssdk1128://" // 基础scheme,打开抖音主界面
|
||
];
|
||
|
||
BOOL opened = NO;
|
||
for (NSString *urlString in urlSchemes) {
|
||
NSURL *douyinURL = [NSURL URLWithString:urlString];
|
||
if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) {
|
||
if (@available(iOS 10.0, *)) {
|
||
[[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL openSuccess) {
|
||
if (completion) {
|
||
completion(openSuccess);
|
||
}
|
||
}];
|
||
} else {
|
||
BOOL openSuccess = [[UIApplication sharedApplication] openURL:douyinURL];
|
||
if (completion) {
|
||
completion(openSuccess);
|
||
}
|
||
}
|
||
opened = YES;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!opened && completion) {
|
||
completion(NO);
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
NSLog(@"保存图片到相册失败:%@", error);
|
||
}
|
||
});
|
||
}];
|
||
}];
|
||
}
|
||
|
||
+ (void)shareVideoToDouyin:(NSURL *)videoUrl completion:(void (^)(BOOL))completion {
|
||
if (!videoUrl) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if (![[NSFileManager defaultManager] fileExistsAtPath:videoUrl.path]) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 将视频保存到相册
|
||
[self requestPhotoLibraryAuthorizationWithCompletion:^(BOOL granted) {
|
||
if (!granted) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
[self showPhotoLibraryPermissionAlert];
|
||
return;
|
||
}
|
||
|
||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoUrl];
|
||
} completionHandler:^(BOOL success, NSError * _Nullable error) {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
if (success) {
|
||
// 获取刚保存的视频的资源标识符
|
||
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
|
||
fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
||
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:fetchOptions];
|
||
|
||
if (fetchResult.firstObject) {
|
||
PHAsset *asset = fetchResult.firstObject;
|
||
NSString *localIdentifier = asset.localIdentifier;
|
||
|
||
// 构建抖音分享URL
|
||
NSString *urlString = [NSString stringWithFormat:@"%@?video=%@&from=app_%@",
|
||
kDouyinShareScheme,
|
||
[self encodeString:localIdentifier],
|
||
[self encodeString:@"msext"]];
|
||
|
||
NSURL *douyinURL = [NSURL URLWithString:urlString];
|
||
if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) {
|
||
if (@available(iOS 10.0, *)) {
|
||
[[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) {
|
||
// 这里不调用completion,等待抖音返回再调用
|
||
}];
|
||
} else {
|
||
[[UIApplication sharedApplication] openURL:douyinURL];
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
NSLog(@"保存视频到相册失败:%@", error);
|
||
}
|
||
});
|
||
}];
|
||
}];
|
||
}
|
||
|
||
+ (void)shareHashtagToDouyin:(NSString *)hashtagName completion:(void (^)(BOOL))completion {
|
||
if (!hashtagName || hashtagName.length == 0) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 构建打开话题挑战的URL
|
||
NSString *urlString = [NSString stringWithFormat:@"%@%@",
|
||
kDouyinHashtagScheme,
|
||
[self encodeString:hashtagName]];
|
||
|
||
NSURL *douyinURL = [NSURL URLWithString:urlString];
|
||
if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) {
|
||
if (@available(iOS 10.0, *)) {
|
||
[[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) {
|
||
if (completion) {
|
||
completion(success);
|
||
}
|
||
}];
|
||
} else {
|
||
BOOL success = [[UIApplication sharedApplication] openURL:douyinURL];
|
||
if (completion) {
|
||
completion(success);
|
||
}
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
}
|
||
}
|
||
|
||
+ (void)openDouyinUserProfile:(NSString *)userId completion:(void (^)(BOOL))completion {
|
||
if (!userId || userId.length == 0) {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 构建打开用户主页的URL
|
||
NSString *urlString = [NSString stringWithFormat:@"%@%@",
|
||
kDouyinUserProfileScheme,
|
||
[self encodeString:userId]];
|
||
|
||
NSURL *douyinURL = [NSURL URLWithString:urlString];
|
||
if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) {
|
||
if (@available(iOS 10.0, *)) {
|
||
[[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) {
|
||
if (completion) {
|
||
completion(success);
|
||
}
|
||
}];
|
||
} else {
|
||
BOOL success = [[UIApplication sharedApplication] openURL:douyinURL];
|
||
if (completion) {
|
||
completion(success);
|
||
}
|
||
}
|
||
} else {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
}
|
||
}
|
||
|
||
+ (void)requestPhotoLibraryAuthorizationWithCompletion:(void (^)(BOOL granted))completion {
|
||
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
|
||
|
||
switch (status) {
|
||
case PHAuthorizationStatusAuthorized: {
|
||
if (completion) {
|
||
completion(YES);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case PHAuthorizationStatusNotDetermined: {
|
||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
if (completion) {
|
||
completion(newStatus == PHAuthorizationStatusAuthorized);
|
||
}
|
||
});
|
||
}];
|
||
break;
|
||
}
|
||
|
||
case PHAuthorizationStatusDenied:
|
||
case PHAuthorizationStatusRestricted: {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
break;
|
||
}
|
||
|
||
default: {
|
||
if (completion) {
|
||
completion(NO);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
+ (NSString *)encodeString:(NSString *)string {
|
||
if (!string) return @"";
|
||
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||
}
|
||
|
||
+ (NSString *)getUrlParam:(NSString *)url paramName:(NSString *)name {
|
||
NSError *error;
|
||
NSString *regTags = [[NSString alloc] initWithFormat:@"(^|&|\\?)%@=([^&]*)(&|$)", name];
|
||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regTags options:NSRegularExpressionCaseInsensitive error:&error];
|
||
|
||
NSTextCheckingResult *match = [regex firstMatchInString:url options:0 range:NSMakeRange(0, [url length])];
|
||
if (match) {
|
||
NSString *paramValue = [url substringWithRange:[match rangeAtIndex:2]];
|
||
return paramValue;
|
||
}
|
||
|
||
return nil;
|
||
}
|
||
|
||
+ (void)showAppNotInstalledAlert:(NSString *)appName {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@未安装", appName]
|
||
message:[NSString stringWithFormat:@"您的设备未安装%@客户端,无法进行分享", appName]
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
|
||
[alertController addAction:okAction];
|
||
|
||
UIViewController *topVC = [self topViewController];
|
||
[topVC presentViewController:alertController animated:YES completion:nil];
|
||
});
|
||
}
|
||
|
||
+ (void)showPhotoLibraryPermissionAlert {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"无法访问相册"
|
||
message:@"请在设置中开启相册权限,以便分享到抖音"
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
|
||
UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:@"去设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
|
||
}];
|
||
|
||
[alertController addAction:cancelAction];
|
||
[alertController addAction:settingsAction];
|
||
|
||
UIViewController *topVC = [self topViewController];
|
||
[topVC presentViewController:alertController animated:YES completion:nil];
|
||
});
|
||
}
|
||
|
||
+ (UIViewController *)topViewController {
|
||
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||
while (rootViewController.presentedViewController) {
|
||
rootViewController = rootViewController.presentedViewController;
|
||
}
|
||
|
||
if ([rootViewController isKindOfClass:[UINavigationController class]]) {
|
||
UINavigationController *navigationController = (UINavigationController *)rootViewController;
|
||
return navigationController.visibleViewController;
|
||
}
|
||
|
||
if ([rootViewController isKindOfClass:[UITabBarController class]]) {
|
||
UITabBarController *tabBarController = (UITabBarController *)rootViewController;
|
||
UIViewController *selectedViewController = tabBarController.selectedViewController;
|
||
|
||
if ([selectedViewController isKindOfClass:[UINavigationController class]]) {
|
||
UINavigationController *navigationController = (UINavigationController *)selectedViewController;
|
||
return navigationController.visibleViewController;
|
||
}
|
||
|
||
return selectedViewController;
|
||
}
|
||
|
||
return rootViewController;
|
||
}
|
||
|
||
+ (void)showImageShareGuidance {
|
||
NSString *title = @"抖音分享准备完成";
|
||
NSString *message = @"✅ 图片已复制到剪贴板\n\n"
|
||
@"🎯 分享到好友或群聊步骤:\n"
|
||
@"1️⃣ 打开抖音,点击右下角「消息」\n"
|
||
@"2️⃣ 选择好友或群聊进入聊天\n"
|
||
@"3️⃣ 在输入框长按粘贴图片\n"
|
||
@"4️⃣ 点击发送即可分享";
|
||
|
||
[self showGuidanceAlertWithTitle:title message:message];
|
||
}
|
||
|
||
+ (void)showTextShareGuidance {
|
||
NSString *title = @"抖音分享准备完成";
|
||
NSString *message = @"✅ 内容已复制到剪贴板\n\n"
|
||
@"🎯 分享到好友或群聊步骤:\n"
|
||
@"1️⃣ 打开抖音,点击右下角「消息」\n"
|
||
@"2️⃣ 选择好友或群聊进入聊天\n"
|
||
@"3️⃣ 在输入框长按粘贴内容\n"
|
||
@"4️⃣ 点击发送即可分享";
|
||
|
||
[self showGuidanceAlertWithTitle:title message:message];
|
||
}
|
||
|
||
+ (void)showGuidanceAlertWithTitle:(NSString *)title message:(NSString *)message {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
||
message:message
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
UIAlertAction *openAction = [UIAlertAction actionWithTitle:@"立即打开抖音"
|
||
style:UIAlertActionStyleDefault
|
||
handler:^(UIAlertAction * _Nonnull action) {
|
||
NSURL *douyinURL = [NSURL URLWithString:kDouyinScheme];
|
||
if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) {
|
||
[[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:nil];
|
||
}
|
||
}];
|
||
|
||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"稍后分享"
|
||
style:UIAlertActionStyleCancel
|
||
handler:nil];
|
||
|
||
[alert addAction:openAction];
|
||
[alert addAction:cancelAction];
|
||
|
||
// 获取顶层视图控制器
|
||
UIViewController *topVC = [self getTopViewController];
|
||
if (topVC) {
|
||
[topVC presentViewController:alert animated:YES completion:nil];
|
||
}
|
||
});
|
||
}
|
||
|
||
+ (UIViewController *)getTopViewController {
|
||
UIViewController *topViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||
while (topViewController.presentedViewController) {
|
||
topViewController = topViewController.presentedViewController;
|
||
}
|
||
return topViewController;
|
||
}
|
||
|
||
@end
|