解决qq和抖音分享
This commit is contained in:
@@ -8,9 +8,101 @@
|
||||
|
||||
#import "QQShareManager.h"
|
||||
#import "FuncPublic.h" // 用于截图功能
|
||||
#import <LinkPresentation/LinkPresentation.h> // 引入 LinkPresentation 用于自定义分享预览
|
||||
|
||||
// 尝试检查是否存在 TencentOpenAPI SDK
|
||||
#if __has_include(<TencentOpenAPI/QQApiInterface.h>)
|
||||
#import <TencentOpenAPI/QQApiInterface.h>
|
||||
#import <TencentOpenAPI/TencentOAuth.h>
|
||||
#define HAS_QQ_SDK 1
|
||||
#else
|
||||
#define HAS_QQ_SDK 0
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// QQShareActivityItemSource
|
||||
// This class is used to customize the preview title and icon in the Share Sheet.
|
||||
// -----------------------------------------------------------------------------
|
||||
@interface QQShareActivityItemSource : NSObject <UIActivityItemSource>
|
||||
|
||||
@property (nonatomic, strong) id content; // URL, String, or Image
|
||||
@property (nonatomic, strong) NSString *title; // Preview Title
|
||||
@property (nonatomic, strong) UIImage *image; // Preview Image (Icon)
|
||||
@property (nonatomic, strong) NSString *desc; // Preview Description (Optional)
|
||||
|
||||
- (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc;
|
||||
|
||||
@end
|
||||
|
||||
@implementation QQShareActivityItemSource
|
||||
|
||||
- (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_content = content;
|
||||
_title = title;
|
||||
_image = image;
|
||||
_desc = desc;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UIActivityItemSource Methods
|
||||
|
||||
- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController {
|
||||
// Placeholder matches the content type
|
||||
return self.content;
|
||||
}
|
||||
|
||||
- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType {
|
||||
// Return the actual content to share
|
||||
return self.content;
|
||||
}
|
||||
|
||||
// Ensure the preview metadata is provided (iOS 13+)
|
||||
- (LPLinkMetadata *)activityViewControllerLinkMetadata:(UIActivityViewController *)activityViewController API_AVAILABLE(ios(13.0)) {
|
||||
LPLinkMetadata *metadata = [[LPLinkMetadata alloc] init];
|
||||
|
||||
// Set Preview Title (User requested App Name or specific title)
|
||||
metadata.title = self.title ? self.title : @"分享内容";
|
||||
|
||||
// Set Preview Icon / Image
|
||||
// 用户反馈 "图片分享正常,但其他情况(链接分享)图标有白边"。
|
||||
// 根本原因: LPLinkMetadata 对于 content-type 为 URL 的 preview,
|
||||
// 如果提供的 image 是 iconProvider,iOS 默认会将其渲染为 "Icon" 样式(带白边、圆角遮罩等)。
|
||||
// 如果提供的 image 是 imageProvider,iOS 会将其渲染为 "Image/Thumbnail" 样式(通常是大图)。
|
||||
|
||||
// 如果我们希望它像 "图片分享" 那样全屏、无白边地展示,我们需要把它伪装成 imageProvider,
|
||||
// 并且可能调整 NSItemProvider 类型。
|
||||
|
||||
// 经调研,system share sheet 的左侧预览区逻辑如下:
|
||||
// 1. 如果有 imageProvider,尝试显示大图 (Thumbnail)。如果图是正方形,系统默认还是有 padding。
|
||||
// 2. 如果只有 iconProvider,由于它仅仅是一个图标,系统会将其放置在一个方形容器中,必定有白边。
|
||||
|
||||
// 最终尝试:
|
||||
// 将其作为 imageProvider 提供,但这要求图片本身具备足够分辨率,且系统对于 URL Share 的 Layout 默认就是 Sidebar 模式。
|
||||
// 我们这里强制使用 imageProvider 试试看,这是目前看来最可能消除“仅Icon白边”的办法。
|
||||
|
||||
if (self.image) {
|
||||
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.image];
|
||||
metadata.imageProvider = itemProvider; // 改回 imageProvider,这是大图预览的关键
|
||||
metadata.iconProvider = itemProvider; // 同时设置
|
||||
}
|
||||
|
||||
// Set Original URL (if content is URL)
|
||||
if ([self.content isKindOfClass:[NSURL class]]) {
|
||||
metadata.originalURL = (NSURL *)self.content;
|
||||
metadata.URL = (NSURL *)self.content;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// QQ URL Schemes
|
||||
#define kQQScheme @"mqqapi://"
|
||||
|
||||
#define kQQShareScheme @"mqqapi://share/"
|
||||
#define kQQFriendScheme @"mqqapi://share/to_fri?"
|
||||
#define kQQZoneScheme @"mqqapi://share/to_qzone?"
|
||||
@@ -101,117 +193,350 @@ static void(^QQShareCompletion)(BOOL) = nil;
|
||||
// 保存回调
|
||||
QQShareCompletion = completion;
|
||||
|
||||
// 构建URL参数 - 使用QQ官方标准格式解决900101错误
|
||||
// 构建URL parameters based on OpenShare implementation logic to fix 900101
|
||||
// 900101 is often "Invalid AppID" or "Signature Mismatch".
|
||||
// OpenShare uses Base64 for almost all text fields.
|
||||
|
||||
NSMutableString *urlString = [NSMutableString stringWithString:kQQFriendScheme];
|
||||
|
||||
// QQ官方要求的标准参数顺序和格式
|
||||
|
||||
// 1. Basic Parameters
|
||||
[urlString appendString:@"version=1"];
|
||||
[urlString appendString:@"&cflag=0"];
|
||||
[urlString appendString:@"&src_type=app"];
|
||||
[urlString appendString:@"&src_type=app"]; // Required
|
||||
// [urlString appendString:@"&cflag=0"]; // OpenShare doesn't always send this, but let's keep it off for now or 0
|
||||
|
||||
// 2. App Identification
|
||||
// thirdAppDisplayName MUST be Base64 encoded for mqqapi usually
|
||||
NSString *base64Name = [self base64Encode:kQQAppName];
|
||||
NSString *encodedName = [self encodeString:base64Name];
|
||||
[urlString appendFormat:@"&thirdAppDisplayName=%@", encodedName];
|
||||
[urlString appendFormat:@"&app_name=%@", encodedName];
|
||||
|
||||
// 尝试使用QQ最新推荐的参数格式
|
||||
[urlString appendFormat:@"&thirdAppDisplayName=%@", [self encodeString:kQQAppName]];
|
||||
[urlString appendFormat:@"&app_id=%@", kQQAppID];
|
||||
[urlString appendFormat:@"&sdkv=2.9.0"];
|
||||
[urlString appendFormat:@"&sdkp=i"];
|
||||
[urlString appendFormat:@"&share_id=%@", kQQAppID];
|
||||
|
||||
// 回调相关参数
|
||||
// 3. Callback
|
||||
// mqqapi usually validates that callback_name matches "QQ" + Hex(AppID)
|
||||
// 102793577 (Decimal) -> 06208169 (Hex)
|
||||
// So callback_name should be QQ06208169
|
||||
NSString *hexCallbackName = @"QQ06208169";
|
||||
[urlString appendString:@"&callback_type=scheme"];
|
||||
[urlString appendFormat:@"&callback_name=%@", [self encodeString:kQQCallbackScheme]];
|
||||
[urlString appendFormat:@"&callback_name=%@", hexCallbackName];
|
||||
|
||||
// 获取Bundle ID用于验证
|
||||
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
// 4. Content Parameters
|
||||
|
||||
// 添加详细调试日志
|
||||
NSLog(@"🔍 ======== QQ分享参数详情 ========");
|
||||
NSLog(@"🔍 AppID: %@", kQQAppID);
|
||||
NSLog(@"🔍 Bundle ID: %@", bundleId);
|
||||
NSLog(@"🔍 AppName: %@", kQQAppName);
|
||||
NSLog(@"🔍 CallbackScheme: %@", kQQCallbackScheme);
|
||||
NSLog(@"🔍 QQ Friend Scheme: %@", kQQFriendScheme);
|
||||
|
||||
// 根据分享类型设置不同的参数
|
||||
switch (type) {
|
||||
case QQShareTypeText:
|
||||
[urlString appendString:@"&req_type=0"];
|
||||
if (title && title.length > 0) {
|
||||
[urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
// -------------------------------------------------------------------------
|
||||
// 策略选择:
|
||||
// 1. useSystemShare: 使用 iOS 系统分享面板 (UIActivityViewController)
|
||||
// 2. useQQSDK: 使用腾讯官方 SDK (TencentOpenAPI) - 需确保已导入SDK
|
||||
// 3. Fallback: 使用 URL Scheme (mqqapi://) - 无需SDK,直接调起 (Legacy)
|
||||
// -------------------------------------------------------------------------
|
||||
BOOL useSystemShare = NO; // 默认为NO
|
||||
BOOL useQQSDK = NO; // 默认为YES, 优先使用SDK
|
||||
|
||||
// 决策逻辑: 是否运行系统分享
|
||||
BOOL runSystemShare = NO;
|
||||
BOOL sdkIsAvailableAndEnabled = NO;
|
||||
|
||||
#if HAS_QQ_SDK
|
||||
if (useQQSDK) sdkIsAvailableAndEnabled = YES;
|
||||
#endif
|
||||
|
||||
if (sdkIsAvailableAndEnabled) {
|
||||
// 如果 SDK 可用且开启,优先走 SDK 逻辑 (后面处理),跳过 SystemShare
|
||||
runSystemShare = NO;
|
||||
} else {
|
||||
// 如果 SDK 不可用或未开启
|
||||
if (useSystemShare) {
|
||||
// 用户显式开启系统分享 -> 运行
|
||||
runSystemShare = YES;
|
||||
} else {
|
||||
// 若都未开启,检查特殊情况:
|
||||
// 图片分享无法通过 Scheme 直接调起会话,降级使用 SystemShare
|
||||
if (type == QQShareTypeImage) {
|
||||
runSystemShare = YES;
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
[urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
}
|
||||
}
|
||||
|
||||
if (runSystemShare) {
|
||||
NSMutableArray *activityItems = [NSMutableArray array];
|
||||
|
||||
// 准备预览图标 (App Icon)
|
||||
UIImage *appIcon = [self getAppIcon];
|
||||
|
||||
// 准备预览标题 (App Name 优先,或者 Title)
|
||||
NSString *previewTitle = kQQAppName; // 默认显示App名称
|
||||
if (title && title.length > 0) {
|
||||
previewTitle = title; // 如果有特定标题,也可以显示标题,或者拼接 "App Name - Title"
|
||||
}
|
||||
|
||||
// 使用 QQShareActivityItemSource 封装内容以自定义预览
|
||||
if (@available(iOS 13.0, *)) {
|
||||
switch (type) {
|
||||
case QQShareTypeText:
|
||||
if (description) {
|
||||
// 用户需求:通过系统分享文本时希望是纯文本形式,而不是外部链接/卡片
|
||||
// 直接传递 NSString 对象,避免使用 UIActivityItemSource 或 LPLinkMetadata 封装
|
||||
[activityItems addObject:description];
|
||||
}
|
||||
break;
|
||||
|
||||
case QQShareTypeImage:
|
||||
if (image) {
|
||||
// 图片分享本身预览就是图片,通常无需干预。
|
||||
// 用户反馈使用 ActivityItemSource 封装后会导致图标显示有白边等问题。
|
||||
// 因此,对于 Image 分享,我们恢复直接传递 image 对象的原生方式,让系统自己处理最佳预览。
|
||||
[activityItems addObject:image];
|
||||
}
|
||||
break;
|
||||
|
||||
case QQShareTypeNews:
|
||||
case QQShareTypeAudio:
|
||||
case QQShareTypeVideo:
|
||||
if (url) {
|
||||
NSURL *shareURL = [NSURL URLWithString:url];
|
||||
if (shareURL) {
|
||||
// 关键:将 URL 封装,并强制指定 appIcon 为 iconProvider
|
||||
QQShareActivityItemSource *item = [[QQShareActivityItemSource alloc] initWithContent:shareURL title:previewTitle image:appIcon description:description];
|
||||
[activityItems addObject:item];
|
||||
}
|
||||
}
|
||||
// 注意:对于链接分享,通常只放一个 NSURL 对象即可。
|
||||
// 放入 Title/Image 可能会导致变成混合分享,某些 App 处理不好。
|
||||
// 通过 ItemSource,我们只分享 URL,但显示的元数据是自定义的。
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case QQShareTypeImage:
|
||||
if (image) {
|
||||
// 将图片保存到本地临时目录
|
||||
NSString *imagePath = [self saveImageToTempDirectory:image];
|
||||
[urlString appendString:@"&req_type=2"];
|
||||
[urlString appendFormat:@"&image_url=%@", [self encodeString:[@"file://" stringByAppendingString:imagePath]]];
|
||||
if (title && title.length > 0) {
|
||||
[urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
} else {
|
||||
// iOS 13 以下降级处理 (不支持自定义预览)
|
||||
switch (type) {
|
||||
case QQShareTypeText:
|
||||
if (title) [activityItems addObject:title];
|
||||
if (description) [activityItems addObject:description];
|
||||
break;
|
||||
|
||||
case QQShareTypeImage:
|
||||
if (image) [activityItems addObject:image];
|
||||
break;
|
||||
|
||||
case QQShareTypeNews:
|
||||
case QQShareTypeAudio:
|
||||
case QQShareTypeVideo:
|
||||
if (url) {
|
||||
NSURL *shareURL = [NSURL URLWithString:url];
|
||||
if (shareURL) [activityItems addObject:shareURL];
|
||||
}
|
||||
if (title) [activityItems addObject:title];
|
||||
// iOS 12及以下,直接传内容,系统自己处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (activityItems.count > 0) {
|
||||
|
||||
NSLog(@"🔍 [QQShareManager] 策略: 使用系统 UIActivityViewController 分享");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
|
||||
activityVC.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) {
|
||||
NSLog(@"[QQShareManager] 系统分享回调: completed=%d", completed);
|
||||
if (completion) completion(completed);
|
||||
};
|
||||
|
||||
UIViewController *topVC = [self topViewController];
|
||||
if (topVC) {
|
||||
if ([activityVC respondsToSelector:@selector(popoverPresentationController)]) {
|
||||
activityVC.popoverPresentationController.sourceView = topVC.view;
|
||||
activityVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width/2.0, topVC.view.bounds.size.height/2.0, 1.0, 1.0);
|
||||
activityVC.popoverPresentationController.permittedArrowDirections = 0;
|
||||
}
|
||||
[topVC presentViewController:activityVC animated:YES completion:nil];
|
||||
} else {
|
||||
if (completion) completion(NO);
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
[urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
});
|
||||
return; // 结束执行,不再走下面的 URL Scheme 逻辑
|
||||
}
|
||||
}
|
||||
|
||||
#if HAS_QQ_SDK
|
||||
// -------------------------------------------------------------------------
|
||||
// 策略: 腾讯官方 SDK 分享
|
||||
// -------------------------------------------------------------------------
|
||||
if (useQQSDK) {
|
||||
NSLog(@"🔍 [QQShareManager] 策略: 使用腾讯官方 SDK 分享");
|
||||
|
||||
// 确保在主线程调用 (SDK要求)
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
QQApiObject *msgObj = nil;
|
||||
|
||||
switch (type) {
|
||||
case QQShareTypeText: {
|
||||
QQApiTextObject *textObj = [QQApiTextObject objectWithText:description ? description : @""];
|
||||
textObj.title = title;
|
||||
msgObj = textObj;
|
||||
break;
|
||||
}
|
||||
case QQShareTypeImage: {
|
||||
if (!image) {
|
||||
if (completion) completion(NO);
|
||||
return;
|
||||
}
|
||||
NSData *imgData = UIImageJPEGRepresentation(image, 0.8);
|
||||
// Image Object
|
||||
QQApiImageObject *imgObj = [QQApiImageObject objectWithData:imgData
|
||||
previewImageData:imgData
|
||||
title:title
|
||||
description:description];
|
||||
msgObj = imgObj;
|
||||
break;
|
||||
}
|
||||
case QQShareTypeNews:
|
||||
case QQShareTypeAudio:
|
||||
case QQShareTypeVideo: {
|
||||
if (!url) {
|
||||
if (completion) completion(NO);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *previewData = nil;
|
||||
if (thumbImage) {
|
||||
previewData = UIImageJPEGRepresentation(thumbImage, 0.5);
|
||||
}
|
||||
|
||||
NSURL *targetUrl = [NSURL URLWithString:url];
|
||||
|
||||
if (type == QQShareTypeAudio) {
|
||||
QQApiAudioObject *audioObj = [QQApiAudioObject objectWithURL:targetUrl
|
||||
title:title
|
||||
description:description
|
||||
previewImageData:previewData];
|
||||
msgObj = audioObj;
|
||||
} else if (type == QQShareTypeVideo) {
|
||||
// Video usually needs flashURL, here acts as News Link basically
|
||||
QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl
|
||||
title:title
|
||||
description:description
|
||||
previewImageData:previewData];
|
||||
msgObj = newsObj;
|
||||
} else {
|
||||
// News
|
||||
QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl
|
||||
title:title
|
||||
description:description
|
||||
previewImageData:previewData];
|
||||
msgObj = newsObj;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgObj) {
|
||||
SendMessageToQQReq *req = [SendMessageToQQReq reqWithContent:msgObj];
|
||||
// 调起QQ
|
||||
QQApiSendResultCode sent = [QQApiInterface sendReq:req];
|
||||
NSLog(@"[QQShareManager] SDK发送请求结果: %d", sent);
|
||||
|
||||
// 处理同步结果
|
||||
// 注意: 最终成功与否通常依赖 AppDelegate 的 onResp 回调
|
||||
// EQQAPISENDSUCESS = 0
|
||||
if (sent == 0) {
|
||||
NSLog(@"✅ SDK请求发送成功");
|
||||
// 这里不立即调用 completion(YES),等待回调
|
||||
} else {
|
||||
NSLog(@"❌ SDK请求发送失败");
|
||||
if (completion) completion(NO);
|
||||
}
|
||||
} else {
|
||||
if (completion) {
|
||||
completion(NO);
|
||||
}
|
||||
return;
|
||||
NSLog(@"❌ [QQShareManager] 构建 SDK 对象失败");
|
||||
if (completion) completion(NO);
|
||||
}
|
||||
});
|
||||
|
||||
return; // 拦截后续 Scheme 逻辑
|
||||
}
|
||||
#endif
|
||||
|
||||
// Falls through to Legacy Scheme Logic if useSystemShare is NO or activityItems is empty
|
||||
switch (type) {
|
||||
case QQShareTypeText:
|
||||
[urlString appendString:@"&file_type=text"];
|
||||
[urlString appendString:@"&req_type=0"]; // Text
|
||||
if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]];
|
||||
if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]];
|
||||
break;
|
||||
|
||||
case QQShareTypeImage: {
|
||||
if (!image) {
|
||||
NSLog(@"❌ [QQShareManager] 图片对象为空");
|
||||
if (completion) completion(NO);
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复原有的 URL Scheme 图片分享逻辑 (Dual Strategy)
|
||||
// 策略: Base64 (小图) vs FilePath (大图/持久化)
|
||||
BOOL useBase64Scheme = YES;
|
||||
|
||||
if (useBase64Scheme) {
|
||||
NSData *imgData = UIImageJPEGRepresentation(image, 0.5);
|
||||
if (imgData.length > 1 * 1024 * 1024) imgData = UIImageJPEGRepresentation(image, 0.3);
|
||||
NSString *base64Str = [imgData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
|
||||
[urlString appendString:@"&file_type=image"];
|
||||
[urlString appendFormat:@"&image_base64=%@", [self encodeString:base64Str]];
|
||||
[urlString appendString:@"&cflag=0"];
|
||||
} else {
|
||||
NSString *imagePath = [self saveImageToDocumentsDirectory:image];
|
||||
[urlString appendString:@"&file_type=image"];
|
||||
[urlString appendString:@"&req_type=5"];
|
||||
[urlString appendString:@"&cflag=0"];
|
||||
|
||||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||||
[pasteboard setData:UIImageJPEGRepresentation(image, 0.6) forPasteboardType:@"com.tencent.mqq.api.apiLargeData"];
|
||||
|
||||
if (title) [urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
if (description) [urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
|
||||
NSString *fullPath = [@"file://" stringByAppendingString:imagePath];
|
||||
NSString *encodedPath = [self encodeString:fullPath];
|
||||
[urlString appendFormat:@"&file_path=%@", encodedPath];
|
||||
[urlString appendFormat:@"&image_url=%@", encodedPath];
|
||||
[urlString appendFormat:@"&object_location=%@", encodedPath];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case QQShareTypeNews:
|
||||
// "News" (Link) share
|
||||
[urlString appendString:@"&file_type=news"];
|
||||
[urlString appendString:@"&req_type=1"];
|
||||
if (title && title.length > 0) {
|
||||
[urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
[urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
}
|
||||
if (url && url.length > 0) {
|
||||
[urlString appendFormat:@"&url=%@", [self encodeString:url]];
|
||||
}
|
||||
// 将缩略图保存到本地
|
||||
|
||||
if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]];
|
||||
if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]];
|
||||
if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]];
|
||||
|
||||
if (thumbImage) {
|
||||
NSString *thumbPath = [self saveImageToTempDirectory:thumbImage];
|
||||
[urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]];
|
||||
NSString *thumbPath = [self saveImageToTempDirectory:thumbImage];
|
||||
NSString *fullPath = [@"file://" stringByAppendingString:thumbPath];
|
||||
// Preview Image URL often requires BASE64 of the path string for OpenShare compatibility
|
||||
[urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]];
|
||||
}
|
||||
break;
|
||||
|
||||
case QQShareTypeAudio:
|
||||
[urlString appendString:@"&req_type=3"];
|
||||
if (title && title.length > 0) {
|
||||
[urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
[urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
}
|
||||
if (url && url.length > 0) {
|
||||
[urlString appendFormat:@"&url=%@", [self encodeString:url]];
|
||||
}
|
||||
if (thumbImage) {
|
||||
NSString *thumbPath = [self saveImageToTempDirectory:thumbImage];
|
||||
[urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]];
|
||||
[urlString appendString:@"&file_type=audio"];
|
||||
[urlString appendString:@"&req_type=2"];
|
||||
if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]];
|
||||
if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]];
|
||||
if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]];
|
||||
if (thumbImage) {
|
||||
NSString *thumbPath = [self saveImageToTempDirectory:thumbImage];
|
||||
NSString *fullPath = [@"file://" stringByAppendingString:thumbPath];
|
||||
[urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]];
|
||||
}
|
||||
break;
|
||||
|
||||
case QQShareTypeVideo:
|
||||
[urlString appendString:@"&req_type=4"];
|
||||
if (title && title.length > 0) {
|
||||
[urlString appendFormat:@"&title=%@", [self encodeString:title]];
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
[urlString appendFormat:@"&description=%@", [self encodeString:description]];
|
||||
}
|
||||
if (url && url.length > 0) {
|
||||
[urlString appendFormat:@"&url=%@", [self encodeString:url]];
|
||||
}
|
||||
if (thumbImage) {
|
||||
NSString *thumbPath = [self saveImageToTempDirectory:thumbImage];
|
||||
[urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]];
|
||||
}
|
||||
// Video might be similar
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -929,9 +1254,18 @@ static void(^QQShareCompletion)(BOOL) = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)base64Encode:(NSString *)string {
|
||||
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [data base64EncodedStringWithOptions:0];
|
||||
}
|
||||
|
||||
+ (NSString *)encodeString:(NSString *)string {
|
||||
if (!string) return @"";
|
||||
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
// We must encode everything that is strictly reserved or unsafe in a query parameter value
|
||||
// Especially + / = which are key for Base64
|
||||
NSMutableCharacterSet *allowed = [[NSCharacterSet alphanumericCharacterSet] mutableCopy];
|
||||
[allowed addCharactersInString:@"-._~"]; // Unreserved characters per RFC 3986
|
||||
return [string stringByAddingPercentEncodingWithAllowedCharacters:allowed];
|
||||
}
|
||||
|
||||
+ (NSString *)saveImageToTempDirectory:(UIImage *)image {
|
||||
@@ -943,6 +1277,21 @@ static void(^QQShareCompletion)(BOOL) = nil;
|
||||
return filePath;
|
||||
}
|
||||
|
||||
+ (NSString *)saveImageToDocumentsDirectory:(UIImage *)image {
|
||||
// Compress image to ensure it's not too large (similar to WeChat's 0.6 behavior)
|
||||
NSData *imageData = UIImageJPEGRepresentation(image, 0.6);
|
||||
NSString *fileName = [NSString stringWithFormat:@"qq_share_persistent_%@.jpg", [self generateUUID]];
|
||||
|
||||
// Use Documents directory for persistence
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths firstObject];
|
||||
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
|
||||
|
||||
[imageData writeToFile:filePath atomically:YES];
|
||||
NSLog(@"🔍 [QQShareManager] Saved image to Documents: %@", filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
+ (NSString *)generateUUID {
|
||||
CFUUIDRef uuid = CFUUIDCreate(NULL);
|
||||
CFStringRef uuidString = CFUUIDCreateString(NULL, uuid);
|
||||
@@ -1029,7 +1378,24 @@ static void(^QQShareCompletion)(BOOL) = nil;
|
||||
// 尝试获取应用桌面图标
|
||||
UIImage *appIcon = nil;
|
||||
|
||||
// 方法1: 从Info.plist获取图标信息
|
||||
// 方法1.1: 优先尝试直接从 Asset Catalog 中获取 "AppIcon-1"
|
||||
// 很多时候 Xcode 并不会把 AppIcon 打包成松散文件,而是编译进 Assets.car
|
||||
// 我们发现 Info.plist 并未包含 CFBundleIcons (在某些旧项目配置中),但 Xcode 设置了 ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-1"
|
||||
|
||||
appIcon = [UIImage imageNamed:@"AppIcon-1"];
|
||||
if (appIcon) {
|
||||
NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon-1");
|
||||
return appIcon;
|
||||
}
|
||||
|
||||
appIcon = [UIImage imageNamed:@"AppIcon"];
|
||||
if (appIcon) {
|
||||
NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon");
|
||||
return appIcon;
|
||||
}
|
||||
|
||||
// 方法1.2: 从Info.plist获取图标信息
|
||||
|
||||
NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
|
||||
|
||||
// iOS 13+ 支持的新格式
|
||||
@@ -1471,4 +1837,86 @@ static void(^QQShareCompletion)(BOOL) = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)shareWithContent:(id)shareContent
|
||||
completion:(void (^ _Nullable)(BOOL success))completion {
|
||||
NSLog(@"🔍 [QQShareManager] 开始QQ直接分享(自动拉起会话选择)");
|
||||
|
||||
// 检查QQ是否已安装
|
||||
if (![self isQQInstalled]) {
|
||||
NSLog(@"❌ [QQShareManager] QQ未安装");
|
||||
if (completion) {
|
||||
completion(NO);
|
||||
}
|
||||
[self showAppNotInstalledAlert:@"QQ"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取参数
|
||||
NSString *title = nil;
|
||||
NSString *description = nil;
|
||||
NSString *webpageUrl = nil;
|
||||
NSString *type = nil;
|
||||
|
||||
@try {
|
||||
if ([shareContent respondsToSelector:@selector(valueForKey:)]) {
|
||||
title = [shareContent valueForKey:@"title"];
|
||||
description = [shareContent valueForKey:@"desc"];
|
||||
webpageUrl = [shareContent valueForKey:@"webpageUrl"];
|
||||
type = [shareContent valueForKey:@"type"];
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"❌ [QQShareManager] 参数提取异常: %@", exception);
|
||||
if (completion) completion(NO);
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断分享类型
|
||||
BOOL isScreenshotShare = [type intValue] == 2;
|
||||
|
||||
if (isScreenshotShare) {
|
||||
// 截图分享 (Image)
|
||||
UIImage *screenImage = [FuncPublic getImageWithFullScreenshot];
|
||||
|
||||
// 如果截屏失败或者为空,尝试检查是否有 image 字段(可能是Base64或UIImage对象)
|
||||
if (!screenImage) {
|
||||
id imageObj = nil;
|
||||
if ([shareContent respondsToSelector:@selector(valueForKey:)]) {
|
||||
@try { imageObj = [shareContent valueForKey:@"image"]; } @catch (NSException *e) {}
|
||||
}
|
||||
if (imageObj && [imageObj isKindOfClass:[UIImage class]]) {
|
||||
screenImage = imageObj;
|
||||
} else if (imageObj && [imageObj isKindOfClass:[NSString class]]) {
|
||||
NSData *data = [[NSData alloc] initWithBase64EncodedString:imageObj options:0];
|
||||
if (data) screenImage = [UIImage imageWithData:data];
|
||||
}
|
||||
}
|
||||
|
||||
if (!screenImage) {
|
||||
NSLog(@"❌ [QQShareManager] 截图失败且未通过参数传入图片");
|
||||
if (completion) completion(NO);
|
||||
return;
|
||||
}
|
||||
|
||||
[self shareToQQFriend:QQShareTypeImage
|
||||
title:title
|
||||
description:description
|
||||
thumbImage:nil
|
||||
url:nil
|
||||
image:screenImage
|
||||
completion:completion];
|
||||
} else {
|
||||
// 网页/链接分享 (News)
|
||||
// 默认使用应用图标作为缩略图
|
||||
UIImage *thumbImage = [self getAppIcon];
|
||||
|
||||
[self shareToQQFriend:QQShareTypeNews
|
||||
title:title
|
||||
description:description
|
||||
thumbImage:thumbImage
|
||||
url:webpageUrl
|
||||
image:nil
|
||||
completion:completion];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user