# iOS13 Apple Push Notification Service (APNS) 部署指南

## 证书配置

在iOS的profile配置文件中，包含有App Identifier，指定的签名证书，以及需要申请的一些系统权限，其中就包括推送通知权限，所以部署APNS需要从证书的生成开始，登录苹果开发者后台<https://developer.apple.com/account> 生成。

## 生成Certificate Signing Request（CSR）文件

打开Keychain App，选择 *证书助理->选择从证书颁发机构请求证书->保存到磁盘*

### 证书申请

1. 点击添加Certificates，选择类别iOS App Developement，选择刚刚的csr文件请求开发者身份证书，并下载到本地备用
2. 点击添加Certificates，选择类别iOS Distribution，选择刚刚的csr文件请求ad hoc和App Store环境用的身份证书，并下载到本地备用
3. 点击添加identifier, 选择App IDs，创建app的bundle identifier
4. 点击添加Certificates，选择类别Apple Push Notification service SSL (Sandbox & Production)，生成推送证书，当然也可以分别选择两种环境进行生成
5. 点击添加Devices，把development环境下需要测试设备的udid添加进去
6. 点击添加profile，分别选择developement和app store环境，选择以上生成的证书进行生成，分别生成两种环境下的profile文件

## 项目配置

### 证书设置

1. 把上面证书申请的所有证书双击添加到钥匙串中
2. 在项目的签名**signing & Capabilities**中，取消automatically manage signing, 在下面手动选择对应环境的profile配置

### 权限声明

选中工程中的**signing & Capabilities**, 选择 **+Capability**，在列表中选择*Push Notification*, *Background Modes*添加到列表中，在*Background Modes*勾选*Remote Notification*

### 环境选择

在上面选择了*Push Notification*之后，系统会自动生成一个entitlement文件在工程的根目录下，同时，在**build setting**的*Code Signing Entitlement*中也会自动指向这个文件的路径。在这个文件中，有一个*APS Environment*的值，可以填写development和production，分别对应推送通知的sandbox和production环境。

### framework

在iOS10以上的版本，需要导入UserNotification.framework框架

## 代码实现

此处代码兼容iOS10以下的版本

### framework导入与版本判断

```
#import <UserNotifications/UserNotifications.h>

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
```

### 遵循协议

UIApplicationDelegate与UNUserNotificationCenterDelegate

### 清零角标

推送通知的角标需要手动调用 `[application setApplicationIconBadgeNumber:0];` 方法进行清0

### 注册推送通知

在`application: didFinishLaunchingWithOptions:`方法中，调用以下方法进行推送通知的注册，同时需要遵循UIApplicationDelegate协议。在iOS10以上版本需要遵循UNUserNotificationCenterDelegate协议来获取用户处理通知的方式

以下分别采用不同方法注册通知

```
- (void)registerForRemoteNotifications {
    if (@available(iOS 10, *)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){
            if (!error){
                dispatch_async(dispatch_get_main_queue(), ^{
                    [[UIApplication sharedApplication] registerForRemoteNotifications];
                    NSLog( @"Push registration success." );
                });
            }
            else {
                NSLog( @"Push registration FAILED" );
                NSLog( @"ERROR: %@ - %@", error.localizedFailureReason, error.localizedDescription );
                NSLog( @"SUGGESTIONS: %@ - %@", error.localizedRecoveryOptions, error.localizedRecoverySuggestion );
            }
        }];
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound |    UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
            [[UIApplication sharedApplication] registerForRemoteNotifications]; 
        });
    }
}
```

### 注册成功后，获取设备token

设备token是注册推送后，苹果服务器返回的一组用来识别当前设备的字符串，需要提交给服务器保存以备识别用户

```
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    const unsigned *tokenBytes = [deviceToken bytes];
    NSString *hexToken = [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])];
    NSLog(@"token: %@", hexToken);
}
```

### 接受通知

接受通知分为冷启动和热启动两种场景，在收到通知的时候，可以获取到通知完整的Dictionary信息

#### 冷启动

冷启动是指app完全没有启动的状态收到推送，然后通过点击推送的方式进行启动app

```
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.


    if (launchOptions) {
        if ([launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSDictionary *dict = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
                NSLog(@"launch:%@", dict);
                [self checkUserInfo:dict];
            });
        }
    }

    [self registerForRemoteNotifications];
    return YES;
}
```

#### 热启动

当前App正在运行中收到的通知

1. 在**iOS 10以上**会回调以下方法，正如注释所描述的，在收到通知，用户未点击时，会返回`userNotificationCenter:willPresentNotification: withCompletionHandler:`方法，而在用户操作之后，根据用户是否dismiss通知，会在`userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:`返回不同的action

```
#pragma mark - UNUserNotificationCenterDelegate
// called when receive notification
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0)){
    NSLog(@"%s", __func__);
    NSLog(@"User Info : %@",notification.request.content.userInfo);
    completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge);

    [self checkUserInfo:notification.request.content.userInfo];
}

//Called to let your app know which action was selected by the user for a given notification.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler API_AVAILABLE(ios(10.0)){
    NSLog(@"%s", __func__);
    if (response.actionIdentifier == UNNotificationDefaultActionIdentifier) {
        NSLog(@"default action");
    }
    else if (response.actionIdentifier == UNNotificationDismissActionIdentifier){
        NSLog(@"Dismiss action");
    }

    NSLog(@"User Info : %@",response.notification.request.content.userInfo);
    completionHandler();
}
```

1. 在**iOS 10**以前的老版本，则会通过`application： didReceiveRemoteNotification：fetchCompletionHandler：`方法触发回调

```
- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void(^)(UIBackgroundFetchResult))completionHandler {
    // iOS 10 will handle notifications through other methods
    if( SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO( @"10.0" ) )
    {
        NSLog( @"iOS version >= 10. Let NotificationCenter handle this one." );
        // set a member variable to tell the new delegate that this is background
        return;
    }
    NSLog( @"HANDLE PUSH, didReceiveRemoteNotification: %@", userInfo );

    // custom code to handle notification content

    if( [UIApplication sharedApplication].applicationState == UIApplicationStateInactive )
    {
        NSLog( @"INACTIVE" );
        completionHandler( UIBackgroundFetchResultNewData );
    }
    else if( [UIApplication sharedApplication].applicationState == UIApplicationStateBackground )
    {
        NSLog( @"BACKGROUND" );
        completionHandler( UIBackgroundFetchResultNewData );
    }
    else
    {
        NSLog( @"FOREGROUND" );
        completionHandler( UIBackgroundFetchResultNewData );
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"fgPush" object:nil userInfo:userInfo];
        NSLog(@"launch:%@", userInfo);
    });
}
```

## 常见问题

### 获取token失败

1. 检查profile与证书配置是否正确
2. 检查网络是否通畅
3. 重启手机

### 推送延时

1. 由于推送通知需要连接国外服务器，所以会有一定的延时，大概在1～2秒左右
2. 第三方不是超级会员之类的，大多会有延时，半小时到两小时不定都有可能
3. 遇到延时问题，可以使用自己的apns服务进行推送测试

### 证书识别错误/找不到证书

1. 关闭XCode
2. 重新导入证书和profile
3. 重启XCode


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://github.gitbook.io/shadowsocks-quick/ios/untitled-1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
