Files
youle_app_ios/msext/Class/Utils/DouyinShareManager.m
joywayer 2296c65974 feat: 重构分享功能 - 将各平台分享逻辑整理到对应Manager中
- 新增 WechatShareManager:封装微信分享逻辑,支持ShareContent对象
- 增强 DouyinShareManager:新增ShareContent支持和分享引导功能
- 优化 QQShareManager:支持截图/纯文本分享类型自动识别
- 重构 SharePanel:简化为UI调度层,移除具体分享实现
- 实现职责分离:各Manager专注自己的平台分享逻辑
- 提升可维护性:修改某平台不影响其他平台
- 增强可扩展性:新增分享平台更容易实现
2025-06-17 19:55:44 +08:00

596 lines
22 KiB
Objective-C
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
//
// 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