iOS

iOS10通知的简单应用

Posted by 易博 on June 14, 2017

iOS10发布的同时苹果推出新的通知框架UserNotification,较之前的消息推送,新的框架有了质的变化。之前提到通知,理解的都是消息推送,新的通知框架在完善消息推送功能的基础上,加入了很多新的功能和权限。本文主要浅析两者的对比和实际应用

新功能概览

1、不再区分本地通知和远程通知,有了统一的行为。远程通知主要是消息推送、本地通知分为以下3种

1. UNTimeIntervalNotificationTrigger 一定时间之后,重复或者不重复推送通知
2. UNCalendarNotificationTrigger 一定日期之后,重复或者不重复推送通知
3. UNLocationNotificationTrigger)地理位置的一种通知,当用户进入或离开一个地理区域来通知

2、应用在前台的时候也能弹出提示。这样就不需要像之前一样自己去做处理来提醒用户

3、能够获取到用户的通知设置信息,甚至修改这些设置。以前的通知是获取不到用户对应用通知相关的设置信息

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];  
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {  
NSLog(@"通知配置信息:\n%@",settings);  
}];

4、通知的发送采用类似网络请求的方式,开发者更加容易接受

5、通知的内容显示增加标题和副标题的区分,主标题加粗显示。通知类型也新增了多媒体通知,能都显示图片、音乐和视频资源

{  
    "aps":{  
        "alert":{  
        "title":"标题",  
        "subtitle":"副标题",  
        "body":"内容"  
        },  
    "sound":"default",  
    "badge":1  
    }  
} 

6、开发者可以自定义通知的UI界面,不再是一成不变的banner

7、支持本地通知的撤回、修改和删除,不论通知是否已经展示还是未到达。目前还不支持远程推送的上述操作

设备注册和获取注册信息

xcode8以上,需要在工程的Capabilities中开启远程通知,并且添加UserNotifications.framework库

//注册
#import <UserNotifications/UserNotifications.h>
-(void)authorizationPushNotificaton:(UIApplication *)application  
{  
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];  
    center.delegate = self; //必须写代理  
    [center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionCarPlay completionHandler:^(BOOL granted, NSError * _Nullable error) {  
    //注册之后的回调  
    if (!error && granted) {  
        NSLog(@"注册成功...");  
    }  
    else{  
        NSLog(@"注册失败...");  
    }  
    }];  

    //获取注册之后的权限设置  
    //注意UNNotificationSettings是只读对象哦,不能直接修改!  
    [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {  
        NSLog(@"通知配置信息:\n%@",settings);  
    }];  

    //注册通知获取token  
    [application registerForRemoteNotifications];  
}
//在App启动之后调用上述方法注册
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
    // Override point for customization after application launch.   

    [UNUserNotificationCenter currentNotificationCenter].delegate = [NotificationHandle shareInstance];  

    [[NotificationHandle shareInstance] authorizationPushNotificaton:application];  

    return YES;  
} 

//注册成功的处理
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{  

    NSString *deviceString = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];  
    deviceString = [deviceString stringByReplacingOccurrencesOfString:@" " withString:@""];  

    NSLog(@"远端获取的deviceToken\n%@",deviceString);  

    //存储得到的token,后面备用  
    [[NSUserDefaults standardUserDefaults] setValue:deviceString forKey:@"deviceToken"];  
    [[NSUserDefaults standardUserDefaults] synchronize];  
}  

//注册失败的处理
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{  
    NSLog(@"获取token失败:%@\n",error.localizedDescription);  
}

发送通知

之前已经提到过,通知发送采用的是类似网络请求的代码风格。我们创建一个通知请求(request),然后将这个请求提交给UNUserNotificationCenter处理,之后会在代理(delegate)中收到这个通知请求对应的回应(response),另外,我们也可以在应用的拓展中对回应进行处理。下面是发送一个简单通知的示例代码段、远程推送的info信息和效果图

//创建通知  
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];  
content.title = @"iOS 10通知";  
content.body = @"这是一个iOS 10的消息通知...";  

//创建一个触发事件  
UNNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeValue repeats:NO];  

//设置通知的唯一标识  
NSString *requestIdentifer = @"timeIntervalNotification";  

//创建通知的请求  
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:trigger];  
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {  
    if (!error) {  
        self.label2.text = error.localizedDescription;  
    }  
    else  
    {  
        self.label2.text = @"发送成功...";  
    }  
}];

{  
    "aps":{  
        "alert":{  
        "title":"iOS 10通知",  
        "body":"这是一个iOS 10的消息通知..."  
        }  
    }  
}

通知的取消和修改

在上述代码段中,我们在发送一个通知请求的时候会有一个标识符,这个标识符就是用来管理通知。利用UserNotifications框架提供的一系列API,可以做到

1. 取消还没有展示的通知,直接调用删除方法
2. 修改还没有展示的通知,用相同的标识符再发送一条通知,即可实现覆盖修改
3. 删除已经展示过的通知,直接调用删除方法
4. 修改已经展示过的通知,用相同的标识符再发送一条通知,即可实现覆盖修改
//创建两个用于测试的消息体  
UNMutableNotificationContent *content1 = [[UNMutableNotificationContent alloc]init];  
content1.title = @"1";  
content1.body = @"通知1";  

UNMutableNotificationContent *content2 = [[UNMutableNotificationContent alloc]init];  
content2.title = @"2";  
content2.body = @"通知2";
  
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];  
NSString *identifier = @"SendAndCancle";  
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content1 trigger:trigger];  

[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {  
    //  
}];  

//延迟2秒之后执行  
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0/*延迟执行时间*/ * NSEC_PER_SEC));  

dispatch_after(delayTime, dispatch_get_main_queue(), ^{  
[[UNUserNotificationCenter currentNotificationCenter] 
    //取消还没有展示的通知
    removePendingNotificationRequestsWithIdentifiers:@[identifier]];  

    //用相同的标识再次发送即可覆盖  
    UNTimeIntervalNotificationTrigger *triggerNew = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3 repeats:NO];  

    UNNotificationRequest *requestNew = [UNNotificationRequest requestWithIdentifier:@"SendAndModify" content:content2 trigger:triggerNew];  

    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:requestNew withCompletionHandler:^(NSError * _Nullable error) {  
        //  
    }];
});

可交互的通知

可交互的通知就是收到通知后,利用3D-Touch可以实现对当前通知的自定义操作。比如示例是收到一条消息后,对消息进行评价操作。首先我们需要向通知注册这些交互事件。input、open、cancle、comment这些标识符用于接收到对应交互事件后的处理判断依据

//注册通知中的action事件  
-(void)registerNotificationCategory  
{  
    //带评论的通知事件注册  
    UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"input" title:@"评论" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"请输入评论内容.."];  
    UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@"open" title:@"打开" options:UNNotificationActionOptionForeground];  
    UNNotificationAction *cancleAction = [UNNotificationAction actionWithIdentifier:@"cancle" title:@"取消" options:UNNotificationActionOptionDestructive];  

    UNNotificationCategory *tapCategory = [UNNotificationCategory categoryWithIdentifier:@"comment" actions:@[inputAction,openAction,cancleAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; 

    //  
    NSSet *set = [[NSSet alloc]initWithObjects:tapCategory, nil nil];  
    [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:set];  
}

//在app启动后调用上述方法注册
[[NotificationHandle shareInstance] registerNotificationCategory];

发送带交互的通知

-(void)btnClicked  
{  
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];  
    content.body = @"这是一个带交互事件的通知";  
    content.title = @"交互事件通知";  
    //此处的唯一标识和NotificationHandle中的一一对应  
    content.categoryIdentifier = @"comment";  

    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3 repeats:NO];  
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"actionable" content:content trigger:trigger];  
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {  
        //  
    }];  
}

//远程推送的payload
{  
    "aps":{  
        "alert":{  
        "title":"事件通知",  
        "body":"这是一个带事件的通知"  
        },  
    "category":"conment"  //标识这是一个交互通知,用于代理事件中获取后处理  
    }  
} 

接收到交互通知的处理,收到通知后都会走didReceiveNotificationResponse这个方法

-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{  
    //收到推送的请求  
    UNNotificationRequest *request = response.notification.request;  
    //收到的内容  
    UNNotificationContent *content = request.content;  
    //收到用户的基本信息  
    NSDictionary *userInfo = content.userInfo;  
    //收到消息的角标  
    NSNumber *badge = content.badge;  
    //收到消息的body  
    NSString *body = content.body;  
    //收到消息的声音  
    UNNotificationSound *sound = content.sound;  
    //推送消息的副标题  
    NSString *subtitle = content.subtitle;  
    //推送消息的标题  
    NSString *title = content.title;  

    if ([response.notification.request.trigger isKindOfClass:[UNNotificationTrigger class]]) {  
        NSLog(@"点击了通知:%@\n",userInfo);  
    }  
    else{  
        NSLog(@"通知:{\nbody:%@,\ntitle:%@,\nsubtitle:%@,\nbadge:%@,\nsound:%@,\nuserInfo:%@}",body,title,subtitle,badge,sound,userInfo);  
    }  

    //处理消息的事件  
    NSString *category = content.categoryIdentifier;  
    if ([category isEqualToString:@"comment"]) {  
        [self handCommnet:response];  
    }

    completionHandler();  
}  

-(void)handCommnet:(UNNotificationResponse *)response  
{  
    NSString *actionType = response.actionIdentifier;  
    NSString *textStr = @"";  

    if ([actionType isEqualToString:@"input"]) {  
        UNTextInputNotificationResponse *temp = (UNTextInputNotificationResponse *)response;  
        textStr = temp.userText;  
    }  
    else if ([actionType isEqualToString:@"open"])  
    {  
        textStr = @"open";  
    }  
    else if ([actionType isEqualToString:@"cancle"])  
    {  
        textStr = @"";  
    }  

    NSLog(@"你刚输入的内容是:%@",textStr);  
}

多媒体通知

新的通知框架,允许用户发送多媒体通知。发送多媒体通知的方法也很简单,只需要通过文件的NSURL创建一个 UNNotificationAttachment对象,然后将这个对象放到数组中赋值给content的attachments属性就行了。如果需要实现远程的多媒体通知,那就需要用到通知拓展,在收到带多媒体标记的通知后,在通知展现之前将多媒体的资源下载下来,然后将附件追加到通知体的attachments中。多媒体通知仅可以发送一个多媒体资源,要发送多个的话需要采用通知拓展来实现。多媒体通知支持的文件类型和大小限制如下

1. 音频资源最大不能超过5M,可支持Wav、MP3、MPEG4格式
2. 图片资源最大不能超过10M,可支持JPEG、GIF、PNG格式
3. 视频资源最大不能超过50M,可支持MPEG、MPEG2、MPEG4、AVI格式
-(void)btnClicked  
{  
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];  
    content.title = @"多媒体通知";  
    content.body = @"显示一个图片";   

    NSString *imageUrlStr = @"http://192.1668.8.100/www2/img/r8.jpg";  

    [self downloadAndSave:[[NSURL alloc] initWithString:imageUrlStr] handler:^(NSURL *localUrl) {  

        UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"attachment" URL:localUrl options:nil error:nil];  

        content.attachments = @[attachment];  

        UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];  
        NSString *identifier = @"media";  
        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];  
        [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {  
            //  
        }];  
    }];  
}  

-(void)downloadAndSave:(NSURL *)url handler: (void (^)(NSURL *localUrl)) handler  
{  
    NSURLRequest *request = [NSURLRequest requestWithURL:url];  

    NSURLSession *session = [NSURLSession sharedSession];  
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {  
    // location是沙盒中临时目录下的一个url,文件下载后会存到这个位置,  
    //由于临时目录中的文件随时可能被删除,建议自己把下载的文件挪到需要的地方  
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];  
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path] error:nil];  
        handler([NSURL fileURLWithPath:path]);  
    }];  
    [task resume];  
}

Notification Service Extension(通知服务拓展)

与通知相关的拓展(extension)有两个:Notification Service Extension(通知服务拓展)和 Notification Content Extension(通知内容拓展)。前者可以让我们有机会在收到远程推送的通知后,展示之前对通知内容进行修改;后者可以用来自定义通知视图的样式。

Notification Service Extension现在只对远程推送的通知有效,且需要在推送payload中增加一个mutable-content值为1的项表示启用内容修改。开启后在didReceiveNotificationRequest方法中拦截通知进行处理。下面的示例代码段展示的是修改远程通知的内容以及远程多媒体通知的实现

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {  
    self.contentHandler = contentHandler;  
    self.bestAttemptContent = [request.content mutableCopy];  

    // Modify the notification content here...  

    if([self.bestAttemptContent.categoryIdentifier isEqualToString:@"modify"])  
    {  
        //如果远程推送的payload中含有modify标识,则表示需要修改消息内容  
        self.bestAttemptContent.body = [NSString stringWithFormat:@"%@,modify by NotificationService",self.bestAttemptContent.body];  
        self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];  

        self.contentHandler(self.bestAttemptContent);  
    }  
    else if([self.bestAttemptContent.categoryIdentifier isEqualToString:@"media"])  
    {  
        //如果远程推送的payload中含有media标识,则表示需要处理多媒体通知,包括图片、音乐、视频  
        NSString *mediaUrlStr = [self.bestAttemptContent.userInfo objectForKey:@"mediaUrl"];  
        NSURL *mediaUrl = [[NSURL alloc]initWithString:mediaUrlStr];  

        [self downloadAndSave:mediaUrl handler:^(NSURL *localUrl) {  
        UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"attachment" URL:localUrl options:nil error:nil];  
            self.bestAttemptContent.attachments = @[attachment];  
            self.contentHandler(self.bestAttemptContent);  
        }];  
    }  
    else  
    {  
        self.contentHandler(self.bestAttemptContent);  
    }  
}

//远程多媒体推送的payload
{  
    "aps":{  
        "alert":{  
        "title":"多媒体通知",  
        "body":"发送了一个美女的图片"  
        },  
    "mutable-content":1,
    "category":"media"  
    },  
    "mediaUrl":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1490937858917&di=f2c49174f5d70fc92683e3b8f2bc2f45&imgtype=0&src=http%3A%2F%2Fwww.zhiyinlady.com%2Fd%2Ffile%2Fyule%2Fbayule%2F20170221%2F20170220150816873mlfamznlthd.jpg"  
} 

Notification Content Extension(通知内容拓展)

创建一个通知内容拓展,这个extension中有一个必须实现的方法 didReceiveNotification(),在系统需要显示自定义样式的通知详情视图时,这个方法将被调用,而我们只需要在其中配置自己想要的UI界面,而UI本身可以通过这个extension中的MainInterface.storyboard来进行定义

示例的UI界面如下图,imageView用来显示图片、textview用来显示标题,label用来显示内容,布局可自行定义

需要在didReceiveNotification(收到远程消息)以及didReceiveNotificationResponse(交互事件获取)中处理UI上的事件

//收到远程消息  
- (void)didReceiveNotification:(UNNotification *)notification {  

    UNNotificationContent *content = notification.request.content;  
    //获取通知中发过来的需要展示的内容  
    NSMutableArray *imageArr = [[NSMutableArray alloc]initWithArray:[content.userInfo objectForKey:@"items"]];  

    self.itemArr = [[NSMutableArray alloc]init];  

    for (int i = 0; i < imageArr.count; i++) {  
        //创建数据模型,包含url、title、text三个属性  
        NotificationItem *item = [[NotificationItem alloc]init];  
        if (i > content.attachments.count - 1) {  
            continue;  
        }  

        item.title = [[imageArr objectAtIndex:i] objectForKey:@"title"];  
        item.text = [[imageArr objectAtIndex:i] objectForKey:@"text"];  
        item.url = [[NSURL alloc] initWithString:[[imageArr objectAtIndex:i] objectForKey:@"imageUrl"]];  
        [self.itemArr addObject:item];  
    }  
    [self uploadUI:0];  
}  

//更新界面  
-(void)uploadUI:(NSInteger)index  
{  
    NotificationItem *item = [self.itemArr objectAtIndex:index];  

    //用sd_webimage来获取远程图片  
    [self.imageView sd_setImageWithURL:item.url placeholderImage:[UIImage imageNamed:@"11"]];  
    self.label.text = item.title;  
    self.textView.text = item.text;  

    self.index = index;  
}  

//交互事件的获取  
-(void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption))completion  
{  
    //用户点击了切换  
    if ([response.actionIdentifier isEqualToString:@"switch"]) {  
        if (0 == self.index) {  
            self.index = 1;  
        }  
        else  
        {  
            self.index = 0;  
        }  
        [self uploadUI:self.index];  
        completion(UNNotificationContentExtensionResponseOptionDoNotDismiss);  
    }  
    else if ([response.actionIdentifier isEqualToString:@"open"])  
    {  
        completion(UNNotificationContentExtensionResponseOptionDismissAndForwardAction);  
    }  
    else if ([response.actionIdentifier isEqualToString:@"cancle"])  
    {  
        completion(UNNotificationContentExtensionResponseOptionDismiss);  
    }  
    else  
    {  
        completion(UNNotificationContentExtensionResponseOptionDismissAndForwardAction);  
    }  
}

自定义UI的通知是和通知category绑定的,我们需要在extension的Info.plist里指定这个通知样式所对应的category标识符,这样这个拓展才会起作用

1. UNNotificationExtensionCategory:表示需要绑定的category,这个可以修改为数组,这样就可以为这个content绑定多个通知
2. UNNotificationExtensionInitialContentSizeRatio:默认的UI界面的高宽比,修改这个值为UI界面的款高比,这样在界面出来的时候不会有很突兀的frame改变
3. UNNotificationExtensionDefaultContentHidden:是否显示系统默认的标题栏和内容,可选参数
4. UNNotificationExtensionOverridesDefaultTitle:是否让系统采用消息的标题作为通知的标题,可选参数

具体的参数说明可以参考下面这个官方文档 https://developer.apple.com/reference/usernotificationsui/unnotificationcontentextension

//发送通知
-(void)btnClicked  
{  
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];  
    content.title = @"多媒体通知";  
    content.body = @"显示多张图片";  

    content.userInfo = @{@"items":@[@{@"title":@"奥迪1",@"text":@"奥迪R8",@"imageUrl":@"http://172.20.90.117/www2/img/r8.jpg"},  
    @{@"title":@"奥迪2",@"text":@"奥迪超跑",@"imageUrl":@"http://172.20.90.117/www2/img/r8-1.jpg"}]};  

    content.categoryIdentifier = @"customUI";  

    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];  
    NSString *indentifier = @"customUI";  
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:indentifier content:content trigger:trigger];  
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {  
        //  
    }];  
} 

//远程通知的payload
{  
    "aps":{  
        "alert":{  
        "title":"多媒体通知",  
        "body":"显示多张图片"  
        },  
    "category":"customUI"  
    },  
    "items": [  
    {  
        "title": "奥迪1",  
        "text": "奥迪R8",  
        "imageUrl": "http://172.20.90.117/www2/img/r8.jpg"  
    },  
    {  
        "title": "奥迪2",  
        "text": "奥迪超跑",  
        "imageUrl": "http://172.20.90.117/www2/img/r8-1.jpg"  
    }  
    ]  
}