feat: 重构分享功能 - 将各平台分享逻辑整理到对应Manager中

- 新增 WechatShareManager:封装微信分享逻辑,支持ShareContent对象
- 增强 DouyinShareManager:新增ShareContent支持和分享引导功能
- 优化 QQShareManager:支持截图/纯文本分享类型自动识别
- 重构 SharePanel:简化为UI调度层,移除具体分享实现
- 实现职责分离:各Manager专注自己的平台分享逻辑
- 提升可维护性:修改某平台不影响其他平台
- 增强可扩展性:新增分享平台更容易实现
This commit is contained in:
joywayer
2025-06-17 19:55:44 +08:00
parent 1666d6cf24
commit 2296c65974
13 changed files with 3730 additions and 2 deletions

View File

@@ -20,6 +20,7 @@
#import "RNCachingURLProtocol.h"
#import "JANALYTICSService.h"
#import "XianliaoApiManager.h"
#import "QQShareManager.h"
@interface AppDelegate ()<BuglyDelegate>
{
BOOL flag;
@@ -31,12 +32,33 @@
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
// QQ
if ([QQShareManager handleOpenURL:url]) {
return YES;
}
//
return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
// QQ
if ([QQShareManager handleOpenURL:url]) {
return YES;
}
//
return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
}
// iOS 9+ URL
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
// QQ
if ([QQShareManager handleOpenURL:url]) {
return YES;
}
//
return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
}
- (void)configureAPIKey
{
if ([APIKey length] == 0)

View File

@@ -45,6 +45,8 @@
#import "XianliaoApiManager.h"
#import "QiniuManager.h"
#import "QiniuConfig.h"
#import "SharePanel.h"
@interface gameController ()
<WKNavigationDelegate,WXApiManagerDelegate,VoiceRecorderBaseVCDelegate,AVAudioPlayerDelegate,ASIHTTPRequestDelegate,AMapLocationManagerDelegate,UIActionSheetDelegate,AgoraRtcEngineDelegate>
{
@@ -578,6 +580,15 @@
NSLog(@"%@",two);
enum WXScene currentScene;
int friend = [one intValue];
// ======
//
[SharePanel showWithDictionary:data completion:^(ShareType type, BOOL success) {
//
}];
return;
if(friend==1)
{
currentScene=WXSceneSession;

View File

@@ -0,0 +1,74 @@
//
// DouyinShareManager.h
// msext
//
// Created on 2025/06/15.
// Copyright © 2025年. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, DouyinShareType) {
DouyinShareTypeImage, // 图片分享
DouyinShareTypeVideo, // 视频分享
DouyinShareTypeHashtag, // 话题挑战分享
DouyinShareTypeOpenProfile // 打开个人主页
};
@interface DouyinShareManager : NSObject
/**
* 检查抖音是否已安装
* @return BOOL 是否已安装抖音
*/
+ (BOOL)isDouyinInstalled;
/**
* 分享到抖音
* @param type 分享类型
* @param hashtagName 话题名称(仅用于话题挑战类型)
* @param image 图片(仅用于图片分享类型)
* @param videoUrl 视频本地URL仅用于视频分享类型
* @param userId 用户ID仅用于打开个人主页类型
* @param completion 完成回调
*/
+ (void)shareToDouyin:(DouyinShareType)type
hashtagName:(NSString * _Nullable)hashtagName
image:(UIImage * _Nullable)image
videoUrl:(NSURL * _Nullable)videoUrl
userId:(NSString * _Nullable)userId
completion:(void(^_Nullable)(BOOL success))completion;
/**
* 处理从抖音返回的URL
* 需要在 AppDelegate 的 application:openURL:options: 方法中调用
*/
+ (BOOL)handleOpenURL:(NSURL *)url;
/**
* 使用ShareContent对象进行抖音分享
* 支持根据type字段自动选择分享类型
* - type=2截图分享复制图片到剪贴板+引导)
* - 其他:文本分享(复制文本到剪贴板+引导)
* @param shareContent 完整的分享内容对象包含title、desc、type字段
* @param completion 分享完成回调
*/
+ (void)shareWithContent:(id)shareContent
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 显示抖音图片分享引导弹窗
*/
+ (void)showImageShareGuidance;
/**
* 显示抖音文本分享引导弹窗
*/
+ (void)showTextShareGuidance;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,595 @@
//
// 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;
}
// KVCShareContent
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

View File

@@ -0,0 +1,222 @@
//
// QQAppIDValidator.m
// msext
//
// Created on 2025/06/16.
// Copyright © 2025. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "QQShareManager.h"
/**
* QQ AppID
* SDKQQ
*/
@interface QQAppIDValidator : NSObject
/**
* QQ
* AppIDURL Schemes
*/
+ (void)performCompleteValidation;
/**
*
*/
+ (void)showValidationResults;
/**
* QQURL
*/
+ (NSString *)generateTestQQShareURL;
@end
@implementation QQAppIDValidator
+ (void)performCompleteValidation {
NSLog(@"\n🔍 开始QQ分享配置验证...\n");
// 1. QQ
BOOL isQQInstalled = [QQShareManager isQQInstalled];
NSLog(@"1⃣ QQ安装检查: %@", isQQInstalled ? @"✅ 已安装" : @"❌ 未安装");
// 2. AppID
BOOL isAppIDValid = [QQShareManager validateQQAppIDConfiguration];
NSString *appID = [QQShareManager getCurrentQQAppID];
NSLog(@"2⃣ AppID配置检查: %@", isAppIDValid ? [NSString stringWithFormat:@"✅ 有效 (%@)", appID] : @"❌ 无效");
// 3. URL Schemes
[self validateURLSchemes];
// 4.
[self validateCallbackConfiguration];
// 5. URL
NSString *testURL = [self generateTestQQShareURL];
NSLog(@"5⃣ 测试分享URL: %@", testURL);
NSLog(@"\n✅ QQ分享配置验证完成\n");
}
+ (void)validateURLSchemes {
NSArray *requiredSchemes = @[@"mqqapi", @"mqq", @"mqqOpensdkSSoLogin", @"mqqopensdkapiV2", @"mqqopensdkapiV3", @"mqqopensdkapiV4", @"wtloginmqq2", @"mqzone"];
BOOL allSchemesConfigured = YES;
NSMutableArray *missingSchemes = [NSMutableArray array];
for (NSString *scheme in requiredSchemes) {
NSString *testURLString = [NSString stringWithFormat:@"%@://test", scheme];
NSURL *testURL = [NSURL URLWithString:testURLString];
if (![[UIApplication sharedApplication] canOpenURL:testURL]) {
allSchemesConfigured = NO;
[missingSchemes addObject:scheme];
}
}
if (allSchemesConfigured) {
NSLog(@"3⃣ URL Schemes配置: ✅ 完整");
} else {
NSLog(@"3⃣ URL Schemes配置: ❌ 缺少 %@", [missingSchemes componentsJoinedByString:@", "]);
NSLog(@" 请在Info.plist的LSApplicationQueriesSchemes中添加缺少的schemes");
}
}
+ (void)validateCallbackConfiguration {
// msextscheme
NSURL *callbackURL = [NSURL URLWithString:@"msext://test"];
NSBundle *mainBundle = [NSBundle mainBundle];
NSDictionary *infoPlist = [mainBundle infoDictionary];
NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"];
BOOL callbackConfigured = NO;
for (NSDictionary *urlType in urlTypes) {
NSArray *schemes = urlType[@"CFBundleURLSchemes"];
if ([schemes containsObject:@"msext"]) {
callbackConfigured = YES;
break;
}
}
NSLog(@"4⃣ 回调配置检查: %@", callbackConfigured ? @"✅ 正确" : @"❌ 缺失msext scheme");
if (!callbackConfigured) {
NSLog(@" 请在Info.plist的CFBundleURLTypes中添加msext scheme配置");
}
}
+ (NSString *)generateTestQQShareURL {
NSString *appID = [QQShareManager getCurrentQQAppID];
if (!appID) {
return @"❌ 无法生成测试URLAppID未配置";
}
// QQURL
NSMutableString *testURL = [NSMutableString stringWithString:@"mqqapi://share/to_fri?"];
[testURL appendString:@"version=1"];
[testURL appendString:@"&cflag=0"];
[testURL appendString:@"&src_type=app"];
[testURL appendString:@"&sdkv=2.9.0"];
[testURL appendString:@"&sdkp=i"];
[testURL appendFormat:@"&appid=%@", appID];
[testURL appendString:@"&app_name=进贤聚友棋牌"];
[testURL appendString:@"&callback_type=scheme"];
[testURL appendString:@"&callback_name=msext"];
[testURL appendString:@"&req_type=0"];
[testURL appendString:@"&title=测试标题"];
[testURL appendString:@"&description=测试描述"];
return testURL;
}
+ (void)showValidationResults {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"QQ分享配置验证"
message:@"请查看控制台日志以获取详细的验证结果"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *validateAction = [UIAlertAction actionWithTitle:@"重新验证"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self performCompleteValidation];
}];
UIAlertAction *testAction = [UIAlertAction actionWithTitle:@"测试分享"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self performTestShare];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"关闭"
style:UIAlertActionStyleCancel
handler:nil];
[alert addAction:validateAction];
[alert addAction:testAction];
[alert addAction:cancelAction];
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *topVC = [self topViewController:rootVC];
[topVC presentViewController:alert animated:YES completion:nil];
});
}
+ (void)performTestShare {
NSLog(@"🧪 开始QQ分享测试...");
[QQShareManager shareToQQFriend:QQShareTypeText
title:@"QQ分享配置测试"
description:@"如果您看到这条消息说明QQ分享配置正确"
thumbImage:nil
url:nil
image:nil
completion:^(BOOL success) {
NSLog(@"🧪 QQ分享测试结果: %@", success ? @"✅ 成功" : @"❌ 失败");
dispatch_async(dispatch_get_main_queue(), ^{
NSString *message = success ? @"QQ分享测试成功配置正确。" : @"QQ分享测试失败请检查配置。";
UIAlertController *resultAlert = [UIAlertController alertControllerWithTitle:@"测试结果"
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[resultAlert addAction:okAction];
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *topVC = [self topViewController:rootVC];
[topVC presentViewController:resultAlert animated:YES completion:nil];
});
}];
}
+ (UIViewController *)topViewController:(UIViewController *)rootViewController {
if (rootViewController.presentedViewController == nil) {
return rootViewController;
}
if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
return [self topViewController:lastViewController];
}
if ([rootViewController.presentedViewController isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabBarController = (UITabBarController *)rootViewController.presentedViewController;
UIViewController *selectedViewController = tabBarController.selectedViewController;
return [self topViewController:selectedViewController];
}
return [self topViewController:rootViewController.presentedViewController];
}
@end
// 便
void validateQQShareConfiguration(void) {
[QQAppIDValidator performCompleteValidation];
}
void showQQShareValidationDialog(void) {
[QQAppIDValidator showValidationResults];
}

View File

@@ -0,0 +1,179 @@
//
// QQShareManager.h
// msext
//
// Created on 2025/06/15.
// Copyright © 2025年. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, QQShareType) {
QQShareTypeText, // 纯文本分享
QQShareTypeImage, // 图片分享
QQShareTypeNews, // 新闻/网页分享
QQShareTypeAudio, // 音频分享
QQShareTypeVideo // 视频分享
};
@interface QQShareManager : NSObject
/**
* 检查QQ是否已安装
* @return BOOL 是否已安装QQ
*/
+ (BOOL)isQQInstalled;
/**
* 验证QQ AppID配置是否正确
* @return BOOL AppID是否有效配置
*/
+ (BOOL)validateQQAppIDConfiguration;
/**
* 获取当前配置的QQ AppID
* @return NSString 当前的QQ AppID如果未配置返回nil
*/
+ (NSString * _Nullable)getCurrentQQAppID;
/**
* 分享到QQ好友
* @param type 分享类型
* @param title 标题
* @param description 描述
* @param thumbImage 缩略图
* @param url 链接URL
* @param image 图片(仅图片分享时使用)
* @param completion 完成回调
*/
+ (void)shareToQQFriend:(QQShareType)type
title:(NSString * _Nullable)title
description:(NSString * _Nullable)description
thumbImage:(UIImage * _Nullable)thumbImage
url:(NSString * _Nullable)url
image:(UIImage * _Nullable)image
completion:(void(^_Nullable)(BOOL success))completion;
/**
* 分享到QQ空间
* @param type 分享类型
* @param title 标题
* @param description 描述
* @param thumbImage 缩略图
* @param url 链接URL
* @param images 图片数组(可多张图片)
* @param completion 完成回调
*/
+ (void)shareToQZone:(QQShareType)type
title:(NSString * _Nullable)title
description:(NSString * _Nullable)description
thumbImage:(UIImage * _Nullable)thumbImage
url:(NSString * _Nullable)url
images:(NSArray<UIImage *> * _Nullable)images
completion:(void(^_Nullable)(BOOL success))completion;
/**
* 简化版QQ分享到好友解决900101错误
* 使用最基础的参数格式,避免复杂参数导致的错误
* @param title 标题
* @param description 描述
* @param url 链接URL可为nil
* @param completion 完成回调
*/
+ (void)simpleShareToQQFriend:(NSString * _Nullable)title
description:(NSString * _Nullable)description
url:(NSString * _Nullable)url
completion:(void(^_Nullable)(BOOL success))completion;
/**
* 使用系统分享面板进行QQ分享最可靠的方案
* @param title 分享标题
* @param description 分享描述
* @param url 分享链接
* @param viewController 用于呈现分享面板的视图控制器
* @param completion 分享完成回调
*/
+ (void)shareWithSystemShare:(NSString * _Nullable)title
description:(NSString * _Nullable)description
url:(NSString * _Nullable)url
fromViewController:(UIViewController *)viewController
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 使用系统分享面板进行QQ分享自动获取顶层视图控制器
* @param title 分享标题
* @param description 分享描述
* @param url 分享链接
* @param completion 分享完成回调
*/
+ (void)shareWithSystemShareAuto:(NSString * _Nullable)title
description:(NSString * _Nullable)description
url:(NSString * _Nullable)url
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 尝试强制弹出QQ会话选择列表
* 使用多种URL格式尝试打开QQ的好友选择界面
* @param completion 完成回调
*/
+ (void)forceOpenQQSessionSelector:(void(^_Nullable)(BOOL success))completion;
/**
* 使用多种方式尝试QQ分享
* 按优先级尝试不同的QQ分享方式直到找到有效的
* @param title 标题
* @param description 描述
* @param completion 完成回调
*/
+ (void)tryMultipleQQShareMethods:(NSString * _Nullable)title
description:(NSString * _Nullable)description
completion:(void(^_Nullable)(BOOL success))completion;
/**
* 处理从QQ返回的URL
* 需要在 AppDelegate 的 application:openURL:options: 方法中调用
*/
+ (BOOL)handleOpenURL:(NSURL *)url;
/**
* 验证和修复URL格式
* @param url 原始URL字符串
* @return 修复后的有效URL如果无法修复返回nil
*/
+ (NSString * _Nullable)validateAndFixURL:(NSString * _Nullable)url;
/**
* 获取应用启动图用作分享缩略图已弃用建议使用getAppIcon
* @return 启动图UIImage对象
*/
+ (UIImage * _Nullable)getAppLaunchImage;
/**
* 获取应用桌面图标用作分享缩略图
* @return 应用桌面图标UIImage对象
*/
+ (UIImage * _Nullable)getAppIcon;
/**
* 检查是否可以安全呈现系统分享面板
* @return BOOL 是否可以安全呈现
*/
+ (BOOL)canSafelyPresentSharePanel;
/**
* 使用系统分享面板进行QQ分享传递完整ShareContent对象
* 支持根据type字段自动选择分享类型
* - type=2截图分享包含屏幕截图+文字说明)
* - 其他:纯文本分享(文字+链接+应用图标)
* @param shareContent 完整的分享内容对象需包含title、desc、webpageUrl、type字段
* @param completion 分享完成回调
*/
+ (void)shareWithSystemShareContent:(id)shareContent
completion:(void (^ _Nullable)(BOOL success))completion;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
//
// SharePanel.h
// msext
//
// Created on 2025/06/15.
// Copyright © 2025年. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// 分享类型枚举
typedef NS_ENUM(NSInteger, ShareType) {
ShareTypeWeChat, // 微信分享
ShareTypeQQ, // QQ分享
ShareTypeDouyin // 抖音分享
};
// 分享内容模型
@interface ShareContent : NSObject
@property (nonatomic, copy) NSString *title; // 标题
@property (nonatomic, copy) NSString *desc; // 描述 (原名 description 与 NSObject 方法冲突)
@property (nonatomic, copy) NSString *webpageUrl; // 链接
@property (nonatomic, copy) NSString *sharefriend; // 好友分享,朋友圈分享
@property (nonatomic, copy) NSString *type; // 好友分享,朋友圈分享
@end
// 分享面板回调
typedef void (^SharePanelCompletionBlock)(ShareType type, BOOL success);
@interface SharePanel : UIView
/**
* 显示分享面板
* @param content 分享内容
* @param completion 分享完成后的回调
*/
+ (void)showWithContent:(ShareContent *)content
completion:(nullable SharePanelCompletionBlock)completion;
/**
* 显示分享面板(使用字典数据)
* @param data 包含分享内容的字典
* @param completion 分享完成后的回调
*
* 字典key说明:
* - title: 标题
* - description: 描述
* - link: 链接URL
* - thumbImage: 缩略图 (UIImage对象)
* - image: 分享图片 (UIImage对象, 仅图片分享时使用)
* - videoPath: 视频路径 (仅视频分享时使用)
*/
+ (void)showWithDictionary:(NSDictionary *)data
completion:(nullable SharePanelCompletionBlock)completion;
/**
* 隐藏分享面板
*/
+ (void)dismiss;
/**
* QQ分享到好友
* @param content 分享内容
* @param completion 分享完成后的回调
*/
+ (void)shareToQQFriend:(ShareContent *)content
completion:(nullable SharePanelCompletionBlock)completion;
/**
* QQ分享到空间
* @param content 分享内容
* @param completion 分享完成后的回调
*/
+ (void)shareToQZone:(ShareContent *)content
completion:(nullable SharePanelCompletionBlock)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,662 @@
//
// SharePanel.m
// msext
//
// Created on 2025/06/15.
// Copyright © 2025. All rights reserved.
//
#import "SharePanel.h"
#import "WechatShareManager.h" //
#import "QQShareManager.h" // QQ
#import "DouyinShareManager.h" //
//
#define kPanelHeight 180.0f
#define kButtonSize 60.0f
#define kButtonMargin 30.0f
#define kButtonTopMargin 40.0f
#define kAnimationDuration 0.25f
@interface SharePanel()
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIButton *wechatButton;
@property (nonatomic, strong) UIButton *qqButton;
@property (nonatomic, strong) UIButton *douyinButton;
@property (nonatomic, strong) UILabel *wechatLabel;
@property (nonatomic, strong) UILabel *qqLabel;
@property (nonatomic, strong) UILabel *douyinLabel;
@property (nonatomic, strong) ShareContent *shareContent;
@property (nonatomic, copy) SharePanelCompletionBlock completionBlock;
//
- (UIViewController *)getTopViewController;
@end
static SharePanel *sharedPanel = nil;
@implementation ShareContent
@end
@implementation SharePanel
#pragma mark - Lifecycle
+ (instancetype)sharedPanel {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPanel = [[self alloc] initWithFrame:CGRectZero];
});
return sharedPanel;
}
- (instancetype)initWithFrame:(CGRect)frame {
CGRect screenBounds = [UIScreen mainScreen].bounds;
self = [super initWithFrame:screenBounds];
if (self) {
//
self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
//
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundTap:)];
[self addGestureRecognizer:tapGesture];
//
_contentView = [[UIView alloc] initWithFrame:CGRectMake(0, screenBounds.size.height, screenBounds.size.width, kPanelHeight)];
_contentView.backgroundColor = [UIColor whiteColor];
//
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_contentView.bounds
byRoundingCorners:UIRectCornerTopLeft|UIRectCornerTopRight
cornerRadii:CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = _contentView.bounds;
maskLayer.path = maskPath.CGPath;
_contentView.layer.mask = maskLayer;
[self addSubview:_contentView];
//
[self setupShareButtons];
}
return self;
}
#pragma mark - UI Setup
- (void)setupShareButtons {
CGFloat contentWidth = self.contentView.frame.size.width;
CGFloat startX = (contentWidth - (3 * kButtonSize + 2 * kButtonMargin)) / 2;
//
_wechatButton = [self createButtonWithImage:@"shareWechat" atIndex:0 startX:startX];
//
[_wechatButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside];
_wechatButton.tag = ShareTypeWeChat;
[self.contentView addSubview:_wechatButton];
//
_wechatLabel = [self createLabelWithText:@"微信" forButton:_wechatButton];
[self.contentView addSubview:_wechatLabel];
// QQ
_qqButton = [self createButtonWithImage:@"shareQQ" atIndex:1 startX:startX];
// QQ
[_qqButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside];
_qqButton.tag = ShareTypeQQ;
[self.contentView addSubview:_qqButton];
// QQ
_qqLabel = [self createLabelWithText:@"QQ" forButton:_qqButton];
[self.contentView addSubview:_qqLabel];
//
_douyinButton = [self createButtonWithImage:@"shareDouyin" atIndex:2 startX:startX];
//
[_douyinButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside];
_douyinButton.tag = ShareTypeDouyin;
[self.contentView addSubview:_douyinButton];
//
_douyinLabel = [self createLabelWithText:@"抖音" forButton:_douyinButton];
[self.contentView addSubview:_douyinLabel];
//
UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
cancelButton.frame = CGRectMake(0, kPanelHeight - 40, contentWidth, 40);
[cancelButton setTitle:@"取消" forState:UIControlStateNormal];
[cancelButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
cancelButton.titleLabel.font = [UIFont systemFontOfSize:16];
[cancelButton addTarget:self action:@selector(cancelAction) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:cancelButton];
// 线
UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, kPanelHeight - 41, contentWidth, 1)];
lineView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
[self.contentView addSubview:lineView];
}
- (UIButton *)createButtonWithImage:(NSString *)imageName atIndex:(NSInteger)index startX:(CGFloat)startX {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
CGFloat x = startX + index * (kButtonSize + kButtonMargin);
button.frame = CGRectMake(x, kButtonTopMargin, kButtonSize, kButtonSize);
[button setImage:[UIImage imageNamed:imageName] forState:UIControlStateNormal];
button.layer.cornerRadius = kButtonSize / 2;
button.clipsToBounds = YES;
return button;
}
- (UILabel *)createLabelWithText:(NSString *)text forButton:(UIButton *)button {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kButtonSize, 20)];
label.center = CGPointMake(button.center.x, button.frame.origin.y + button.frame.size.height + 15);
label.text = text;
label.textColor = [UIColor darkGrayColor];
label.font = [UIFont systemFontOfSize:12];
label.textAlignment = NSTextAlignmentCenter;
return label;
}
#pragma mark - Public Methods
+ (void)showWithContent:(ShareContent *)content completion:(SharePanelCompletionBlock)completion {
SharePanel *panel = [SharePanel sharedPanel];
panel.shareContent = content;
panel.completionBlock = completion;
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:panel];
[UIView animateWithDuration:kAnimationDuration animations:^{
CGRect frame = panel.contentView.frame;
frame.origin.y = [UIScreen mainScreen].bounds.size.height - kPanelHeight;
panel.contentView.frame = frame;
panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
}];
}
+ (void)showWithDictionary:(NSDictionary *)data completion:(SharePanelCompletionBlock)completion {
// ShareContent
ShareContent *content = [[ShareContent alloc] init];
// objectForKey
// content.title = data[@"title"];//
// content.desc = data[@"description"]; // desc
// content.webpageUrl = data[@"webpageUrl"];//
// content.type = data[@"type"];// 1-/ 2-
// content.sharefriend = data[@"sharefriend"];//
content.title = [data objectForKey:@"title"];//
content.desc = [data objectForKey:@"description"]; // desc
content.webpageUrl = [data objectForKey:@"webpageUrl"];//
content.type = [data objectForKey:@"type"];// 1-/ 2-
content.sharefriend = [data objectForKey: @"sharefriend"];//
// 使
[self showWithContent:content completion:completion];
}
+ (void)dismiss {
SharePanel *panel = [SharePanel sharedPanel];
//[panel removeFromSuperview];
[UIView animateWithDuration:kAnimationDuration animations:^{
CGRect frame = panel.contentView.frame;
frame.origin.y = [UIScreen mainScreen].bounds.size.height;
panel.contentView.frame = frame;
panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
} completion:^(BOOL finished) {
[panel removeFromSuperview];
}];
}
+ (void)dismissWithCompletion:(void (^)(void))completion {
SharePanel *panel = [SharePanel sharedPanel];
[UIView animateWithDuration:kAnimationDuration animations:^{
CGRect frame = panel.contentView.frame;
frame.origin.y = [UIScreen mainScreen].bounds.size.height;
panel.contentView.frame = frame;
panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
} completion:^(BOOL finished) {
[panel removeFromSuperview];
if (completion) {
completion();
}
}];
}
#pragma mark - Actions
- (void)handleBackgroundTap:(UITapGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self];
if (!CGRectContainsPoint(self.contentView.frame, point)) {
[SharePanel dismiss];
}
}
- (void)cancelAction {
[SharePanel dismiss];
}
- (void)wechatShareAction {
[self handleWechatShareAction];
}
- (void)qqShareAction {
[self handleQQShareAction];
}
- (void)douyinShareAction {
[self handleDouyinShareAction];
}
#pragma mark - Button Action Handlers
- (void)handleButtonClick:(UIButton *)sender {
ShareType shareType = (ShareType)sender.tag;
//
__weak typeof(self) weakSelf = self;
[SharePanel dismissWithCompletion:^{
switch (shareType) {
case ShareTypeWeChat:
[weakSelf handleWechatShareAction];
break;
case ShareTypeQQ:
[weakSelf handleQQShareAction];
break;
case ShareTypeDouyin:
[weakSelf handleDouyinShareAction];
break;
default:
break;
}
}];
}
- (void)handleWechatShareAction {
NSLog(@"🔍 [SharePanel] 处理微信分享");
ShareContent *content = self.shareContent;
SharePanelCompletionBlock completionBlock = self.completionBlock;
// 使WechatShareManager
[WechatShareManager shareWithContent:content
completion:^(BOOL success) {
NSLog(@"微信分享结果: %@", success ? @"✅ 成功" : @"❌ 失败");
if (completionBlock) {
completionBlock(ShareTypeWeChat, success);
}
}];
}
- (void)handleQQShareAction {
NSLog(@"🔍 [SharePanel] 处理QQ分享");
ShareContent *content = self.shareContent;
SharePanelCompletionBlock completionBlock = self.completionBlock;
// 使QQShareManagerQQ
[QQShareManager shareWithSystemShareContent:content
completion:^(BOOL success) {
NSLog(@"QQ分享结果: %@", success ? @"✅ 成功" : @"❌ 失败");
if (completionBlock) {
completionBlock(ShareTypeQQ, success);
}
}];
}
- (void)handleDouyinShareAction {
NSLog(@"🔍 [SharePanel] 处理抖音分享");
ShareContent *content = self.shareContent;
SharePanelCompletionBlock completionBlock = self.completionBlock;
// 使DouyinShareManager
[DouyinShareManager shareWithContent:content
completion:^(BOOL success) {
NSLog(@"抖音分享结果: %@", success ? @"✅ 成功" : @"❌ 失败");
if (completionBlock) {
completionBlock(ShareTypeDouyin, success);
}
}];
}
#pragma mark - Helper Methods
- (void)showErrorAlertWithMessage:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"分享失败"
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:okAction];
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *topVC = [self topViewController:rootVC];
[topVC presentViewController:alert animated:YES completion:nil];
}
- (UIViewController *)topViewController:(UIViewController *)rootViewController {
if (rootViewController.presentedViewController == nil) {
return rootViewController;
}
if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
return [self topViewController:lastViewController];
}
if ([rootViewController.presentedViewController isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabBarController = (UITabBarController *)rootViewController.presentedViewController;
UIViewController *selectedViewController = tabBarController.selectedViewController;
return [self topViewController:selectedViewController];
}
return [self topViewController:rootViewController.presentedViewController];
}
- (UIImage *)getAppIconImage {
// Bundle
NSBundle *mainBundle = [NSBundle mainBundle];
//
NSDictionary *infoDictionary = [mainBundle infoDictionary];
NSArray *iconFiles = infoDictionary[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"];
//
if (iconFiles && [iconFiles count] > 0) {
// 使
NSString *iconLastName = [iconFiles lastObject];
UIImage *image = [UIImage imageNamed:iconLastName];
return image;
}
// 使
NSString *appIconName = infoDictionary[@"CFBundleIconFile"];
if (appIconName) {
return [UIImage imageNamed:appIconName];
}
// 使
UIImage *appIcon = [UIImage imageNamed:@"AppIcon"];
if (appIcon) {
return appIcon;
}
// 1x1
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
UIImage *blankImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return blankImage;
}
//
- (void)showCopySuccessAlert {
// UIWindow
UIWindow *toastWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
toastWindow.windowLevel = UIWindowLevelStatusBar + 100; //
toastWindow.backgroundColor = [UIColor clearColor];
//
__block UIViewController *rootVC = [[UIViewController alloc] init];
rootVC.view.backgroundColor = [UIColor clearColor];
toastWindow.rootViewController = rootVC;
//
UIView *toastView = [[UIView alloc] initWithFrame:CGRectMake(0, -60, toastWindow.bounds.size.width - 40, 60)];
toastView.center = CGPointMake(CGRectGetMidX(toastWindow.bounds), toastView.center.y);
toastView.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8];
toastView.layer.cornerRadius = 10;
toastView.alpha = 0;
//
UIImageView *iconView = [[UIImageView alloc] initWithFrame:CGRectMake(15, 15, 30, 30)];
UIImage *checkmarkImage = [self createCheckmarkImage];
iconView.image = checkmarkImage;
iconView.contentMode = UIViewContentModeScaleAspectFit;
[toastView addSubview:iconView];
//
UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(55, 0, toastView.bounds.size.width - 70, toastView.bounds.size.height)];
messageLabel.text = @"内容已复制到剪贴板,请在抖音中粘贴";
messageLabel.textColor = [UIColor whiteColor];
messageLabel.textAlignment = NSTextAlignmentLeft;
messageLabel.numberOfLines = 2;
messageLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
[toastView addSubview:messageLabel];
// toastwindow
[rootVC.view addSubview:toastView];
//
[toastWindow makeKeyAndVisible];
//
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
toastView.frame = CGRectOffset(toastView.frame, 0, 80 + [self safeAreaInsets].top);
toastView.alpha = 1.0;
} completion:nil];
// 2
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
toastView.frame = CGRectOffset(toastView.frame, 0, -80 - [self safeAreaInsets].top);
toastView.alpha = 0.0;
} completion:^(BOOL finished) {
//
toastWindow.hidden = YES;
// nil
rootVC = nil;
}];
});
}
//
- (UIImage *)createCheckmarkImage {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(30, 30), NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
//
CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.2 green:0.8 blue:0.4 alpha:1.0].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0, 0, 30, 30));
//
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 2.5);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
//
CGContextMoveToPoint(context, 8, 15);
CGContextAddLineToPoint(context, 13, 20);
CGContextAddLineToPoint(context, 22, 10);
CGContextStrokePath(context);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
//
- (UIEdgeInsets)safeAreaInsets {
if (@available(iOS 11.0, *)) {
return [UIApplication sharedApplication].keyWindow.safeAreaInsets;
}
return UIEdgeInsetsZero;
}
// URL
- (NSString *)encodeUrlString:(NSString *)string {
if (!string) return @"";
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
}
#pragma mark - QQ
+ (void)shareToQQFriend:(ShareContent *)content
completion:(SharePanelCompletionBlock)completion {
if (![QQShareManager isQQInstalled]) {
[self showAlertWithMessage:@"您的设备未安装QQ客户端无法进行QQ分享"];
if (completion) {
completion(ShareTypeQQ, NO);
}
return;
}
//
NSString *title = content.title ?: @"游戏分享";
NSString *description = content.desc ?: @"精彩游戏分享";
NSString *shareUrl = content.webpageUrl;
//
UIImage *thumbImage = [UIImage imageNamed:@"sharelogo"] ?: [UIImage imageNamed:@"Icon180"];
//
QQShareType shareType = shareUrl ? QQShareTypeNews : QQShareTypeText;
// 使QQShareManager
if (shareType == QQShareTypeText) {
//
[QQShareManager shareToQQFriend:QQShareTypeText
title:title
description:description
thumbImage:nil
url:nil
image:nil
completion:^(BOOL success) {
if (completion) {
completion(ShareTypeQQ, success);
}
}];
} else {
//
[QQShareManager shareToQQFriend:QQShareTypeNews
title:title
description:description
thumbImage:thumbImage
url:shareUrl
image:nil
completion:^(BOOL success) {
if (completion) {
completion(ShareTypeQQ, success);
}
}];
}
}
+ (void)shareToQZone:(ShareContent *)content
completion:(SharePanelCompletionBlock)completion {
if (![QQShareManager isQQInstalled]) {
[self showAlertWithMessage:@"您的设备未安装QQ客户端无法进行QQ空间分享"];
if (completion) {
completion(ShareTypeQQ, NO);
}
return;
}
//
NSString *title = content.title ?: @"游戏分享";
NSString *description = content.desc ?: @"精彩游戏分享";
NSString *shareUrl = content.webpageUrl;
//
UIImage *thumbImage = [UIImage imageNamed:@"sharelogo"] ?: [UIImage imageNamed:@"Icon180"];
//
QQShareType shareType = shareUrl ? QQShareTypeNews : QQShareTypeText;
[QQShareManager shareToQZone:shareType
title:title
description:description
thumbImage:thumbImage
url:shareUrl
images:thumbImage ? @[thumbImage] : nil
completion:^(BOOL success) {
if (completion) {
completion(ShareTypeQQ, success);
}
}];
}
+ (void)showAlertWithMessage:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示"
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:okAction];
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;
}
//
- (UIViewController *)getTopViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
if ([topController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)topController;
return navigationController.visibleViewController;
}
if ([topController isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabBarController = (UITabBarController *)topController;
UIViewController *selectedViewController = tabBarController.selectedViewController;
if ([selectedViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)selectedViewController;
return navigationController.visibleViewController;
}
return selectedViewController;
}
return topController;
}
@end

View File

@@ -0,0 +1,82 @@
//
// WechatShareManager.h
// msext
//
// Created on 2025/06/17.
// Copyright © 2025年. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, WechatShareType) {
WechatShareTypeText, // 纯文本分享
WechatShareTypeImage, // 图片分享
WechatShareTypeLink, // 链接分享
WechatShareTypeScreenshot // 截图分享
};
typedef NS_ENUM(NSInteger, WechatScene) {
WechatSceneSession, // 微信好友
WechatSceneTimeline // 微信朋友圈
};
@interface WechatShareManager : NSObject
/**
* 检查微信是否已安装
* @return BOOL 是否已安装微信
*/
+ (BOOL)isWechatInstalled;
/**
* 使用ShareContent对象进行微信分享
* @param shareContent 完整的分享内容对象包含title、desc、webpageUrl、type、sharefriend字段
* @param completion 分享完成回调
*/
+ (void)shareWithContent:(id)shareContent
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 分享链接到微信
* @param url 链接URL
* @param title 标题
* @param description 描述
* @param thumbImage 缩略图
* @param scene 分享场景(好友/朋友圈)
* @param completion 完成回调
*/
+ (void)shareLinkToWechat:(NSString *)url
title:(NSString * _Nullable)title
description:(NSString * _Nullable)description
thumbImage:(UIImage * _Nullable)thumbImage
scene:(WechatScene)scene
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 分享图片到微信
* @param image 图片
* @param tagName 标签名称
* @param messageExt 扩展信息
* @param thumbImage 缩略图
* @param scene 分享场景(好友/朋友圈)
* @param completion 完成回调
*/
+ (void)shareImageToWechat:(UIImage *)image
tagName:(NSString * _Nullable)tagName
messageExt:(NSString * _Nullable)messageExt
thumbImage:(UIImage * _Nullable)thumbImage
scene:(WechatScene)scene
completion:(void (^ _Nullable)(BOOL success))completion;
/**
* 获取应用图标作为缩略图
* @return 应用图标UIImage对象
*/
+ (UIImage * _Nullable)getAppIconImage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,265 @@
//
// WechatShareManager.m
// msext
//
// Created on 2025/06/17.
// Copyright © 2025. All rights reserved.
//
#import "WechatShareManager.h"
#import "WXApiManager.h"
#import "WXApiRequestHandler.h"
#import "FuncPublic.h"
@implementation WechatShareManager
#pragma mark - Public Methods
+ (BOOL)isWechatInstalled {
return [WXApi isWXAppInstalled];
}
+ (void)shareWithContent:(id)shareContent
completion:(void (^ _Nullable)(BOOL success))completion {
NSLog(@"🔍 [WechatShareManager] 开始微信分享");
//
if (![self isWechatInstalled]) {
NSLog(@"❌ [WechatShareManager] 微信未安装");
if (completion) {
completion(NO);
}
return;
}
// ShareContent
if (!shareContent) {
NSLog(@"❌ [WechatShareManager] ShareContent对象为空");
if (completion) {
completion(NO);
}
return;
}
// KVCShareContent
NSString *title = nil;
NSString *description = nil;
NSString *webpageUrl = nil;
NSString *type = nil;
NSString *sharefriend = nil;
@try {
if ([shareContent respondsToSelector:@selector(valueForKey:)]) {
title = [shareContent valueForKey:@"title"];
description = [shareContent valueForKey:@"desc"];
webpageUrl = [shareContent valueForKey:@"webpageUrl"];
type = [shareContent valueForKey:@"type"];
sharefriend = [shareContent valueForKey:@"sharefriend"];
}
NSLog(@"🔍 [WechatShareManager] 从ShareContent提取参数:");
NSLog(@"🔍 - 标题: %@", title ?: @"(无)");
NSLog(@"🔍 - 描述: %@", description ?: @"(无)");
NSLog(@"🔍 - 链接: %@", webpageUrl ?: @"(无)");
NSLog(@"🔍 - 类型: %@", type ?: @"(无)");
NSLog(@"🔍 - 分享目标: %@", sharefriend ?: @"(无)");
} @catch (NSException *exception) {
NSLog(@"❌ [WechatShareManager] 解析ShareContent时发生异常: %@", exception.reason);
if (completion) {
completion(NO);
}
return;
}
//
enum WXScene currentScene;
if ([sharefriend intValue] == 1) {
currentScene = WXSceneSession; //
NSLog(@"🔍 [WechatShareManager] 分享目标: 微信好友");
} else {
currentScene = WXSceneTimeline; //
NSLog(@"🔍 [WechatShareManager] 分享目标: 微信朋友圈");
}
//
UIImage *thumbImage = [self getAppIconImage];
//
BOOL isScreenshotShare = [type intValue] == 2;
if (isScreenshotShare) {
//
NSLog(@"🔍 [WechatShareManager] 执行截图分享");
UIImage *screenImage = [FuncPublic getImageWithFullScreenshot];
if (screenImage) {
NSData *imageData = UIImageJPEGRepresentation(screenImage, 0.6);
[WXApiRequestHandler sendImageData:imageData
TagName:[NSString stringWithFormat:@"%@游戏截图分享", @"进贤聚友棋牌"]
MessageExt:description ?: @""
Action:@"share_action"
ThumbImage:thumbImage
InScene:currentScene];
NSLog(@"✅ [WechatShareManager] 截图分享请求已发送");
} else {
NSLog(@"❌ [WechatShareManager] 截图获取失败");
if (completion) {
completion(NO);
}
return;
}
} else {
//
NSLog(@"🔍 [WechatShareManager] 执行链接分享");
[WXApiRequestHandler sendLinkURL:webpageUrl ?: @""
TagName:[NSString stringWithFormat:@"%@游戏分享", @"进贤聚友棋牌"]
Title:title ?: @""
Description:description ?: @""
ThumbImage:thumbImage
InScene:currentScene];
NSLog(@"✅ [WechatShareManager] 链接分享请求已发送");
}
// AppDelegate
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"✅ [WechatShareManager] 分享完成");
if (completion) {
completion(YES);
}
});
}
+ (void)shareLinkToWechat:(NSString *)url
title:(NSString * _Nullable)title
description:(NSString * _Nullable)description
thumbImage:(UIImage * _Nullable)thumbImage
scene:(WechatScene)scene
completion:(void (^ _Nullable)(BOOL success))completion {
//
if (![self isWechatInstalled]) {
NSLog(@"❌ [WechatShareManager] 微信未安装");
if (completion) {
completion(NO);
}
return;
}
enum WXScene wxScene = (scene == WechatSceneSession) ? WXSceneSession : WXSceneTimeline;
[WXApiRequestHandler sendLinkURL:url ?: @""
TagName:@"游戏分享"
Title:title ?: @""
Description:description ?: @""
ThumbImage:thumbImage ?: [self getAppIconImage]
InScene:wxScene];
//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (completion) {
completion(YES);
}
});
}
+ (void)shareImageToWechat:(UIImage *)image
tagName:(NSString * _Nullable)tagName
messageExt:(NSString * _Nullable)messageExt
thumbImage:(UIImage * _Nullable)thumbImage
scene:(WechatScene)scene
completion:(void (^ _Nullable)(BOOL success))completion {
//
if (![self isWechatInstalled]) {
NSLog(@"❌ [WechatShareManager] 微信未安装");
if (completion) {
completion(NO);
}
return;
}
if (!image) {
NSLog(@"❌ [WechatShareManager] 图片为空");
if (completion) {
completion(NO);
}
return;
}
enum WXScene wxScene = (scene == WechatSceneSession) ? WXSceneSession : WXSceneTimeline;
NSData *imageData = UIImageJPEGRepresentation(image, 0.6);
[WXApiRequestHandler sendImageData:imageData
TagName:tagName ?: @"图片分享"
MessageExt:messageExt ?: @""
Action:@"share_action"
ThumbImage:thumbImage ?: [self getAppIconImage]
InScene:wxScene];
//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (completion) {
completion(YES);
}
});
}
+ (UIImage *)getAppIconImage {
//
UIImage *appIcon = nil;
// 1: Info.plist
NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
NSArray *iconFiles = infoPlist[@"CFBundleIconFiles"];
if (iconFiles && iconFiles.count > 0) {
NSString *iconFileName = [iconFiles lastObject]; //
appIcon = [UIImage imageNamed:iconFileName];
}
// 2:
if (!appIcon) {
NSArray *commonIconNames = @[@"Icon-180", @"Icon180", @"AppIcon60x60@3x", @"AppIcon"];
for (NSString *iconName in commonIconNames) {
appIcon = [UIImage imageNamed:iconName];
if (appIcon) break;
}
}
// 3: SharePanelsharelogo
if (!appIcon) {
appIcon = [UIImage imageNamed:@"sharelogo"];
}
// :
if (!appIcon) {
//
UIGraphicsBeginImageContextWithOptions(CGSizeMake(60, 60), NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
//
CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:1.0].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0, 0, 60, 60));
//
NSString *text = @"游戏";
NSDictionary *attributes = @{
NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
NSForegroundColorAttributeName: [UIColor whiteColor]
};
CGSize textSize = [text sizeWithAttributes:attributes];
CGPoint textPoint = CGPointMake((60 - textSize.width) / 2, (60 - textSize.height) / 2);
[text drawAtPoint:textPoint withAttributes:attributes];
appIcon = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return appIcon;
}
@end

View File

@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>进贤聚友棋牌</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -52,6 +52,26 @@
<string>xianliaoU1jJq3wgWluyB660</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.skyapp.ylgamehall</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ylgame</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>msext.qq.callback</string>
<key>CFBundleURLSchemes</key>
<array>
<string>msext</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.7</string>
@@ -64,6 +84,16 @@
<string>alipays</string>
<string>wechat</string>
<string>weixin</string>
<string>mqqapi</string>
<string>mqq</string>
<string>mqqOpensdkSSoLogin</string>
<string>mqqopensdkapiV2</string>
<string>mqqopensdkapiV3</string>
<string>mqqopensdkapiV4</string>
<string>wtloginmqq2</string>
<string>mqzone</string>
<string>snssdk1128</string>
<string>snssdk1233</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>