iOS第三發平臺元件化解耦實踐

張玉棠發表於2019-04-04

iOS第三發平臺元件化解耦實踐

背景

之前寫過一篇類似的,以下是舊的背景介紹,因為這部分沒有變動,依舊還是使用舊的背景介紹,引用如下。這次把這個元件做了一個比較大的改動,所以重新寫了一篇文章總結,固有此文。

專案使用到了一些第三方平臺的登入、分享、支付功能,包括了微信、微博、QQ平臺登入分享和支付寶、微信平臺的支付,使用的是原生的接入配置整合的,功能上基本上對照著SDK的開發文件就能夠成功的整合了。但是問題也後面也漸漸的暴露出來了,第三方平臺的登入、分享、支付功能不同平臺的的SDK實現方式還是有很大的差別的,包括了輸入的引數以及回撥方式都有差別很大,如果只是簡單的按照文件整合,那麼一定會遇到程式碼呼叫結構很亂,回撥雜亂不統一的問題,更為嚴重的是,後面如果新增刪除一個第三發平臺,那麼修改變得很困難和難以維護,這違反了軟體開發中的開閉原則,所以考慮到了把這部分程式碼做一個重構。

本文主要介紹的內容集中在實現步驟這部分,其他部分可以視為引子,實現步驟這部分的內容包含如下:

  • 舊的方式侷限性
  • 實現思路
  • 架構設計(抽象介面分析設計)
    • 平臺處理的介面設計
    • 平臺請求的介面設計
    • 平臺回撥的介面設計
  • 典型案例分析(QQ登入分享以及QQ錢包支付為例)
    • 平臺處理的類實現
    • 平臺請求的類實現
    • 平臺回撥的類實現
    • 平臺管理類
      • 平臺管理類管理平臺處理類的實現
      • 平臺的配置引數處理
      • 平臺管理類提供的擴充套件點

結果

專案地址:YTThirdPlatformManager

以一個使用案例和一個外掛擴充套件自定義平臺的例子來簡單說下結果

使用案例

第三方平臺註冊使用如下方法,提供引數可配置的介面,以在不同的app中只要修改配置引數即可,不用修改實現類類的程式碼

    /**** 第三方平臺註冊 *****/
    // 微信
    [configInstance setPlaform:PTThirdPlatformTypeWechat
                         appID:kWXAppID
                        appKey:nil
                     appSecret:kWXAppSecret
                   redirectURL:nil
                    URLSchemes:nil];
    // QQ授權分享
    [configInstance setPlaform:PTThirdPlatformTypeTencentQQ
                       subType:PTThirdPlatformSubTypeAuthShare
                         appID:kTencentAppID
                        appKey:kTencentAppKey
                     appSecret:kTencentAppSecret
                   redirectURL:nil
                    URLSchemes:nil];
複製程式碼

分享和支付的例子如下:

// 分享模型
    ThirdPlatformShareModel* shareModel = [[ThirdPlatformShareModel alloc] init];
    shareModel.image = nil;
    shareModel.imageUrlString = @"";
    shareModel.title = @"title";
    shareModel.text = @"text";
    shareModel.weiboText = @"weibo text";
    shareModel.urlString = @"http://www.baidu.com";
    shareModel.fromViewController = self;
    shareModel.shareResultBlock = ^(PTShareType pplatform, PTShareResult result, NSError * error) {
        
    };
    [self addActionWithName:@"釘釘分享Demo" callback:^{
        shareModel.platform = PTCustumShareTypeDingTalk;
        [[PTThirdPlatformManager sharedInstance] shareWithModel:shareModel];
    }];
    
    
    // 支付資訊模型
    OrderModel* order = [[OrderModel alloc] init];
    [self addActionWithName:@"支付寶支付" callback:^{
        [[PTThirdPlatformManager sharedInstance] payWithPlateform:PTThirdPlatformTypeAlipay order:order paymentBlock:^(PTPayResult result) {
            
        }];
    }];
複製程式碼

外掛擴充套件自定義平臺的例子

該庫提供了擴充套件點提供使用者的自定義功能擴充套件,以下是釘釘分享的一個自定義擴充套件接入的示例程式碼

    // 自定義的第三方平臺以外掛的方式新增
    PTThirdPlatformManager* configInstance = [PTThirdPlatformManager sharedInstance];
    [configInstance addCustomSharePlatform:PTCustumShareTypeDingTalk
                              managerClass:PTDingTalkManager.class];
    [configInstance setPlaform:PTCustumShareTypeDingTalk
                         appID:kDingTalkAppID
                        appKey:nil
                     appSecret:nil
                   redirectURL:nil
                    URLSchemes:nil];
複製程式碼

呼叫擴充套件的功能程式碼如下:

// 分享模型
    ThirdPlatformShareModel* shareModel = [[ThirdPlatformShareModel alloc] init];
    shareModel.image = nil;
    shareModel.imageUrlString = @"";
    shareModel.title = @"title";
    shareModel.text = @"text";
    shareModel.weiboText = @"weibo text";
    shareModel.urlString = @"http://www.baidu.com";
    shareModel.fromViewController = self;
    shareModel.shareResultBlock = ^(PTShareType pplatform, PTShareResult result, NSError * error) {
        
    };
    [self addActionWithName:@"釘釘分享Demo" callback:^{
        shareModel.platform = PTCustumShareTypeDingTalk;
        [[PTThirdPlatformManager sharedInstance] shareWithModel:shareModel];
    }];
複製程式碼

實現步驟

實現步驟從以下四個方面展開來說

  • 舊的方式侷限性
  • 實現思路
  • 架構設計(抽象介面分析設計)
  • 典型案例分析(QQ登入分享以及QQ錢包支付為例)

舊的方式侷限性

在我把這個功能實現拆分解耦的上一個版本,我的實現方式是使用OC中的Category,我定義了一個PTThirdPlatformManager第三方平臺的管理類,這個類的職責是第三方授權登入,然後使用兩個Category:PTThirdPlatformManager+PayPTThirdPlatformManager+Share分別處理第三方平臺的支付和分享,下面是PTThirdPlatformManager+Pay中的介面設計和部分的實現設計

PTThirdPlatformManager (Share) 標頭檔案

typedef void(^PTShareResultBlock)(PTShareType platform, PTShareResult shareResult, NSError* error);

@interface PTThirdPlatformManager (Share) <WXApiManagerDelegate, PTTencentApiManagerDelegate>

@property (nonatomic, copy) PTShareResultBlock shareResultBlock;


/**
 獲取分享的URL
 */
- (NSString*)shareURLStringWithParams:(NSDictionary*)params;

/**
 *  第三方分享,支援分享到Facebook、Twitter、etc..
 *  @param platform           第三方分享平臺
 *  @param text               分享的文字
 *  @param url                分享的URL
 *  @param fromViewController 從哪個頁面呼叫的分享
 *  @param shareResultBlock   分享結果回撥Block
 */
- (void)shareToPlateform:(PTShareType)platform
                   image:(UIImage*)image
          imageUrlString:(NSString*)imageUrlString
                   title:(NSString*)title
                    text:(NSString*)text
                     urlString:(NSString*)urlString
      fromViewController:(UIViewController*)fromViewController
        shareResultBlock:(PTShareResultBlock)shareResultBlock;

@end
複製程式碼

PTThirdPlatformManager (Share) 部分實現檔案

@implementation PTThirdPlatformManager (Share)

/**
 獲取分享的URL
 */
- (NSString*)shareURLStringWithParams:(NSDictionary*)params {
    NSString* globalShareLink = [MMGlobalConfigManager sharedInstance].globalConfig.shareLink;
    if (globalShareLink) {
        NSString* randStr = [NSString stringWithFormat:@"%d", arc4random_uniform(100000)];
        NSMutableDictionary* tmpParams =
        [@{@"name": ValueOrEmpty([AccountManager sharedInstance].account.nickname),
           @"country": ValueOrEmpty([ProductParameters currentCountry]),
           @"av": ValueOrEmpty([ProductParameters appVersion]),
           @"pid": ValueOrEmpty([ProductParameters productID]),
           @"rand": ValueOrEmpty(randStr)} mutableCopy];
        if (params) {
            [tmpParams addEntriesFromDictionary:params];
        }
        for (NSString* key in tmpParams.allKeys) {
            tmpParams[key] = [GlobalURL URLEncode:tmpParams[key]];
        }
        NSString* composedUrlString = [GlobalURL URLStringWithBaseURLString:globalShareLink params:tmpParams];
        return composedUrlString;
    }
    return nil;
}

/**
 *  第三方分享,支援分享到Facebook、Twitter、etc..
 *  @param platform           第三方分享平臺
 *  @param text               分享的文字
 *  @param url                分享的URL
 *  @param fromViewController 從哪個頁面呼叫的分享
 *  @param shareResultBlock   分享結果回撥Block
 */
- (void)shareToPlateform:(PTShareType)platform
                   image:(UIImage*)image
          imageUrlString:(NSString*)imageUrlString
                   title:(NSString*)title
                    text:(NSString*)text
               urlString:(NSString*)urlString
      fromViewController:(UIViewController*)fromViewController
        shareResultBlock:(PTShareResultBlock)shareResultBlock {
    self.shareResultBlock = shareResultBlock;
    
    __block UIImage* sharedImage = nil;
    if (image) {
        sharedImage = image;
        [self doSharePlateform:platform image:sharedImage imageUrlString:imageUrlString title:title text:text urlString:urlString fromViewController:fromViewController shareResultBlock:shareResultBlock];
    } else if (imageUrlString != nil) {
        [[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:imageUrlString] options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
            if (image) {
                sharedImage = image;
            } else {
                sharedImage = [UIImage imageNamed:@"app_icon"];
            }
            [self doSharePlateform:platform image:sharedImage imageUrlString:imageUrlString title:title text:text urlString:urlString fromViewController:fromViewController shareResultBlock:shareResultBlock];
        }];
    } else {
        sharedImage = [UIImage imageNamed:@"signin_logo"];
        [self doSharePlateform:platform image:sharedImage imageUrlString:imageUrlString title:title text:text urlString:urlString fromViewController:fromViewController shareResultBlock:shareResultBlock];
    }
}


- (void)doSharePlateform:(PTShareType)platform
                   image:(UIImage*)image
          imageUrlString:(NSString*)imageUrlString
                   title:(NSString*)title
                    text:(NSString*)text
               urlString:(NSString*)urlString
      fromViewController:(UIViewController*)fromViewController
        shareResultBlock:(PTShareResultBlock)shareResultBlock {
    switch (platform) {
        case PTShareTypeWechat:
        {
            [self doWechatShareWithImage:image urlString:urlString title:title text:text fromViewController:fromViewController];
        }
            break;
        case PTShareTypeWechatLine:
        {
            [self doWechatLineShareWithImage:image
                                   urlString:urlString
                                       title:title
                                        text:text
                          fromViewController:fromViewController];
        }
        case PTShareTypeQQ:
        case PTShareTypeQQZone:
        {
            [self doQQShareWithImage:image
                      imageUrlString:imageUrlString
                           urlString:urlString
                               title:title
                                text:text
                           shareType:platform
                  fromViewController:fromViewController];
        }
        default:
            break;
    }
}

- (void)doWechatShareWithImage:(UIImage*)image
                           urlString:(NSString*)urlString
                         title:(NSString*)title
                          text:(NSString*)text
            fromViewController:(UIViewController*)fromViewController {
    [PTWXApiManager sharedManager].delegate = self;
    BOOL shareResult = [PTWXRequestHandler sendMessageWithImage:image urlString:urlString title:title text:text scene:WXSceneSession];
    if (shareResult == NO) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [Dialog toast:_(@"Please install Wechat")];
        });
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultFailed, nil);
    }
}

- (void)doWechatLineShareWithImage:(UIImage*)image
                         urlString:(NSString*)urlString
                             title:(NSString*)title
                              text:(NSString*)text
                fromViewController:(UIViewController*)fromViewController {
    [PTWXApiManager sharedManager].delegate = self;
    BOOL shareResult = [PTWXRequestHandler sendMessageWithImage:image urlString:urlString title:(NSString*)title text:text scene:WXSceneTimeline];
    if (shareResult == NO) {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultFailed, nil);
    }
}

// 分享到QQ
- (void)doQQShareWithImage:(UIImage*)image
            imageUrlString:(NSString*)imageUrlString
                     urlString:(NSString*)urlString
                         title:(NSString*)title
                          text:(NSString*)text
                 shareType:(PTShareType)shareType
            fromViewController:(UIViewController*)fromViewController {
    [PTTencentApiManager sharedManager].delegate = self;
    BOOL shareResult = [PTTencentApiRequestHandler sendMessageWithImage:image imageUrlString:imageUrlString urlString:urlString title:title text:text shareType:shareType];
    if (shareResult == NO) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // [Dialog toast:_(@"Please install Wechat")];
        });
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultFailed, nil);
    }
}


#pragma mark - WXApiManagerDelegate

- (void)managerDidRecvMessageResponse:(SendMessageToWXResp *)response; {
    if (response.errCode == WXSuccess) {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultSuccess, nil);
    } else if (response.errCode == WXErrCodeUserCancel) {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultCancel, nil);
    } else {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeWechat, PTShareResultFailed, nil);
    }
}


#pragma mark - ......::::::: PTTencentApiManagerDelegate :::::::......

- (void)qq_managerDidRecvMessageResponse:(BOOL)result {
    if (result) {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeQQ, PTShareResultSuccess, nil);
    } else {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeQQ, PTShareResultFailed, nil);
    }
}

#pragma mark - ......::::::: getter & setter :::::::......

- (void)setShareResultBlock:(PTShareResultBlock)shareResultBlock {
    objc_setAssociatedObject(self, @selector(shareResultBlock), shareResultBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (PTShareResultBlock)shareResultBlock {
    return objc_getAssociatedObject(self, _cmd);
}

@end
複製程式碼

這個類承擔了職責目前包括了:分享資訊的預處理、分享到微信、分享到微信的回撥結果處理、分享到QQ、分享到QQ的回撥結果處理、分享到微博、分享到微博的回撥結果處理,這個類負責的職責太多了,這違反了軟體設計中的單一職責原則。後續如果有其他的分享平臺,我們必須去修改這個類的程式碼,這個類會新增更多的if/else、switch/case條件分支以及對應平臺的結果回撥處理,會導致這個類的內容變得更加的膨脹和難以維護,這違反了軟甲設計中的開閉原則。此外因為是Category,需要使用runtime處理屬性的getter和setter,顯得笨重、麻煩、不夠優雅並且可能會因為記憶體管理產生不必要的問題。

由上面的分析,使用這種方式有以下的缺點:

  • 違反了單一職責原則
  • 違反了開閉原則
  • Category中定義屬性儲存資料不優雅和易出錯

實現思路

根據上一節「舊的方式侷限性」中的分析,針對缺點,具體問題具體分析,找到對應的解決方案

  • 違反了單一職責原則

分享分類處理了微信、QQ、微博三個平臺的業務邏輯,導致程式碼邏輯上的複雜,所以如果從平臺角度切入,每個平臺獨立處理自己的業務那麼程式碼將會得到簡化,職責也會變得更加的清晰。
針對單個平臺分析,單個平臺處理步驟的指標包括髮送分享的請求、接收非同步回撥、返回資訊給客戶端,單個平臺的職責還是可以進行拆分為:傳送請求、處理非同步回撥
以上從平臺平臺的傳送和處理回撥兩個角度去拆分職責,讓每個類的職責更加的單一和清晰。

  • 違反了開閉原則

如果從平臺平臺的傳送和處理回撥兩個角度去拆分職責、那麼新增一個第三方分享只要新增對應的平臺類、請求處理類、非同步回撥處理類,雖然增加了類的數量,但是這個代價相比不遵循設計原則導致了類膨脹和難以維護來說還是值得的。

  • Category中定義屬性儲存資料不優雅和易出錯

從以上的單一職責原則開閉原則兩方面的分析,大概可以從平臺、平臺的請求、平臺的回撥三個維度進行抽象,設計對應的子類去實現抽象的介面,所以這是一個繼承的體系,不存在Category了,可以使用在普通類中那樣的去定義屬性即可。

架構設計(抽象介面分析設計)

平臺處理的介面設計

平臺處理的介面包含了以下功能:

  • 第三方平臺處理URL
  • 第三方登入
  • 第三方分享
  • 第三方支付
  • 檢測平臺APP是否安裝
@protocol PTAbsThirdPlatformManager <NSObject>

@optional

- (void)thirdPlatConfigWithApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

/**
 第三方平臺處理URL
 */
- (BOOL)thirdPlatCanOpenUrlWithApplication:(UIApplication *)application
                                   openURL:(NSURL *)url
                         sourceApplication:(NSString *)sourceApplication
                                annotation:(id)annotation;

/**
 第三方登入
 
 @param thirdPlatformType 第三方平臺
 @param viewController 從哪個頁面呼叫的分享
 @param callback 登入回撥
 */
- (void)signInWithType:(PTThirdPlatformType)thirdPlatformType
    fromViewController:(UIViewController *)viewController
              callback:(void (^)(ThirdPlatformUserInfo* userInfo, NSError* err))callback;

/**
 第三方分享
 */
- (void)shareWithModel:(ThirdPlatformShareModel*)model;

/**
 第三方支付

 @param payMethodType 支付平臺
 @param order 支付訂單模型
 @param paymentBlock 支付結果回撥
 */
- (void)payWithPlateform:(PTThirdPlatformType)payMethodType order:(OrderModel*)order paymentBlock:(void (^)(BOOL result))paymentBlock;

// APP是否安裝
- (BOOL)isThirdPlatformInstalled:(PTShareType)thirdPlatform;

@end
複製程式碼

平臺請求的介面設計

平臺請求介面設計包含了以下功能:

  • 第三方授權登入
  • 第三方支付
  • 第三方分享
@protocol PTAbsThirdPlatformRequestHandler <NSObject>

@optional

// 第三方授權
+ (BOOL)sendAuthInViewController:(UIViewController *)viewController;

// 支付
+ (BOOL)payWithOrder:(OrderModel*)order;

// 分享
+ (BOOL)sendMessageWithModel:(ThirdPlatformShareModel*)model;

@end
複製程式碼

平臺回撥的介面設計

平臺回撥的介面設計處理單個平臺的SDK回撥,一般的是第三方APP通過URL的跨程式通訊,跳轉到宿主APP,傳遞結果引數。這個回撥不同的第三方平臺設計的介面不一樣,所以在我們的平臺回撥的介面設計種就把這種差異化統一,使用自定義的回撥,把資料通過統一的方法回撥到上層去。

  • PTAbsThirdPlatformRespManagerDelegate 是統一回撥介面的抽象設計
  • PTAbsThirdPlatformRespManager 實現不同平臺的回撥介面,通過統一的PTAbsThirdPlatformRespManagerDelegate物件delegate回撥到上層
// RespManagerDelegate
@protocol PTAbsThirdPlatformRespManagerDelegate <NSObject>

@optional

- (void)respManagerDidRecvPayResponse:(BOOL)result platform:(PTThirdPlatformType)platform;
- (void)respManagerDidRecvAuthResponse:(ThirdPlatformUserInfo *)response platform:(PTThirdPlatformType)platform;
- (void)respManagerDidRecvMessageResponse:(BOOL)result platform:(PTShareType)platform;

@end


@protocol PTAbsThirdPlatformRespManager <NSObject>

@optional

// 代理,子類需要設定getter/setter
@property (nonatomic, weak) id<PTAbsThirdPlatformRespManagerDelegate> delegate;

@end
複製程式碼

典型案例分析(QQ登入分享以及QQ錢包支付為例)

QQ登入分享和QQ錢包支付雖然是屬於同一個平臺,但是卻需要整合不同的SDK以及不同的配置,相比微信這種一個SDK整合了授權、分享、支付功能的第三方平臺來說,QQ平臺具體典型性,所以選擇QQ平臺進行案例分析。此外,因為功能根據職責進行了拆分,生成多個平臺處理類,需要一個管理類來管理這些平臺處理類,並且為了實現更好的擴充套件,比如說需要擴充套件一個不再該庫中內建的平臺處理類,管理類應當提供一個擴充套件點進行擴充套件,這樣庫才具有更好的靈活性和擴充套件性。

從以下四個方面來實現QQ平臺的授權登入、分享、支付的功能

  • 平臺處理的類實現
  • 平臺請求的類實現
  • 平臺回撥的類實現
  • 平臺管理類
    • 平臺管理類管理平臺處理類的實現
    • 平臺的配置引數處理
    • 平臺管理類提供的擴充套件點

平臺處理的類實現

平臺處理類包含了以下功能:

  • 第三方平臺處理URL
  • 第三方登入
  • 第三方分享
  • 第三方支付
  • 檢測平臺APP是否安裝

平臺請求處理類依賴平臺請求的類平臺回撥的類,可以認為是一個中間者,負責協調平臺請求的類平臺回撥的類之間的關係。

@interface PTTencentManager () <PTAbsThirdPlatformRespManagerDelegate>
@end

@implementation PTTencentManager

DEF_SINGLETON

- (void)thirdPlatConfigWithApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 子類實現
    // 初始化QQ模組
    [PTTencentRespManager sharedInstance];
}

/**
 第三方平臺處理URL
 */
- (BOOL)thirdPlatCanOpenUrlWithApplication:(UIApplication *)application
                                   openURL:(NSURL *)url
                         sourceApplication:(NSString *)sourceApplication
                                annotation:(id)annotation {
    // QQ 授權
    if ([TencentOAuth CanHandleOpenURL:url] && [TencentOAuth HandleOpenURL:url]) {
        return YES;
    }
    
    // QQ 業務
    if ([QQApiInterface handleOpenURL:url delegate:[PTTencentRespManager sharedInstance]]) {
        return YES;
    }
    
    // QQ錢包,在此函式中註冊回撥監聽
    if ([[QQWalletSDK sharedInstance] hanldeOpenURL:url]) {
        return YES;
    }
    
    return NO;
}

/**
 第三方登入
 */
- (void)signInWithType:(PTThirdPlatformType)thirdPlatformType fromViewController:(UIViewController *)viewController callback:(void (^)(ThirdPlatformUserInfo* userInfo, NSError* err))callback {
    self.callback = callback;
    [PTTencentRespManager sharedInstance].delegate = self;
    [PTTencentRequestHandler sendAuthInViewController:viewController];
}

// 分享
- (void)doShareWithModel:(ThirdPlatformShareModel *)model {
    self.shareResultBlock = model.shareResultBlock;
    [PTTencentRespManager sharedInstance].delegate = self;
    BOOL shareResult = [PTTencentRequestHandler sendMessageWithModel:model];
    if (shareResult == NO) {
        !self.shareResultBlock ?: self.shareResultBlock(PTShareTypeQQ, PTShareResultFailed, nil);
    }
}

/**
 第三方支付
 */
- (void)payWithPlateform:(PTThirdPlatformType)payMethodType order:(OrderModel*)order paymentBlock:(void (^)(PTPayResult result))paymentBlock {
    self.paymentBlock = paymentBlock;
    // 使用QQ支付
    [PTTencentRespManager sharedInstance].delegate = self;
    [PTTencentRequestHandler payWithOrder:order];
}

- (BOOL)isThirdPlatformInstalled:(PTShareType)thirdPlatform {
    return [TencentOAuth iphoneQQInstalled] || [TencentOAuth iphoneTIMInstalled];
}

@end
複製程式碼

平臺請求的類實現

平臺請求的類是對SDK提供的api的一層封裝,把這層單獨出來也是為了實現更好的職責分離,主要功能包含如下,這些功能是可選的,可以選擇其中的任意個實現:

  • 第三方授權登入
  • 第三方支付
  • 第三方分享
@implementation PTTencentRequestHandler

// 第三方授權
+ (BOOL)sendAuthInViewController:(UIViewController *)viewController {
    NSArray* permissions = [NSArray arrayWithObjects:@"get_user_info",@"get_simple_userinfo", @"add_t", nil];
    BOOL result = [[PTTencentRespManager sharedInstance].tencentOAuth authorize:permissions inSafari:NO];
    return result;
}

// 分享
+ (BOOL)sendMessageWithModel:(ThirdPlatformShareModel *)model {
    QQApiObject* obj;
    if (PTShareContentTypeVideo == model.mediaObject.contentType) {
        // PTSharedVideoObject* mediaObj = (PTSharedVideoObject*)model.mediaObject;
        obj = [QQApiVideoObject objectWithURL:[NSURL URLWithString:ValueOrEmpty(model.urlString)] title:model.title description:model.text previewImageURL:[NSURL URLWithString:ValueOrEmpty(model.imageUrlString)]];
    } else {
       obj = [QQApiNewsObject
           objectWithURL:[NSURL URLWithString:ValueOrEmpty(model.urlString)]
           title:model.title
           description:model.text
           previewImageURL:[NSURL URLWithString:ValueOrEmpty(model.imageUrlString)]];
    }
    SendMessageToQQReq *req = [SendMessageToQQReq reqWithContent:obj];
    QQApiSendResultCode sent = 0;
    if (PTShareTypeQQ == model.platform) {
        //將內容分享到qq
        sent = [QQApiInterface sendReq:req];
    } else {
        //將內容分享到qzone
        sent = [QQApiInterface SendReqToQZone:req];
    }
    return EQQAPISENDSUCESS == sent;
}

// 支付
+ (BOOL)payWithOrder:(OrderModel*)order {
    // 發起支付
    NSString* appID = [[PTThirdPlatformManager sharedInstance] appIDWithPlaform:PTThirdPlatformTypeTencentQQ subType:PTThirdPlatformSubTypePay];
    NSString* scheme = [[PTThirdPlatformManager sharedInstance] URLSchemesWithPlaform:PTThirdPlatformTypeTencentQQ subType:PTThirdPlatformSubTypePay];
    [[QQWalletSDK sharedInstance] startPayWithAppId:appID
                                        bargainorId:order.prepayid
                                            tokenId:order.package
                                          signature:order.sign
                                              nonce:order.noncestr
                                             scheme:scheme
                                         completion:^(QQWalletErrCode errCode, NSString *errStr){
                                             // 支付完成的回撥處理
                                             if (errCode == QQWalletErrCodeSuccess) {
                                                 // 對支付成功的處理
                                                 [[PTTencentRespManager sharedInstance] setPayResult:PTPayResultSuccess];
                                             } else if (errCode == QQWalletErrCodeUserCancel) {
                                                 // 對支付取消的處理
                                                 [[PTTencentRespManager sharedInstance] setPayResult:PTPayResultCancel];
                                             } else {
                                                 // 對支付失敗的處理
                                                 [[PTTencentRespManager sharedInstance] setPayResult:PTPayResultFailed];
                                             }
                                         }];
    return YES;
}

@end
複製程式碼

平臺回撥的類實現

平臺回撥的介面設計處理單個平臺的SDK回撥,一般的是第三方APP通過URL的跨程式通訊,跳轉到宿主APP,傳遞結果引數。這個回撥不同的第三方平臺設計的介面不一樣,所以在我們的平臺回撥的介面設計種就把這種差異化統一,使用自定義的回撥,把資料通過統一的方法回撥到上層去。

平臺回撥類處理的是第三方APP通過- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation方法進行跨程式資料通訊的一些結果,一般的是重寫SDK對應的Delegate協議的方法,還有可能對資料進行二次處理,最終返回給上層的是統一可直接使用的資料物件。比如在這個例子中,重寫的是TencentSessionDelegate協議的- (void)getUserInfoResponse:(APIResponse*) response方法,再把獲取到的資料進行二次整理,最後封裝為上層可用的ThirdPlatformUserInfo類的物件。另外有的SDK採用的不是這種Delegate協議回撥方法的方式,而是採用block回撥的方式,我這裡採用的方式是在平臺回撥的類中新增一個公有方法,把資料傳到這層,然後再資料進行二次處理回撥給上層,這樣雖然增加了步驟,但是資料流父更加的統一。比如QQ錢包支付,回撥使用的是block的方式,我會把結果通過PTTencentRespManager定義的公有方法- (void)setPayResult:(PTPayResult)payResult,把結果資料傳遞到這層來處理,然後統一回撥

@implementation PTTencentRespManager

DEF_SINGLETON

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSString* appID = [[PTThirdPlatformManager sharedInstance] appIDWithPlaform:PTThirdPlatformTypeTencentQQ subType:PTThirdPlatformSubTypeAuthShare];
        _tencentOAuth = [[TencentOAuth alloc] initWithAppId:appID andDelegate:self];
    }
    return self;
}

- (TencentOAuth *)tencentOAuth {
    if (!_tencentOAuth) {
        NSString* appID = [[PTThirdPlatformManager sharedInstance] appIDWithPlaform:PTThirdPlatformTypeTencentQQ subType:PTThirdPlatformSubTypeAuthShare];
        _tencentOAuth = [[TencentOAuth alloc] initWithAppId:appID andDelegate:self];
    }
    return _tencentOAuth;
}

#pragma mark - ......::::::: TencentLoginDelegate :::::::......

/**
 * 登入成功後的回撥
 */
- (void)tencentDidLogin {
    NSLog(@"===");
    [self.tencentOAuth getUserInfo];
}

/**
 * 登入失敗後的回撥
 * \param cancelled 代表使用者是否主動退出登入
 */
- (void)tencentDidNotLogin:(BOOL)cancelled {
    NSLog(@"===");
    if ([self.delegate respondsToSelector:@selector(respManagerDidRecvAuthResponse:platform:)]) {
        [self.delegate respManagerDidRecvAuthResponse:nil platform:PTThirdPlatformTypeTencentQQ];
    }
}

/**
 * 登入時網路有問題的回撥
 */
- (void)tencentDidNotNetWork {
    NSLog(@"===");
    if ([self.delegate respondsToSelector:@selector(respManagerDidRecvAuthResponse:platform:)]) {
        [self.delegate respManagerDidRecvAuthResponse:nil platform:PTThirdPlatformTypeTencentQQ];
    }
}


#pragma mark - ......::::::: TencentSessionDelegate :::::::......

/**
 * 獲取使用者個人資訊回撥
 * \param response API返回結果,具體定義參見sdkdef.h檔案中\ref APIResponse
 * \remarks 正確返回示例: \snippet example/getUserInfoResponse.exp success
 *          錯誤返回示例: \snippet example/getUserInfoResponse.exp fail
 */
- (void)getUserInfoResponse:(APIResponse*) response {
    NSLog(@"===");
    if (URLREQUEST_SUCCEED == response.retCode
        && kOpenSDKErrorSuccess == response.detailRetCode) {
        ThirdPlatformUserInfo *user = [self.class userbyTranslateTencentResult:response.jsonResponse];
        user.userId = self.tencentOAuth.openId;
        user.tokenString = self.tencentOAuth.accessToken;
        if ([self.delegate respondsToSelector:@selector(respManagerDidRecvAuthResponse:platform:)]) {
            [self.delegate respManagerDidRecvAuthResponse:user platform:PTThirdPlatformTypeTencentQQ];
        }
    } else {
        if ([self.delegate respondsToSelector:@selector(respManagerDidRecvAuthResponse:platform:)]) {
            [self.delegate respManagerDidRecvAuthResponse:nil platform:PTThirdPlatformTypeTencentQQ];
        }
    }
}

+ (ThirdPlatformUserInfo *)userbyTranslateTencentResult:(id)result {
    ThirdPlatformUserInfo *user = [[ThirdPlatformUserInfo alloc] init];
    user.thirdPlatformType = PTThirdPlatformTypeTencentQQ;
    
    if ([result isKindOfClass:[NSDictionary class]]) {
        user.gender = [result objectForKey:@"gender"];
        user.username = [result objectForKey:@"nickname"];
        user.head = [result objectForKey:@"figureurl_qq_2"];
        NSString *year = [result objectForKeyedSubscript:@"year"];
        NSDateFormatter *dateFoematter = [[NSDateFormatter alloc] init];
        [dateFoematter setDateFormat:@"yyyy"];
        NSString *currDate = [dateFoematter stringFromDate:[NSDate date]];
        int ageNum = [currDate intValue] - [year intValue];
        user.age = [NSString stringWithFormat:@"%d",ageNum];
    }
    return user;
}

/**
 * 社交API統一回撥介面
 * \param response API返回結果,具體定義參見sdkdef.h檔案中\ref APIResponse
 * \param message 響應的訊息,目前支援‘SendStory’,‘AppInvitation’,‘AppChallenge’,‘AppGiftRequest’
 */
- (void)responseDidReceived:(APIResponse*)response forMessage:(NSString *)message {
    NSLog(@"===");
}


/**
 處理來至QQ的請求
 */
- (void)onReq:(QQBaseReq *)req {
    NSLog(@"===");
}

/**
 處理來至QQ的響應
 */
- (void)onResp:(QQBaseResp *)resp {
    NSLog(@"===");
    if ([resp isKindOfClass:[SendMessageToQQResp class]]) {
        if ([self.delegate respondsToSelector:@selector(respManagerDidRecvMessageResponse:platform:)]) {
            if ([resp.result isEqualToString:@"0"]) {
                [self.delegate respManagerDidRecvMessageResponse:YES platform:PTShareTypeQQ];
            } else {
                [self.delegate respManagerDidRecvMessageResponse:NO platform:PTShareTypeQQ];
            }
        }
    }
}

/**
 處理QQ線上狀態的回撥
 */
- (void)isOnlineResponse:(NSDictionary *)response {
    NSLog(@"===");
}

#pragma mark - ......::::::: Public :::::::......

- (void)setPayResult:(PTPayResult)payResult {
    if ([self.delegate respondsToSelector:@selector(respManagerDidRecvPayResponse:platform:)]) {
        [self.delegate respManagerDidRecvPayResponse:payResult platform:PTThirdPlatformTypeTencentQQ];
    }
}

@end
複製程式碼

平臺管理類

平臺管理類本身也是一個平臺處理類,在這基礎上新增了配置引數處理功能以及平臺擴充套件的功能

從以下三個方面分析和實現平臺管理類的功能:

  • 平臺管理類管理平臺處理類的實現
  • 平臺的配置引數處理
  • 平臺管理類提供的擴充套件點
平臺管理類管理平臺處理類的實現

平臺管理類管理平臺處理類簡單來說就是把平臺型別和對應的處理類做成成一個配置,在平臺管理類中定義的如下的資料來儲存這些配置和關係。在配置的時候會查詢不同型別平臺對應的實現類,然後通過執行時生成類物件,最終把請求轉發給這個生成類物件處理,下面是配置的程式碼段

// 配置管理類的類名
- (NSMutableSet*)thirdPlatformManagerClasses {
    if (nil == _thirdPlatformManagerClasses) {
        _thirdPlatformManagerClasses = [[NSMutableSet alloc] init];
        [_thirdPlatformManagerClasses
         addObjectsFromArray:@[@"PTAlipayManager",
                               @"PTTencentManager",
                               @"PTWeiboManager",
                               @"PTWXManager",
                               ]];
    }
    return _thirdPlatformManagerClasses;
}

// 配置第三方登入支付對應的管理類
- (NSMutableDictionary*)thirdPlatformManagerConfig {
    if (nil == _thirdPlatformManagerConfig) {
        _thirdPlatformManagerConfig = [[NSMutableDictionary alloc] init];
        [_thirdPlatformManagerConfig addEntriesFromDictionary:
         @{
           @(PTThirdPlatformTypeWechat): @"PTWXManager",
           @(PTThirdPlatformTypeTencentQQ): @"PTTencentManager",
           @(PTThirdPlatformTypeWeibo): @"PTWeiboManager",
           @(PTThirdPlatformTypeAlipay): @"PTAlipayManager",
           }];
    }
    return _thirdPlatformManagerConfig;
}

// 配置第三方分享對應的管理類
- (NSMutableDictionary*)thirdPlatformShareManagerConfig {
    if (nil == _thirdPlatformShareManagerConfig) {
        _thirdPlatformShareManagerConfig = [[NSMutableDictionary alloc] init];
        [_thirdPlatformShareManagerConfig addEntriesFromDictionary:
         @{
           @(PTShareTypeWechat): @"PTWXManager",
           @(PTShareTypeWechatLine): @"PTWXManager",
           @(PTShareTypeQQ): @"PTTencentManager",
           @(PTShareTypeQQZone): @"PTTencentManager",
           @(PTShareTypeWeibo): @"PTWeiboManager",
           }];
    }
    return _thirdPlatformShareManagerConfig;
}
複製程式碼

在配置第三方平臺時使用到上面的配置

/**
 第三方平臺配置
 */
- (void)thirdPlatConfigWithApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    for (NSString* classString in [self thirdPlatformManagerClasses]) {
        id<PTAbsThirdPlatformManager> manager = [self managerFromClassString:classString];
        if (manager && [manager conformsToProtocol:@protocol(PTAbsThirdPlatformManager)]) {
            [manager thirdPlatConfigWithApplication:application didFinishLaunchingWithOptions:launchOptions];
        }
    }
}
複製程式碼

在第三方平臺分享時使用到上面的配置

/**
 第三方分享

 @param model 分享資料
 */
- (void)shareWithModel:(ThirdPlatformShareModel *)model {
    NSString* classString = [[self thirdPlatformShareManagerConfig] objectForKey:@(model.platform)];
    id<PTAbsThirdPlatformManager> manager = [self managerFromClassString:classString];
    [manager shareWithModel:model];
}
複製程式碼

此外,第三方平臺處理授權登入、支付、處理跨程式通訊的回撥URL時候也會使用到上面的配置,不在一一例舉了

平臺的配置引數處理

因為QQ登入分享和QQ錢包支付雖然是屬於同一個平臺,但是卻需要整合不同的SDK以及不同的配置,所以需要兩個配置,我採用的方式是定義一個主型別列舉和定義一個子型別列舉來處理,比如主型別列舉如下:

// 第三方平臺型別
typedef NS_ENUM(NSInteger, PTThirdPlatformType) {
    PTThirdPlatformTypeWechat = 100,//微信
    PTThirdPlatformTypeTencentQQ,//QQ
    PTThirdPlatformTypeWeibo,//微博
    PTThirdPlatformTypeAlipay,//支付寶
};
複製程式碼

子型別列舉如下:

// 第三方平臺型別對應的子型別
typedef NS_ENUM(NSInteger, PTThirdPlatformSubType) {
    PTThirdPlatformSubTypeTotal = 1,//所有的子型別,不區分
    PTThirdPlatformSubTypeAuthShare,//分享授權子型別
    PTThirdPlatformSubTypePay,//支付子型別
};
複製程式碼

設定的資料儲存在一個NSDictionary裡面,以及可以使用一系列的方法來獲取配置資訊,相關部分的程式碼如下,更詳細的可開啟我的Demo功能來檢視

- (BOOL)setPlaform:(PTThirdPlatformType)platformType
           subType:(PTThirdPlatformSubType)subType
             appID:(NSString *)appID
            appKey:(NSString *)appKey
         appSecret:(NSString *)appSecret
       redirectURL:(NSString *)redirectURL
        URLSchemes:(NSString*)URLSchemes {
    NSDictionary* subTypeConfig = @{@(PTThirdPlatformAppID): ValueOrEmpty(appID),
                                    @(PTThirdPlatformAppKey): ValueOrEmpty(appKey),
                                    @(PTThirdPlatformAppSecret): ValueOrEmpty(appSecret),
                                    @(PTThirdPlatformRedirectURI): ValueOrEmpty(redirectURL),
                                    @(PTThirdPlatformURLSchemes): ValueOrEmpty(URLSchemes),
                                    };
    
    if (![self.thirdPlatformKeysConfig objectForKey:@(platformType)]) {
        [self.thirdPlatformKeysConfig setObject:[@{} mutableCopy] forKey:@(platformType)];
    }
    
    [[self.thirdPlatformKeysConfig objectForKey:@(platformType)] setObject:subTypeConfig forKey:@(subType)];
    return YES;
}

//......

- (NSString*)appIDWithPlaform:(PTThirdPlatformType)platformType {
    return [self appIDWithPlaform:platformType subType:PTThirdPlatformSubTypeTotal];
}

- (NSString*)appKeyWithPlaform:(PTThirdPlatformType)platformType {
    return [self appKeyWithPlaform:platformType subType:PTThirdPlatformSubTypeTotal];
}

- (NSString*)appSecretWithPlaform:(PTThirdPlatformType)platformType {
    return [self appSecretWithPlaform:platformType subType:PTThirdPlatformSubTypeTotal];
}

- (NSString*)appRedirectURLWithPlaform:(PTThirdPlatformType)platformType {
    return [self appRedirectURLWithPlaform:platformType subType:PTThirdPlatformSubTypeTotal];
}

- (NSString*)URLSchemesWithPlaform:(PTThirdPlatformType)platformType {
    return [self URLSchemesWithPlaform:platformType subType:PTThirdPlatformSubTypeTotal];
}

- (NSString*)appIDWithPlaform:(PTThirdPlatformType)platformType subType:(PTThirdPlatformSubType)subType {
    return [[[self.thirdPlatformKeysConfig objectForKey:@(platformType)] objectForKey:@(subType)] objectForKey:@(PTThirdPlatformAppID)];
}

- (NSString*)appKeyWithPlaform:(PTThirdPlatformType)platformType subType:(PTThirdPlatformSubType)subType {
    return [[[self.thirdPlatformKeysConfig objectForKey:@(platformType)] objectForKey:@(subType)] objectForKey:@(PTThirdPlatformAppKey)];
}

- (NSString*)appSecretWithPlaform:(PTThirdPlatformType)platformType subType:(PTThirdPlatformSubType)subType {
    return [[[self.thirdPlatformKeysConfig objectForKey:@(platformType)] objectForKey:@(subType)] objectForKey:@(PTThirdPlatformAppSecret)];
}

- (NSString*)appRedirectURLWithPlaform:(PTThirdPlatformType)platformType subType:(PTThirdPlatformSubType)subType {
    return [[[self.thirdPlatformKeysConfig objectForKey:@(platformType)] objectForKey:@(subType)] objectForKey:@(PTThirdPlatformRedirectURI)];
}

- (NSString*)URLSchemesWithPlaform:(PTThirdPlatformType)platformType subType:(PTThirdPlatformSubType)subType {
    return [[[self.thirdPlatformKeysConfig objectForKey:@(platformType)] objectForKey:@(subType)] objectForKey:@(PTThirdPlatformURLSchemes)];
}
複製程式碼
平臺管理類提供的擴充套件點

在上面的步驟中,已經把平臺型別和平臺處理類形成了配置,擴充套件點其實就是把自定義的平臺型別和對應的平臺處理類新增到這些配置後面就行了,管理類中提供了的擴充套件點方法有以下兩個

#pragma mark 外掛接入點

/**
 外掛接入點-新增登入或者是支付的管理類
 
 @param platformType 自定義的第三方平臺型別,大於999
 @param managerClass 實現了PTAbsThirdPlatformManager介面的自定義第三方平臺管理類
 */
- (void)addCustomPlatform:(NSInteger)platformType managerClass:(Class)managerClass {
    NSString* classString = NSStringFromClass(managerClass);
    if (classString) {
        [self.thirdPlatformManagerConfig setObject:NSStringFromClass(managerClass) forKey:@(platformType)];
        [self.thirdPlatformManagerClasses addObject:classString];
    }
}

/**
 外掛接入點-新增分享的管理類
 
 @param sharePlatformType 自定義的第三方平臺分享型別,大於999
 @param managerClass 實現了PTAbsThirdPlatformManager介面的自定義第三方平臺管理類
 */
- (void)addCustomSharePlatform:(NSInteger)sharePlatformType managerClass:(Class)managerClass {
    NSString* classString = NSStringFromClass(managerClass);
    if (classString) {
        [self.thirdPlatformShareManagerConfig setObject:classString forKey:@(sharePlatformType)];
        [self.thirdPlatformManagerClasses addObject:classString];
    }
}
複製程式碼

下面以釘釘分享為例,實現一個自定義的第三方平臺,使用擴充套件點介面擴充套件該功能,釘釘平臺處理類的類名為PTDingTalkManager,使用擴充套件點介面新增該平臺處理類的程式碼如下,具體的實現可以檢視Demo程式碼

    // 自定義的第三方平臺以外掛的方式新增
    PTThirdPlatformManager* configInstance = [PTThirdPlatformManager sharedInstance];
    [configInstance addCustomSharePlatform:PTCustumShareTypeDingTalk
                              managerClass:PTDingTalkManager.class];
    [configInstance setPlaform:PTCustumShareTypeDingTalk
                         appID:kDingTalkAppID
                        appKey:nil
                     appSecret:nil
                   redirectURL:nil
                    URLSchemes:nil];
複製程式碼

總結

以上就是第三發平臺元件化解耦實踐的一些總結,如有不妥之處,還請不吝賜教。

相關文章