# iOS推送集成文档
# 资源下载:
ThinkingDataPushExtension
正式版本: v1.0.0 Framework 下载 (opens new window)
GitHub:ThinkingDataPushExtension (opens new window)
ThinkingDataAnalyticsExtension
正式版本: v1.0.0 Framework 下载 (opens new window)
GitHub:ThinkingDataAnalyticsExtension (opens new window)
ThinkingDataCore
正式版本: v1.0.1 Framework 下载 (opens new window)
GitHub:ThinkingDataCore (opens new window)
ThinkingSDK
所有的事件上报行为需要依赖于 ThinkingSDK 的上报能力,所以项目首先需要集成 ThinkingSDK。
步骤参照 ThinkingSDK 使用文档 (opens new window)
# 概述
ThinkingSDK v3.0.1 之后的版本可以实现推送点击事件上报。
ThinkingDataPushExtension SDK (iOS 10 以上可用),实现推送触达事件的上报。
# 主 Target 中需要集成以下 SDK
- 建议使用 cocoapods 引入
 - ThinkingSDK 用来实现数据采集功能,推送点击事件上报
 
# 通知扩展 Target 中需要集成以下SDK
- 建议使用 cocoapods 引入
 - ThinkingDataPushExtension 用来实现通知到达事件的自动上报
 
以上SDK都共同依赖 ThinkingDataCore 库,使用 cocoapods 集成时,会自动引入。当使用 Framework 方式时,需要手动引入 ThinkingDataCore 的 Framework 库。
使用 cocoapods 时,ThinkingDataPushExtension 会自动引入 ThinkingDataAnalyticsExtension 库,来实现轻量级的数据采集功能。如果使用 Framework 方式,需要手动导入 ThinkingDataAnalyticsExtension 的 Framework 库。
# 接入流程
- 在 app 主 target 中接入采集SDK版本(>= 3.0.1)
 
pod 'ThinkingSDK', '3.0.1'
- 开启 NotificationService
 
您的项目需要新建一个通知扩展的 Target,例如:TANotification

- 在通知扩展中集成 ThinkingDataPushExtension
 
pod 'ThinkingDataPushExtension'
# 自动回收推送点击方案
# 开启自动回收
NSString *appId = @"appId";
NSString *serverUrl = @"serverUrl";
TDConfig *config = [[TDConfig alloc] initWithAppId:appId serverUrl:serverUrl];
// 开启推送自动回收
config.enableAutoPush = YES;
[TDAnalytics startAnalyticsWithConfig:config];
# FCM 推送
# 上报“推送 ID”
- 集成 FCM 推送 SDK 后:ThinkingSDK 会自动上报(ThinkingSDK 版本需 >= v3.0.1)
 - 账号切换
 
切换账号之后需要调用 login 接口,SDK 内部会自动将推送 token 通过 userSet 接口上报至新的账号。
[TDAnalytics login:@"new_account_id"];
# 采集推送点击事件
自动上报推送点击事件,无需手动操作。
# 极光推送
# 上报“推送 ID”
- 集成极光推送 SDK 后:ThinkingSDK 会自动上报(ThinkingSDK 版本需 >= v3.0.1)
 - 账号切换
 
切换账号之后需要调用 login 接口,SDK 内部会自动将推送 token 通过 userSet 接口上报至新的账号。
[TDAnalytics login:@"new_account_id"];
# 采集推送点击事件
自动上报推送点击事件,无需手动操作。
# APNs 推送
# 上报“推送 ID”
APNs 通道暂时只支持手动上报
- 在调用 login 或者切换账号之后,手动上传 APNs Device Token。
 
// login 之后,再次上报 APNs Device Token
[TDAnalytics login:@"test_id"];
// 示例:获取系统注册的 RemoteNotificationsWithDeviceToken
NSString *token = [self getDviceTokenDemoFunction];
[TDAnalytics userSet:@{ @"#apns_token": token }];
在 - application:didRegisterForRemoteNotificationsWithDeviceToken: 回调中上报 APNs Device Token
// SDK 初始化之后,在 APNs 回调中上报 APNs Device Token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // 建议在此处记录下来 APNs Device Token,以便在切换账号时再次上报
    NSString *token = [self formatDeviceTokenToHexStr:deviceToken];
    [TDAnalytics userSet:@{ @"#apns_token": token }];
}
// (NSData *)deviceToken 转换成 (NSString *)deviceToken
- (NSString *)formatDeviceTokenToHexStr:(NSData *)deviceToken {
    NSString *tokenStr;
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0) {
        const unsigned *tokenBytes = [deviceToken bytes];
        tokenStr = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                 ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                 ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                 ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
    } else {
        tokenStr = [[deviceToken description] stringByReplacingOccurrencesOfString:@"<" withString:@""];
        tokenStr = [tokenStr stringByReplacingOccurrencesOfString:@">" withString:@""];
        tokenStr = [tokenStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    }
    return tokenStr;
}
- 账号切换
 
切换账号之后需要调用 login 接口,SDK 内部会自动将推送 token 通过 userSet 接口上报至新的账号。
[TDAnalytics login:@"new_account_id"];
# 采集推送点击事件
自动上报推送点击事件,无需手动操作。
# 手动回收推送点击方案
# FCM 推送
# 上报 "推送 ID"
- 在调用 login 或者切换账号之后上传FCM的Token。
 
NSString *appId = @"app-id";
NSString *serverUrl = @"server-url";
TDConfig *tdConfig = [[TDConfig alloc] initWithAppId:appId serverUrl:serverUrl];
[TDAnalytics startAnalyticsWithConfig:tdConfig];
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
      if (error != nil) {
        NSLog(@"Error getting FCM registration token: %@", error);
      } else {
        NSLog(@"FCM registration token: %@", token);
        [TDAnalytics userSet:@{@"fcm_token": token}];
      }
}];
# 采集推送点击事件
用户点击推送通知时,可以在系统的推送点击回调中发送推送点击事件。
// use <UserNotifications/UserNotifications.h> framework
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    // trackAppOpenNotification 方法示例见下文《附录》
    [self trackAppOpenNotification:userInfo];
    
    completionHandler();
}
# 极光推送
# 上报 "推送 ID"
- 在调用 TE login或者切换账号之后上传极光的Registration ID。
 
// login 之后,再次上报 registrationID
[TDAnalytics login:@"test_id"];
[TDAnalytics userSet:@{@"jiguang_id": registrationID}];
- 在 - registrationIDCompletionHandler: 回调中上传极光的Registration ID。
 
// TE SDK 初始化之后,在极光回调中上报 registrationID
[JPUSHService registrationIDCompletionHandler:^(int resCode, NSString *registrationID) {
    [TDAnalytics userSet:@{@"jiguang_id": registrationID}];
}];
# 采集推送点击事件
//  iOS10以下,点击通知的回调
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    [JPUSHService handleRemoteNotification:userInfo];
    // trackAppOpenNotification 方法示例见下文《附录》
    [self trackAppOpenNotification:userInfo];
    
    completionHandler(UIBackgroundFetchResultNewData);
}
 
// iOS10以上,点击通知的回调
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler  API_AVAILABLE(ios(10.0)){
    // Required
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    // trackAppOpenNotification 方法示例见下文《附录》
    [self trackAppOpenNotification:userInfo];
    // 系统要求执行这个方法
    completionHandler();  
}
# APNs推送
# 上报 "推送 ID"
- 在调用 TE login或者切换账号之后上传 APNs Device Token。
 
// login 之后,再次上报 APNs Device Token
[TDAnalytics login:@"test_id"];
// 示例:获取系统注册的 RemoteNotificationsWithDeviceToken
NSString *token = [self getDviceTokenDemoFunction];
[TDAnalytics userSet:@{ @"#apns_token": token }];
- 在 - application:didRegisterForRemoteNotificationsWithDeviceToken: 回调中上报 APNs Device Token
 
// SDK 初始化之后,在 APNs 回调中上报 APNs Device Token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // 建议在此处记录下来 APNs Device Token,以便在切换账号时再次上报
    NSString *token = [self formatDeviceTokenToHexStr:deviceToken];
    [TDAnalytics userSet:@{ @"#apns_token": token }];
}
// (NSData *)deviceToken 转换成 (NSString *)deviceToken
- (NSString *)formatDeviceTokenToHexStr:(NSData *)deviceToken {
    NSString *tokenStr;
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0) {
        const unsigned *tokenBytes = [deviceToken bytes];
        tokenStr = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                 ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                 ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                 ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
    } else {
        tokenStr = [[deviceToken description] stringByReplacingOccurrencesOfString:@"<" withString:@""];
        tokenStr = [tokenStr stringByReplacingOccurrencesOfString:@">" withString:@""];
        tokenStr = [tokenStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    }
    return tokenStr;
}
# 采集推送点击事件
//  iOS10以下,点击通知的回调
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    // trackAppOpenNotification 方法示例见下文《附录》
    [self trackAppOpenNotification:userInfo];
    completionHandler(UIBackgroundFetchResultNewData);
}
// iOS10以上,点击通知的回调
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    // trackAppOpenNotification 方法示例见下文《附录》
    [self trackAppOpenNotification:userInfo];
    // 系统要求执行这个方法
    completionHandler();
}
# 采集消息到达事件
在通知扩展的 target 中添加如下代码
#import <ThinkingDataPushExtension/ThinkingDataPushExtension.h>
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    NSString *appId = @"your app id in TE";
    NSString *serverUrl = @"your server url in TE";
    NSString *accountId = @"user account id. AccountId and DistinctId cannot be empty at the same time";
    NSString *distinctId = @"user distinct id. AccountId and DistinctId cannot be empty at the same time";
    BOOL result = [TDPushExtension handleNotificationRequest:request withContentHandler:contentHandler appId:appId serverUrl:serverUrl accountId:accountId distinctID:distinctId];
    if (result) {
        return;
    }
    self.contentHandler(self.bestAttemptContent);
}
参数介绍:
- appId:在 TE 后台可以查看到您项目的 APP ID
 - serverUrl:在 TE 后台可以查看到您项目的 receiver url
 - accountId: 需要你在运营后台配置透传参数,然后这里手动解析出 accountId
 - distinctId: 需要你在运营后台配置透传参数,然后这里手动解析出 distinctId
 
运营后台配置入口:

# 处理推送消息
# FCM 推送
用户点击推送通知时,可以在系统的推送点击回调中获取推送参数。
建议以下两种方案二选一。
- 解析消息类型:调用handleTEPushAction方法。
 - 解析透传参数:调用handlePassThroughAction方法。
 
// use <UserNotifications/UserNotifications.h> framework
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    //如果处理普通参数 调用如下方法
    [self handleTEPushAction:userInfo];
    //如果处理透传参数 调用如下方法
    [self handleTEPassThroughAction:userInfo];
    completionHandler();
}
# 极光推送
建议以下两种方案二选一。
- 解析消息类型:调用handleTEPushAction方法。
 - 解析透传参数:调用handlePassThroughAction方法。
 
//  iOS10以下,点击通知的回调
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    // Required, iOS 7 Support
    [JPUSHService handleRemoteNotification:userInfo];
    //如果处理普通参数 调用如下方法
    [self handleTEPushAction:userInfo];
    //如果处理透传参数 调用如下方法
    [self handleTEPassThroughAction:userInfo];
    completionHandler(UIBackgroundFetchResultNewData);
}
 
// iOS10以上,点击通知的回调
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler  API_AVAILABLE(ios(10.0)){
    // Required
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    //如果处理普通参数 调用如下方法
    [self handleTEPushAction:userInfo];
    //如果处理透传参数 调用如下方法
    [self handleTEPassThroughAction:userInfo];
    // 系统要求执行这个方法
    completionHandler();
}
# APNs 推送
建议以下两种方案二选一。
- 普通参数:调用handleTEPushAction方法。
 - 透传参数:调用handlePassThroughAction方法。
 
//  iOS10以下,点击通知的回调
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  //如果处理普通参数 调用如下方法
  [self handleTEPushAction:userInfo];
  //如果处理透传参数 调用如下方法
  [self handleTEPassThroughAction:userInfo];
  completionHandler(UIBackgroundFetchResultNewData);
}
 
// iOS10以上,点击通知的回调
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    //如果处理普通参数 调用如下方法
    [self handleTEPushAction:userInfo];
    //如果处理透传参数 调用如下方法
    [self handleTEPassThroughAction:userInfo];
    // 系统要求执行这个方法
    completionHandler();
}
# 展示推送图片
需要在通知扩展 target 中集成 ThinkingDataPushExtension SDK。如果你已添加了采集消息到达事件,那么不需要重复此操作。
在通知扩展的 target 中添加如下代码
#import <ThinkingDataPushExtension/ThinkingDataPushExtension.h>
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    NSString *appId = @"your app id in TE";
    NSString *serverUrl = @"your server url in TE";
    NSString *accountId = @"user account id. AccountId and DistinctId cannot be empty at the same time";
    NSString *distinctId = @"user distinct id. AccountId and DistinctId cannot be empty at the same time";
    BOOL result = [TDPushExtension handleNotificationRequest:request withContentHandler:contentHandler appId:appId serverUrl:serverUrl accountId:accountId distinctID:distinctId];
    if (result) {
        return;
    }
    self.contentHandler(self.bestAttemptContent);
}
参数介绍:
- appId:在 TE 后台可以查看到您项目的 APP ID
 - serverUrl:在 TE 后台可以查看到您项目的 receiver url
 - accountId: 需要你在运营后台配置透传参数,然后这里手动解析出 accountId
 - distinctId: 需要你在运营后台配置透传参数,然后这里手动解析出 distinctId
 
运营后台配置入口:

# 附录
# 客户端收到推送参数示例
以下仅展示客户端收到的扩展字段参数
{
    "te_extras": {
        //点击推送的跳转方式
        "ops_loading_type": "OPEN_APP",
        //透传参数
        "passthrough_params": {
                "param1": "abc",
                "param2": 101,
                "param3": [{
                        "subText1": "xyz",
                        "subText2": 2
                }]
        },
        //TE运营通道回执属性
        "#ops_receipt_properties": {
                "ops_task_id": "0082",
                "ops_project_id": 1,
                "ops_task_instance_id": "0082_20230331",
                "ops_push_language": "default",
                "ops_task_exec_detail_id": "55"
        }
    }
}
# 如何验证客户推送回收链路接入成功?
- 接入推送之后,首次启动或者推送token变更时,是否会上报以下事件。
 
{
    "#type": "user_set",
    "#time": "2023-11-13 15:50:55.729",
    "#distinct_id": "distinct",
    "properties": {
        "jiguang_id": "190e35f7e15c8481caa"
    },
    "#uuid": "9f233c31-a664-46ff-94d6-f767a3098c3a"
}
- 点击推送通知启动应用,观察是否会上传te_ops_push_click事件,观察事件属性中是否包含#ops_receipt_properties。
 
{
    "#type": "track",
    "#time": "2023-03-16 16:08:32.191",
    "#distinct_id": "90d80464-6832-43f1-80d9-bd93fc09c4fe",
    "#event_name": "te_ops_push_click",
    "properties": {
        "#lib_version": "3.0.1-beata.1",
        "#carrier": "中国移动",
        "#os": "Android",
        "#device_id": "6262ca7f71e6aca3",
        "#screen_height": 2400,
        "#bundle_id": "cn.thinkingdata.random",
        "#device_model": "M2012K11AC",
        "#screen_width": 1080,
        "#system_language": "zh",
        "#install_time": "2023-03-10 11:24:44.285",
        "#simulator": false,
        "#lib": "Android",
        "#manufacturer": "Xiaomi",
        "#os_version": "11",
        "#app_version": "1.0",
        "#fps": 60,
        "#network_type": "WIFI",
        "#ram": "2.7\/7.4",
        "#disk": "4.6\/106.3",
        "#device_type": "Phone",
        "#ops_receipt_properties": {
            "ops_project_id": 1,
            "ops_request_id": "3b21d2a8-8d3d-44fa-b460-3bb311ed3bcd"
        },
        "#zone_offset": 8
    },
    "#uuid": "7a977e23-b78a-4433-baae-ead17ad2fde9"
}
# trackAppOpenNotification
推送点击事件
- (void)trackAppOpenNotification:(NSDictionary *)userInfo{
    @try {
        if ([userInfo.allKeys containsObject:@"te_extras"] && [userInfo[@"te_extras"] isKindOfClass:[NSString class]]) {
            NSData *jsonData = [userInfo[@"te_extras"] dataUsingEncoding:NSUTF8StringEncoding];
            NSError *err;
            NSDictionary *responseDic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err];
            NSDictionary *opsReceiptProperties = responseDic[@"#ops_receipt_properties"];
            if ([opsReceiptProperties isKindOfClass:[NSString class]]) {
                NSString *opsStr = (NSString *)opsReceiptProperties;
                opsReceiptProperties = [NSJSONSerialization JSONObjectWithData:[opsStr dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:&err];
            }
            if (opsReceiptProperties && [opsReceiptProperties isKindOfClass:[NSDictionary class]]) {
                NSMutableDictionary *pushProperties = [NSMutableDictionary dictionary]; // track 字典
                pushProperties[@"#ops_receipt_properties"] = opsReceiptProperties;
                [TDAnalytics track:@"te_ops_push_click" properties:pushProperties];
                [TDAnalytics flush];
            }
        }
    } @catch (NSException *exception) {
        
    }
}
# handleTEPushAction
解析推送消息类型
- (void)handleTEPushAction:(NSDictionary *)userInfo{
    @try {
        NSString *jsonString = userInfo[@"te_extras"];
        NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        NSDictionary *sfDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
        if (!sfDictionary || error) {
            return;
        }
        NSString *sf_landing_type = sfDictionary[@"ops_loading_type"];
        if ([sf_landing_type isEqualToString:@"OPEN_APP"]) {
            // 打开 App
        }
        else if ([sf_landing_type isEqualToString:@"OPEN_URL"]) {
            // 打开 URL
           NSString *url = sfDictionary[@"ops_url"];
        }
        else if ([sf_landing_type isEqualToString:@"CUSTOMIZED"]) {
           // 处理自定义消息
           NSString *customized = sfDictionary[@"ops_customized"];
        }
        
    } @catch (NSException *exception) {
        
    }
}
# handleTEPassThroughAction
解析透传参数
- (void)handleTEPassThroughAction:(NSDictionary *)userInfo{
    @try {
        NSString *jsonString = userInfo[@"te_extras"];
        NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        NSDictionary *sfDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
        if (!sfDictionary || error) {
            return;
        }
        NSString *params = sfDictionary[@"passthrough_params"];
        if (params) {
            // params为透传参数,接下来是实现具体的业务逻辑
        }
    } @catch (NSException *exception) {
        
    }
}
