[iOS] 通知详解: iOS 10 UserNotifications -- 自定义通知UI

通知相关系列文章
iOS10 之前通知使用介绍
[iOS] 通知详解: UIUserNotification
iOS10 相关API
[iOS] 通知详解:iOS 10 UserNotifications API
iOS10 本地/远程通知
[iOS] 通知详解: iOS 10 UserNotifications
iOS10 通知附加包
[iOS] 通知详解: iOS 10 UserNotifications -- 附加包Media Attachments
iOS10 自定义UI
[iOS] 通知详解: iOS 10 UserNotifications -- 自定义通知UI

新建 Notification content extension

通知UI的自定义使用到了Notification content extension,同创建Notification Service Extension一样,我们需要创建一个新的 Target ,只不过这次选择Notification content extension

下一步,为这个Target起一个名字,完成即可!

可以看到多了下面几个文件:


这里的NotificationViewController就是我们编写自定义UI逻辑的控制器,他和一般的控制器一样,MainInterface.storyboard是与其绑定的,可以在此往storyboard添加控件。Info.plist为其相关的配置文件,有些操作需要在这里配置一些设置后,才能看到预期的效果,下面关于此部分的所有配置,都是在这里进行的。

NotificationViewController中,实现了UNNotificationContentExtension协议,他有两个协议方法

// 必须实现,用来处理自定义UI的内容
public func didReceive(_ notification: UNNotification)
// 选择实现,用来处理action的事件
optional public func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void)

第一个是必须要实现的,在NotificationViewController默认已经实现了,主要是处理当通知来的时候,布局自定义的UI内容以及相关的处理逻辑的地方;
第二个方法,当前发送的通知带有快捷操作action的时候(UNNotificationAction),来处理相关的点击事件。

因为我们自定义的任何View都是无法交互的,只能借助添加的action来处理相关的事件。

绑定 Category

Notification content extension添加完成后,在通知界面是看不到我们自定义的UI的,还需要绑定相关的 Category,即在创建通知的时候,我们添加的UNNotificationCategory,如果没有需要交互的action,可以传个空数组:

let category = UNNotificationCategory(identifier: "categoryidentifier", actions: [], intentIdentifiers: [], options: UNNotificationCategoryOptions.customDismissAction)
        UNUserNotificationCenter.current().setNotificationCategories(Set.init([category]))

然后在该扩展下的Info.plist中添加该Categoryidentifier,对应着UNNotificationExtensionCategory字段:

注意:这里的UNNotificationExtensionCategory可以修改为数组类型,如果我们有多个Category公用一套UI,可以将此值修改为Array类型,然后在数组里添加多个 Category 的identifier。

再去发送通知,注意此时的Payload中要添加category字段:

{
"aps":
    {
        "alert":
        {
            "title":"iOS10远程推送标题",
            "subtitle" : "iOS10 远程推送副标题",
            "body":"这是在iOS10以上版本的推送内容,并且携带来一个图片附件"
        },

"category":"categoryidentifier",
        "badge":1,
        "mutable-content":1,
        "sound":"default",
"image":"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3078873712,1340878922&fm=26&gp=0.jpg"
        
    }
}

弹框和锁屏页显示的内容和之前一样,打开通知或者下拉弹框,就会看到我们自定义的页面了:

比较丑的那部分就是我们自定义的UI了,可以看到真的很丑,大小还不合适,而且和系统默认的也重复的。

如果我们想要隐藏系统默认的内容页面,也就是下面的那部分,头是隐藏不了的;只需要在Info.plist里添加字段UNNotificationExtensionDefaultContentHidden,bool类型并设置其值为YES;

关于页面太大的问题,有的说通过修改其宽高比UNNotificationExtensionInitialContentSizeRatio的值,如果你的UI是固定的,可以通过适配大部分屏幕后,通过修改此值来得到合适的宽高比视图,但其值也是需要各种尝试的。
另外也可以使用autolayout,如果是在storyboard里添加的实图,顺便添加相应的约束即可;然后重新发送消息,大概就是这个样子:

这样,通知页面会先显示一个大的页面,然后再resize到约束后的页面大小,这样就会一个缩放的动画,这是因为在通知即将展示的时候,系统还没有调用我们的约束代码,也就是约束还没有起效,所以会有个resize的动画过渡。
为解决这个问题,只能在自定义UI的时候配合UNNotificationExtensionInitialContentSizeRatio设置合适页面大小,即采用固定的样式来展示通知内容。

显示附加包(attachment)的内容

如果我们的通知是携带附加包的,例如一张图片,添加自定义的UI后,打开通知或者下拉弹框会发现,大图不显示了,我们可以把相关的内容显示到自定义的UI上,还是以图片为例,在didReceive方法里添加以下获取附加包数据的代码:

if let att = notification.request.content.attachments.first {

            if att.url.startAccessingSecurityScopedResource() {
                self.coverImage.image  = UIImage(contentsOfFile: att.url.path)
                att.url.stopAccessingSecurityScopedResource()
            }
        }

这里需要说一下startAccessingSecurityScopedResourcestopAccessingSecurityScopedResource方法:
因为attachment是由系统单独管理的,所以这里我们在使用attachment之前,需要告诉iOS系统,我们需要使用它,并且在使用完毕之后告诉系统我们使用完毕了。对应上述代码就是startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()的操作。当我们获取到了attachment的使用权之后,我们就可以使用那个文件获取我们想要的信息了。

再去发送上面的Payload,打开后就是这样了:

意思是那么个意思,但是加载的图片好像不太完整,上面我们是从attachment里面获取的,目前不清楚出现这个情况的原因,可能原数据被压缩了导致数据不全。所以,我们可以从发送的Payload中来获取数据:

if let aps = notification.request.content.userInfo["aps"] as? [String: Any] {

            if let imagePath = aps["image"] as? String {

                if let url = URL(string: imagePath) {

                    if let data = try? Data.init(contentsOf: url) {

                        self.coverImage.image = UIImage(data: data)
                    }
                }
            }
        }

这样就能正常显示了:

处理action事件

如果我们添加的category是带有action的,并且action的点击事件要响应到我们自定义的UI里面,例如点击的时候更换一个图片, 就需要UNNotificationContentExtension协议的另一个协议方法了:

// response:可以拿到点击的action,和通知的内容
// completion:处理完成后需要告诉系统,接下来该如何处理该通知
optional public func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void)

UNNotificationContentExtensionResponseOption 是一个枚举,他有三个值:

@available(iOS 10.0, *)
public enum UNNotificationContentExtensionResponseOption : UInt {

    // 通知页面不会消失,例如更新UI,显示出来
    case doNotDismiss
// 关闭当前通知页面
    case dismiss
// 将此action事件传递给app,在通知中心的代理方法里继续处理该事件
    case dismissAndForwardAction
}

需要注意的是,如果实现了此方法,就需要对所有添加的action进行处理,而不能只处理某个action

例如我们这样处理点击事件:

func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {
// 改变标题
        self.label?.text = self.label?.text ?? "" + "点击了 "
        
        if response.actionIdentifier == "okidentifier" {
            // 点击了查看按钮,这里改变了标题的颜色

            self.label?.textColor = UIColor.red
            
            completion(.doNotDismiss)
        } else if response.actionIdentifier == "cancelidentifier" {
            // 点击了关闭,直接关闭通知
            completion(.dismiss)
        } else {
            // 如果还有其他的按钮,交给app处理
            completion(.dismissAndForwardAction)
        }
    }

然后,在创建通知的时候,添加相应的action:

let okAction = UNNotificationAction(identifier: "okidentifier", title: "查看", options: UNNotificationActionOptions.foreground)
   
        let cancel = UNNotificationAction(identifier: "cancelidentifier", title: "关闭", options: UNNotificationActionOptions.destructive)
        
        let category = UNNotificationCategory(identifier: "categoryidentifier", actions: [okAction, cancel], intentIdentifiers: [], options: UNNotificationCategoryOptions.customDismissAction)
        UNUserNotificationCenter.current().setNotificationCategories(Set.init([category]))

再次发生Payload,点击通知的查看action,会发现标题和标题的颜色都修改了。

处理快捷回复(输入文字)

前面知道,我们可以在通知中心进行快捷回复,只需要创建UNTextInputNotificationAction的action,添加到对应的category即可:

let okAction = UNTextInputNotificationAction(identifier: "okidentifier", title: "回复", options: .foreground, textInputButtonTitle: "快捷回复", textInputPlaceholder: "请输入。。。")
        
        
        let cancel = UNNotificationAction(identifier: "cancelidentifier", title: "关闭", options: UNNotificationActionOptions.destructive)
        
        let category = UNNotificationCategory(identifier: "categoryidentifier", actions: [okAction, cancel], intentIdentifiers: [], options: UNNotificationCategoryOptions.customDismissAction)
        UNUserNotificationCenter.current().setNotificationCategories(Set.init([category]))

这里,我们需要这样来处理接收到的反馈:

func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {
        self.label?.text = "点击了 "
        
        if response.actionIdentifier == "okidentifier" {
            

            // 这里处理输入框的事件
            if let txRes = response as? UNTextInputNotificationResponse {
                // 如果是输入框的反馈,获取输入内容,显示出来
                let text = txRes.userText
                self.label?.text = text
            }
            
            // 点击了查看按钮,这里改变了标题的颜色
            self.label?.textColor = UIColor.red
            
            completion(.doNotDismiss)
        } else if response.actionIdentifier == "cancelidentifier" {
            // 点击了关闭,直接关闭通知
            completion(.dismiss)
        } else {
            // 如果还有其他的按钮,交给app处理
            completion(.dismissAndForwardAction)
        }
    }

然后在通知中心点击回复按钮的时候会弹出输入框,输入结束后,通知中心即显示了输入的内容:

到此,断断续续,总算是把通知相关的内容整体过了一遍,虽然感觉上还是有些逻辑混乱,基本上能够体现通知的一些使用方法。
参考文章
iOS10-UserNotifications
WWDC2016 Session笔记 - iOS 10 推送Notification新特性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容