@@ -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 ] ;
[ url String appendFormat : @ "&previewimageUrl=%@" , [ self encodeString : [ @ "file://" stringByAppendingString : thumbPath ] ] ] ;
NSString * thumbPath = [ self saveImageToTempDirectory : thumbImage ] ;
NS String * 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 : @ "&u rl=%@" , [ 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 : @ "&previewimageU rl=%@" , [ 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 URLQueryAllowedCh arac terSet ] ] ;
// We must encode everything that is strictly reserved or unsafe in a query p arame ter 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