iOS10 适配汇总

来源:互联网 发布:淘宝等级权限 编辑:程序博客网 时间:2024/06/06 11:02

iOS 10 中以前杂乱的和通知相关的 API 都被统一了,现在开发者可以使用独立的 UserNotifications.framework 来集中管理和使用 iOS 系统中通知的功能。在此基础上,Apple 还增加了撤回单条通知,更新已展示通知,中途修改通知内容,在通知中展示图片视频,自定义通知 UI 等一系列新功能,非常强大。

对于开发者来说,相较于之前版本,iOS 10 提供了一套非常易用通知处理接口,是 SDK 的一次重大重构。而之前的绝大部分通知相关 API 都已经被标为弃用 (deprecated)。

这篇文章将首先回顾一下 Notification 的发展历史和现状,然后通过一些例子来展示 iOS 10 SDK 中相应的使用方式,来说明新 SDK 中通知可以做的事情以及它们的使用方式。

您可以在 WWDC 16 的 Introduction to Notifications 和 Advanced Notifications 这两个 Session 中找到详细信息;另外也不要忘了参照 UserNotifications 的官方文档以及本文的实例项目 UserNotificationDemo。

Notification 历史和现状

碎片化时间是移动设备用户在使用应用时的一大特点,用户希望随时拿起手机就能查看资讯,处理事务,而通知可以在重要的事件和信息发生时提醒用户。完美的通知展示可以很好地帮助用户使用应用,体现出应用的价值,进而有很大可能将用户带回应用,提高活跃度。正因如此,不论是 Apple 还是第三方开发者们,都很重视通知相关的开发工作,而通知也成为了很多应用的必备功能,开发者们都希望通知能带来更好地体验和更多的用户。

但是理想的丰满并不能弥补现实的骨感。自从在 iOS 3 引入 Push Notification 后,之后几乎每个版本 Apple 都在加强这方面的功能。我们可以回顾一下整个历程和相关的主要 API:

iOS 3 - 引入推送通知UIApplication 的 registerForRemoteNotificationTypes 与 UIApplicationDelegate 的 application(_:didRegisterForRemoteNotificationsWithDeviceToken:),application(_:didReceiveRemoteNotification:)iOS 4 - 引入本地通知scheduleLocalNotification,presentLocalNotificationNow:,application(_:didReceive:)iOS 5 - 加入通知中心页面iOS 6 - 通知中心页面与 iCloud 同步iOS 7 - 后台静默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:)iOS 8 - 重新设计 notification 权限请求,Actionable 通知registerUserNotificationSettings(_:),UIUserNotificationAction 与 UIUserNotificationCategory,application(_:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等iOS 9 - Text Input action,基于 HTTP/2 的推送请求UIUserNotificationActionBehavior,全新的 Provider API 等

有点晕,不是么?一个开发者很难在不借助于文档的帮助下区分 application(_:didReceiveRemoteNotification:) 和 application(_:didReceiveRemoteNotification:fetchCompletionHandle:),新入行的开发者也不可能明白 registerForRemoteNotificationTypes 和 registerUserNotificationSettings(_:) 之间是不是有什么关系,Remote 和 Local Notification 除了在初始化方式之外那些细微的区别也让人抓狂,而很多 API 都被随意地放在了 UIApplication 或者 UIApplicationDelegate 中。除此之外,应用已经在前台时,远程推送是无法直接显示的,要先捕获到远程来的通知,然后再发起一个本地通知才能完成现实。更让人郁闷的是,应用在运行时和非运行时捕获通知的路径还不一致。虽然这些种种问题都是由一定历史原因造成的,但不可否认,正是混乱的组织方式和之前版本的考虑不周,使得 iOS 通知方面的开发一直称不上“让人愉悦”,甚至有不少“坏代码”的味道。

另一方面,现在的通知功能相对还是简单,我们能做的只是本地或者远程发起通知,然后显示给用户。虽然 iOS 8 和 9 中添加了按钮和文本来进行交互,但是已发出的通知不能更新,通知的内容也只是在发起时唯一确定,而这些内容也只能是简单的文本。 想要在现有基础上扩展通知的功能,势必会让原本就盘根错节的 API 更加难以理解。

在 iOS 10 中新加入 UserNotifications 框架,可以说是 iOS SDK 发展到现在的最大规模的一次重构。新版本里通知的相关功能被提取到了单独的框架,通知也不再区分类型,而有了更统一的行为。我们接下来就将由浅入深地解析这个重构后的框架的使用方式。

UserNotifications 框架解析

基本流程

iOS 10 中通知相关的操作遵循下面的流程:

首先你需要向用户请求推送权限,然后发送通知。对于发送出的通知,如果你的应用位于后台或者没有运行的话,系统将通过用户允许的方式 (弹窗,横幅,或者是在通知中心) 进行显示。如果你的应用已经位于前台正在运行,你可以自行决定要不要显示这个通知。最后,如果你希望用户点击通知能有打开应用以外的额外功能的话,你也需要进行处理。

权限申请

通用权限

iOS 8 之前,本地推送 (UILocalNotification) 和远程推送 (Remote Notification) 是区分对待的,应用只需要在进行远程推送时获取用户同意。iOS 8 对这一行为进行了规范,因为无论是本地推送还是远程推送,其实在用户看来表现是一致的,都是打断用户的行为。因此从 iOS 8 开始,这两种通知都需要申请权限。iOS 10 里进一步消除了本地通知和推送通知的区别。向用户申请通知权限非常简单:

?
1
2
3
4
5
6
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
    granted, error in
    ifgranted {
        // 用户允许进行通知
    }
}

当然,在使用 UN 开头的 API 的时候,不要忘记导入 UserNotifications 框架:

?
1
importUserNotifications

第一次调用这个方法时,会弹出一个系统弹窗。

8K月薪!学门真技术 找个好工作
【点击进入】
java/c++/web/js/app/html5/前端/软件…… 达内带薪IT培训,钱程无忧

要注意的是,一旦用户拒绝了这个请求,再次调用该方法也不会再进行弹窗,想要应用有机会接收到通知的话,用户必须自行前往系统的设置中为你的应用打开通知,而这往往是不可能的。因此,在合适的时候弹出请求窗,在请求权限前预先进行说明,而不是直接粗暴地在启动的时候就进行弹窗,会是更明智的选择。

远程推送

一旦用户同意后,你就可以在应用中发送本地通知了。不过如果你通过服务器发送远程通知的话,还需要多一个获取用户 token 的操作。你的服务器可以使用这个 token 将用向 Apple Push Notification 的服务器提交请求,然后 APNs 通过 token 识别设备和应用,将通知推给用户。

提交 token 请求和获得 token 的回调是现在“唯二”不在新框架中的 API。我们使用 UIApplication 的 registerForRemoteNotifications 来注册远程通知,在 AppDelegate 的 application(_:didRegisterForRemoteNotificationsWithDeviceToken) 中获取用户 token:

?
1
2
3
4
5
6
7
8
// 向 APNs 请求 token:
UIApplication.shared.registerForRemoteNotifications()
 
// AppDelegate.swift
 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let tokenString = deviceToken.hexString
    print("Get Push token: \(tokenString)")
}

获取得到的 deviceToken 是一个 Data 类型,为了方便使用和传递,我们一般会选择将它转换为一个字符串。Swift 3 中可以使用下面的 Data 扩展来构造出适合传递给 Apple 的字符串:

?
1
2
3
4
5
6
7
8
9
extension Data {
    var hexString: String {
        returnwithUnsafeBytes {(bytes: UnsafePointer<uint8>) -> String in
            let buffer = UnsafeBufferPointer(start: bytes, count: count)
            returnbuffer.map {String(format: "%02hhx", $0)}.reduce("", { $0+ $1})
        }
    }
}
</uint8>

权限设置

用户可以在系统设置中修改你的应用的通知权限,除了打开和关闭全部通知权限外,用户也可以限制你的应用只能进行某种形式的通知显示,比如只允许横幅而不允许弹窗及通知中心显示等。一般来说你不应该对用户的选择进行干涉,但是如果你的应用确实需要某种特定场景的推送的话,你可以对当前用户进行的设置进行检查:

?
1
2
3
4
5
6
UNUserNotificationCenter.current().getNotificationSettings {
    settings in
    print(settings.authorizationStatus)// .authorized | .denied | .notDetermined
    print(settings.badgeSetting)// .enabled | .disabled | .notSupported
    // etc...
}

关于权限方面的使用,可以参考 Demo 中 AuthorizationViewController 的内容。

发送通知

UserNotifications 中对通知进行了统一。我们通过通知的内容 (UNNotificationContent),发送的时机 (UNNotificationTrigger) 以及一个发送通知的 String 类型的标识符,来生成一个 UNNotificationRequest 类型的发送请求。最后,我们将这个请求添加到 UNUserNotificationCenter.current() 中,就可以等待通知到达了:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 创建通知内容
let content = UNMutableNotificationContent()
content.title = "Time Interval Notification"
content.body = "My first notification"
 
// 2. 创建发送触发
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
 
// 3. 发送请求标识符
let requestIdentifier = "com.onevcat.usernotification.myFirstNotification"
 
// 4. 创建一个发送请求
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
 
// 将请求添加到发送中心
UNUserNotificationCenter.current().add(request) { error in
    iferror == nil {
        print("Time Interval Notification scheduled: \(requestIdentifier)")
    }
}

iOS 10 中通知不仅支持简单的一行文字,你还可以添加 title 和 subtitle,来用粗体字的形式强调通知的目的。对于远程推送,iOS 10 之前一般只含有消息的推送 payload 是这样的:

?
1
2
3
4
5
6
7
{
  "aps":{
    "alert":"Test",
    "sound":"default",
    "badge":1
  }
}

如果我们想要加入 title 和 subtitle 的话,则需要将 alert 从字符串换为字典,新的 payload 是:

?
1
2
3
4
5
6
7
8
9
10
11
{
  "aps":{
    "alert":{
      "title":"I am title",
      "subtitle":"I am subtitle",
      "body":"I am body"
    },
    "sound":"default",
    "badge":1
  }
}

好消息是,后一种字典的方法其实在 iOS 8.2 的时候就已经存在了。虽然当时 title 只是用在 Apple Watch 上的,但是设置好 body 的话在 iOS 上还是可以显示的,所以针对 iOS 10 添加标题时是可以保证前向兼容的。

另外,如果要进行本地化对应,在设置这些内容文本时,本地可以使用 String.localizedUserNotificationString(forKey: "your_key", arguments: []) 的方式来从 Localizable.strings 文件中取出本地化字符串,而远程推送的话,也可以在 payload 的 alert 中使用 loc-key 或者 title-loc-key 来进行指定。关于 payload 中的 key,可以参考这篇文档。

触发器是只对本地通知而言的,远程推送的通知的话默认会在收到后立即显示。现在 UserNotifications 框架中提供了三种触发器,分别是:在一定时间后触发 UNTimeIntervalNotificationTrigger,在某月某日某时触发 UNCalendarNotificationTrigger 以及在用户进入或是离开某个区域时触发 UNLocationNotificationTrigger。

请求标识符可以用来区分不同的通知请求,在将一个通知请求提交后,通过特定 API 我们能够使用这个标识符来取消或者更新这个通知。我们将在稍后再提到具体用法。

在新版本的通知框架中,Apple 借用了一部分网络请求的概念。我们组织并发送一个通知请求,然后将这个请求提交给 UNUserNotificationCenter 进行处理。我们会在 delegaet 中接收到这个通知请求对应的 response,另外我们也有机会在应用的 extension 中对 request 进行处理。我们在接下来的章节会看到更多这方面的内容。

在提交通知请求后,我们锁屏或者将应用切到后台,并等待设定的时间后,就能看到我们的通知出现在通知中心或者屏幕横幅了:

8K月薪!学门真技术 找个好工作
【点击进入】
java/c++/web/js/app/html5/前端/软件…… 达内带薪IT培训,钱程无忧

关于最基础的通知发送,可以参考 Demo 中 TimeIntervalViewController 的内容。

取消和更新

在创建通知请求时,我们已经指定了标识符。这个标识符可以用来管理通知。在 iOS 10 之前,我们很难取消掉某一个特定的通知,也不能主动移除或者更新已经展示的通知。想象一下你需要推送用户账户内的余额变化情况,多次的余额增减或者变化很容易让用户十分困惑 - 到底哪条通知才是最正确的?又或者在推送一场比赛的比分时,频繁的通知必然导致用户通知中心数量爆炸,而大部分中途的比分对于用户来说只是噪音。

iOS 10 中,UserNotifications 框架提供了一系列管理通知的 API,你可以做到:

取消还未展示的通知更新还未展示的通知移除已经展示过的通知更新已经展示过的通知

其中关键就在于在创建请求时使用同样的标识符。

比如,从通知中心中移除一个展示过的通知:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false)
let identifier = "com.onevcat.usernotification.notificationWillBeRemovedAfterDisplayed"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
 
UNUserNotificationCenter.current().add(request) { error in
    iferror != nil {
        print("Notification request added: \(identifier)")
    }
}
 
delay(4) {
    print("Notification request removed: \(identifier)")
    UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
}

类似地,我们可以使用 removePendingNotificationRequests,来取消还未展示的通知请求。对于更新通知,不论是否已经展示,都和一开始添加请求时一样,再次将请求提交给 UNUserNotificationCenter 即可:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// let request: UNNotificationRequest = ...
UNUserNotificationCenter.current().add(request) { error in
    iferror != nil {
        print("Notification request added: \(identifier)")
    }
}
 
delay(2) {
    let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
 
    // Add new request with the same identifier to update a notification.
    let newRequest = UNNotificationRequest(identifier: identifier, content:newContent, trigger: newTrigger)
    UNUserNotificationCenter.current().add(newRequest) { error in
        iferror != nil {
            print("Notification request updated: \(identifier)")
        }
    }
}

远程推送可以进行通知的更新,在使用 Provider API 向 APNs 提交请求时,在 HTTP/2 的 header 中 apns-collapse-id key 的内容将被作为该推送的标识符进行使用。多次推送同一标识符的通知即可进行更新。

对应本地的 removeDeliveredNotifications,现在还不能通过类似的方式,向 APNs 发送一个包含 collapse id 的 DELETE 请求来删除已经展示的推送,APNs 服务器并不接受一个 DELETE 请求。不过从技术上来说 Apple 方面应该不存在什么问题,我们可以拭目以待。现在如果想要消除一个远程推送,可以选择使用后台静默推送的方式来从本地发起一个删除通知的调用。关于后台推送的部分,可以参考我之前的一篇关于 iOS7 中的多任务的文章。

关于通知管理,可以参考 Demo 中 ManagementViewController 的内容。为了能够简单地测试远程推送,一般我们都会用一些方便发送通知的工具,Knuff 就是其中之一。我也为 Knuff 添加了 apns-collapse-id 的支持,你可以在这个 fork 的 repo 或者是原 repo 的 pull request 中找到相关信息。

处理通知

应用内展示通知

现在系统可以在应用处于后台或者退出的时候向用户展示通知了。不过,当应用处于前台时,收到的通知是无法进行展示的。如果我们希望在应用内也能显示通知的话,需要额外的工作。

UNUserNotificationCenterDelegate 提供了两个方法,分别对应如何在应用内展示通知,和收到通知响应时要如何处理的工作。我们可以实现这个接口中的对应方法来在应用内展示通知:

?
1
2
3
4
5
6
7
8
9
10
11
classNotificationHandler: NSObject, UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                       willPresent notification: UNNotification,
                       withCompletionHandler completionHandler: @escaping(UNNotificationPresentationOptions) -> Void)
    {
        completionHandler([.alert, .sound])
 
        // 如果不想显示某个通知,可以直接用空 options 调用 completionHandler:
        // completionHandler([])
    }
}

实现后,将 NotificationHandler 的实例赋值给 UNUserNotificationCenter 的 delegate 属性就可以了。没有特殊理由的话,AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 就是一个不错的选择:

?
1
2
3
4
5
6
7
classAppDelegate: UIResponder, UIApplicationDelegate {
    let notificationHandler = NotificationHandler()
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        UNUserNotificationCenter.current().delegate = notificationHandler
        returntrue
    }
}

对通知进行响应

UNUserNotificationCenterDelegate 中还有一个方法,userNotificationCenter(_:didReceive:withCompletionHandler:)。这个代理方法会在用户与你推送的通知进行交互时被调用,包括用户通过通知打开了你的应用,或者点击或者触发了某个 action (我们之后会提到 actionable 的通知)。因为涉及到打开应用的行为,所以实现了这个方法的 delegate 必须在 applicationDidFinishLaunching: 返回前就完成设置,这也是我们之前推荐将 NotificationHandler 尽早进行赋值的理由。

一个最简单的实现自然是什么也不错,直接告诉系统你已经完成了所有工作。

?
1
2
3
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler:@escaping() -> Void) {
    completionHandler()
}

想让这个方法变得有趣一点的话,在创建通知的内容时,我们可以在请求中附带一些信息:

?
1
2
3
4
5
let content = UNMutableNotificationContent()
content.title = "Time Interval Notification"
content.body = "My first notification"
 
content.userInfo = ["name":"onevcat"]

在该方法里,我们将获取到这个推送请求对应的 response,UNNotificationResponse 是一个几乎包括了通知的所有信息的对象,从中我们可以再次获取到 userInfo 中的信息:

?
1
2
3
4
5
6
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler:@escaping() -> Void) {
    iflet name = response.notification.request.content.userInfo["name"] as? String {
        print("I know it's you! \(name)")
    }
    completionHandler()
}

更好的消息是,远程推送的 payload 内的内容也会出现在这个 userInfo 中,这样一来,不论是本地推送还是远程推送,处理的路径得到了统一。通过 userInfo 的内容来决定页面跳转或者是进行其他操作,都会有很大空间。

Actionable 通知发送和处理

注册 Category

iOS 8 和 9 中 Apple 引入了可以交互的通知,这是通过将一簇 action 放到一个 category 中,将这个 category 进行注册,最后在发送通知时将通知的 category 设置为要使用的 category 来实现的。

8K月薪!学门真技术 找个好工作
【点击进入】
java/c++/web/js/app/html5/前端/软件…… 达内带薪IT培训,钱程无忧

注册一个 category 非常容易:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
privatefunc registerNotificationCategory() {
    let saySomethingCategory: UNNotificationCategory = {
        // 1
        let inputAction = UNTextInputNotificationAction(
            identifier:"action.input",
            title:"Input",
            options: [.foreground],
            textInputButtonTitle:"Send",
            textInputPlaceholder:"What do you want to say...")
 
        // 2
        let goodbyeAction = UNNotificationAction(
            identifier:"action.goodbye",
            title:"Goodbye",
            options: [.foreground])
 
        let cancelAction = UNNotificationAction(
            identifier:"action.cancel",
            title:"Cancel",
            options: [.destructive])
 
        // 3
        returnUNNotificationCategory(identifier:"saySomethingCategory", actions: [inputAction, goodbyeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction])
    }()
 
    UNUserNotificationCenter.current().setNotificationCategories([saySomethingCategory])
}
UNTextInputNotificationAction 代表一个输入文本的 action,你可以自定义框的按钮 title 和 placeholder。你稍后会使用 identifier 来对 action 进行区分。普通的 UNNotificationAction 对应标准的按钮。为 category 指定一个 identifier,我们将在实际发送通知的时候用这个标识符进行设置,这样系统就知道这个通知对应哪个 category 了。

当然,不要忘了在程序启动时调用这个方法进行注册:

?
1
2
3
4
5
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    registerNotificationCategory()
    UNUserNotificationCenter.current().delegate = notificationHandler
    returntrue
}

发送一个带有 action 的通知

在完成 category 注册后,发送一个 actionable 通知就非常简单了,只需要在创建 UNNotificationContent 时把 categoryIdentifier 设置为需要的 category id 即可:

?
1
content.categoryIdentifier = "saySomethingCategory"

尝试展示这个通知,在下拉或者使用 3D touch 展开通知后,就可以看到对应的 action 了:

上海自力教育--总有你想学!
【点击进入】
中国领先职业教育机构,开设自考学历, 职业资格,会计金融,教师资格,日语英语课程

远程推送也可以使用 category,只需要在 payload 中添加 category 字段,并指定预先定义的 category id 就可以了:

?
1
2
3
4
5
6
{
  "aps":{
    "alert":"Please say something",
    "category":"saySomething"
  }
}

处理 actionable 通知

和普通的通知并无二致,actionable 通知也会走到 didReceive 的 delegate 方法,我们通过 request 中包含的 categoryIdentifier 和 response 里的 actionIdentifier 就可以轻易判定是哪个通知的哪个操作被执行了。对于 UNTextInputNotificationAction 触发的 response,直接将它转换为一个 UNTextInputNotificationResponse,就可以拿到其中的用户输入了:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler:@escaping() -> Void) {
 
    iflet category = UserNotificationCategoryType(rawValue: response.notification.request.content.categoryIdentifier) {
        switchcategory {
        case.saySomething:
            handleSaySomthing(response: response)
        }
    }
    completionHandler()
}
 
privatefunc handleSaySomthing(response: UNNotificationResponse) {
    let text: String
 
    iflet actionType = SaySomethingCategoryAction(rawValue: response.actionIdentifier) {
        switchactionType {
        case.input: text = (response as! UNTextInputNotificationResponse).userText
        case.goodbye: text = "Goodbye"
        case.none: text = ""
        }
    }else{
        // Only tap or clear. (You will not receive this callback when user clear your notification unless you set .customDismissAction as the option of category)
        text = ""
    }
 
    if!text.isEmpty {
        UIAlertController.showConfirmAlertFromTopViewController(message:"You just said \(text)")
    }
}

上面的代码先判断通知响应是否属于 "saySomething",然后从用户输入或者是选择中提取字符串,并且弹出一个 alert 作为响应结果。当然,更多的情况下我们会发送一个网络请求,或者是根据用户操作更新一些 UI 等。

关于 Actionable 的通知,可以参考 Demo 中 ActionableViewController 的内容。

Notification Extension

iOS 10 中添加了很多 extension,作为应用与系统整合的入口。与通知相关的 extension 有两个:Service Extension 和 Content Extension。前者可以让我们有机会在收到远程推送的通知后,展示之前对通知内容进行修改;后者可以用来自定义通知视图的样式。

截取并修改通知内容

NotificationService 的模板已经为我们进行了基本的实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
classNotificationService: UNNotificationServiceExtension {
 
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
 
    // 1
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler:@escaping(UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
 
        iflet bestAttemptContent = bestAttemptContent {
            ifrequest.identifier == "mutableContent"{
                bestAttemptContent.body = "\(bestAttemptContent.body), onevcat"
            }
            contentHandler(bestAttemptContent)
        }
    }
 
    // 2
    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        iflet contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}
didReceive: 方法中有一个等待发送的通知请求,我们通过修改这个请求中的 content 内容,然后在限制的时间内将修改后的内容调用通过 contentHandler 返还给系统,就可以显示这个修改过的通知了。在一定时间内没有调用 contentHandler 的话,系统会调用这个方法,来告诉你大限已到。你可以选择什么都不做,这样的话系统将当作什么都没发生,简单地显示原来的通知。可能你其实已经设置好了绝大部分内容,只是有很少一部分没有完成,这时你也可以像例子中这样调用 contentHandler 来显示一个变更“中途”的通知。

Service Extension 现在只对远程推送的通知起效,你可以在推送 payload 中增加一个 mutable-content 值为 1 的项来启用内容修改:

?
1
2
3
4
5
6
7
8
9
{
  "aps":{
    "alert":{
      "title":"Greetings",
      "body":"Long time no see"
    },
    "mutable-content":1
  }
}

这个 payload 的推送得到的结果,注意 body 后面附上了名字。

使用在本机截取推送并替换内容的方式,可以完成端到端 (end-to-end) 的推送加密。你在服务器推送 payload 中加入加密过的文本,在客户端接到通知后使用预先定义或者获取过的密钥进行解密,然后立即显示。这样一来,即使推送信道被第三方截取,其中所传递的内容也还是安全的。使用这种方式来发送密码或者敏感信息,对于一些金融业务应用和聊天应用来说,应该是必备的特性。

在通知中展示图片/视频

相比于旧版本的通知,iOS 10 中另一个亮眼功能是多媒体的推送。开发者现在可以在通知中嵌入图片或者视频,这极大丰富了推送内容的可读性和趣味性。

为本地通知添加多媒体内容十分简单,只需要通过本地磁盘上的文件 URL 创建一个 UNNotificationAttachment 对象,然后将这个对象放到数组中赋值给 content 的 attachments 属性就行了:

?
1
2
3
4
5
6
7
8
9
let content = UNMutableNotificationContent()
content.title = "Image Notification"
content.body = "Show me an image!"
 
iflet imageURL = Bundle.main.url(forResource: "image", withExtension: "jpg"),
   let attachment = try? UNNotificationAttachment(identifier: "imageAttachment", url: imageURL, options: nil)
{
    content.attachments = [attachment]
}

在显示时,横幅或者弹窗将附带设置的图片,使用 3D Touch pop 通知或者下拉通知显示详细内容时,图片也会被放大展示:

除了图片以外,通知还支持音频以及视频。你可以将 MP3 或者 MP4 这样的文件提供给系统来在通知中进行展示和播放。不过,这些文件都有尺寸的限制,比如图片不能超过 5MB,视频不能超过 50MB 等,不过对于一般的能在通知中展示的内容来说,这个尺寸应该是绰绰有余了。关于支持的文件格式和尺寸,可以在文档中进行确认。在创建 UNNotificationAttachment 时,如果遇到了不支持的格式,SDK 也会抛出错误。

通过远程推送的方式,你也可以显示图片等多媒体内容。这要借助于上一节所提到的通过 Notification Service Extension 来修改推送通知内容的技术。一般做法是,我们在推送的 payload 中指定需要加载的图片资源地址,这个地址可以是应用 bundle 内已经存在的资源,也可以是网络的资源。不过因为在创建 UNNotificationAttachment 时我们只能使用本地资源,所以如果多媒体还不在本地的话,我们需要先将其下载到本地。在完成 UNNotificationAttachment 创建后,我们就可以和本地通知一样,将它设置给 attachments 属性,然后调用 contentHandler 了。

简单的示例 payload 如下:

?
1
2
3
4
5
6
7
8
9
10
{
  "aps":{
    "alert":{
      "title":"Image Notification",
      "body":"Show me an image from web!"
    },
    "mutable-content":1
  },
  "image":"https://onevcat.com/assets/images/background-cover.jpg"
}

mutable-content 表示我们会在接收到通知时对内容进行更改,image 指明了目标图片的地址。

在 NotificationService 里,加入如下代码来下载图片,并将其保存到磁盘缓存中:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
privatefunc downloadAndSave(url: URL, handler: @escaping(_ localURL: URL?) -> Void) {
    let task = URLSession.shared.dataTask(with: url, completionHandler: {
        data, res, error in
 
        var localURL: URL? = nil
 
        iflet data = data {
            let ext = (url.absoluteString as NSString).pathExtension
            let cacheURL = URL(fileURLWithPath: FileManager.default.cachesDirectory)
            let url = cacheURL.appendingPathComponent(url.absoluteString.md5).appendingPathExtension(ext)
 
            iflet _ = try? data.write(to: url) {
                localURL = url
            }
        }
 
        handler(localURL)
    })
 
    task.resume()
}

然后在 didReceive: 中,接收到这类通知时提取图片地址,下载,并生成 attachment,进行通知展示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iflet imageURLString = bestAttemptContent.userInfo["image"] as? String,
   let URL = URL(string: imageURLString)
{
    downloadAndSave(url: URL) { localURL in
        iflet localURL = localURL {
            do{
                let attachment = tryUNNotificationAttachment(identifier:"image_downloaded", url: localURL, options: nil)
                bestAttemptContent.attachments = [attachment]
            }catch{
                print(error)
            }
        }
        contentHandler(bestAttemptContent)
    }
}

关于在通知中展示图片或者视频,有几点想补充说明:

UNNotificationContent 的 attachments 虽然是一个数组,但是系统只会展示第一个 attachment 对象的内容。不过你依然可以发送多个 attachments,然后在要展示的时候再重新安排它们的顺序,以显示最符合情景的图片或者视频。另外,你也可能会在自定义通知展示 UI 时用到多个 attachment。我们接下来一节中会看到一个相关的例子。在当前 beta (iOS 10 beta 4) 中,serviceExtensionTimeWillExpire 被调用之前,你有 30 秒时间来处理和更改通知内容。对于一般的图片来说,这个时间是足够的。但是如果你推送的是体积较大的视频内容,用户又恰巧处在糟糕的网络环境的话,很有可能无法及时下载完成。如果你想在远程推送来的通知中显示应用 bundle 内的资源的话,要注意 extension 的 bundle 和 app main bundle 并不是一回事儿。你可以选择将图片资源放到 extension bundle 中,也可以选择放在 main bundle 里。总之,你需要保证能够获取到正确的,并且你具有读取权限的 url。关于从 extension 中访问 main bundle,可以参看这篇回答。系统在创建 attachement 时会根据提供的 url 后缀确定文件类型,如果没有后缀,或者后缀无法不正确的话,你可以在创建时通过 UNNotificationAttachmentOptionsTypeHintKey 来指定资源类型。如果使用的图片和视频文件不在你的 bundle 内部,它们将被移动到系统的负责通知的文件夹下,然后在当通知被移除后删除。如果媒体文件在 bundle 内部,它们将被复制到通知文件夹下。每个应用能使用的媒体文件的文件大小总和是有限制,超过限制后创建 attachment 时将抛出异常。可能的所有错误可以在 UNError 中找到。

你可以访问一个已经创建的 attachment 的内容,但是要注意权限问题。可以使用 startAccessingSecurityScopedResource 来暂时获取以创建的 attachment 的访问权限。比如:

?
1
2
3
4
5
6
7
let content = notification.request.content
iflet attachment = content.attachments.first { 
    ifattachment.url.startAccessingSecurityScopedResource() { 
        eventImage.image = UIImage(contentsOfFile: attachment.url.path!) 
        attachment.url.stopAccessingSecurityScopedResource() 
    

关于 Service Extension 和多媒体通知的使用,可以参考 Demo 中NotificationService 和 MediaViewController 的内容。

自定义通知视图样式

iOS 10 SDK 新加的另一个 Content Extension 可以用来自定义通知的详细页面的视图。新建一个 Notification Content Extension,Xcode 为我们准备的模板中包含了一个实现了 UNNotificationContentExtension 的 UIViewController 子类。这个 extension 中有一个必须实现的方法 didReceive(_:),在系统需要显示自定义样式的通知详情视图时,这个方法将被调用,你需要在其中配置你的 UI。而 UI 本身可以通过这个 extension 中的 MainInterface.storyboard 来进行定义。自定义 UI 的通知是和通知 category 绑定的,我们需要在 extension 的 Info.plist 里指定这个通知样式所对应的 category 标识符:

系统在接收到通知后会先查找有没有能够处理这类通知的 content extension,如果存在,那么就交给 extension 来进行处理。另外,在构建 UI 时,我们可以通过 Info.plist 控制通知详细视图的尺寸,以及是否显示原始的通知。关于 Content Extension 中的 Info.plist 的 key,可以在这个文档中找到详细信息。

虽然我们可以使用包括按钮在内的各种 UI,但是系统不允许我们对这些 UI 进行交互。点击通知视图 UI 本身会将我们导航到应用中,不过我们可以通过 action 的方式来对自定义 UI 进行更新。UNNotificationContentExtension 为我们提供了一个可选方法 didReceive(_:completionHandler:),它会在用户选择了某个 action 时被调用,你有机会在这里更新通知的 UI。如果有 UI 更新,那么在方法的 completionHandler 中,开发者可以选择传递 .doNotDismiss 来保持通知继续被显示。如果没有继续显示的必要,可以选择 .dismissAndForwardAction 或者 .dismiss,前者将把通知的 action 继续传递给应用的 UNUserNotificationCenterDelegate 中的 userNotificationCenter(:didReceive:withCompletionHandler),而后者将直接解散这个通知。

如果你的自定义 UI 包含视频等,你还可以实现 UNNotificationContentExtension 里的 media 开头的一系列属性,它将为你提供一些视频播放的控件和相关方法。

关于 Content Extension 和自定义通知样式,可以参考 Demo 中NotificationViewController 和 CustomizeUIViewController 的内容。

总结

iOS 10 SDK 中对通知这块进行了 iOS 系统发布以来最大的一次重构,很多“老朋友”都被标记为了 deprecated:

iOS 10 中被标为弃用的 API

UILocalNotificationUIMutableUserNotificationActionUIMutableUserNotificationCategoryUIUserNotificationActionUIUserNotificationCategoryUIUserNotificationSettingshandleActionWithIdentifier:forLocalNotification:handleActionWithIdentifier:forRemoteNotification:didReceiveLocalNotification:withCompletion:didReceiveRemoteNotification:withCompletion:

等一系列在 UIKit 中的发送和处理通知的类型及方法。

现状以及尽快使用新的 API

相比于 iOS 早期时代的 API,新的 API 展现出了高度的模块化和统一特性,易用性也非常好,是一套更加先进的 API。如果有可能,特别是如果你的应用是重度依赖通知特性的话,直接从 iOS 10 开始可以让你充分使用在新通知体系的各种特性。

虽然原来的 API 都被标为弃用了,但是如果你需要支持 iOS 10 之前的系统的话,你还是需要使用原来的 API。我们可以使用

?
1
2
3
if#available(iOS10.0, *) {
    // Use UserNotification
}
的方式来指针对 iOS 10 进行新通知的适配,并让 iOS 10 的用户享受到新通知带来的便利特性,然后在将来版本升级到只支持 iOS 10 以上时再移除掉所有被弃用的代码。对于优化和梳理通知相关代码来说,新 API 对代码设计和组织上带来的好处足以弥补适配上的麻烦,而且它还能为你的应用提供更好的通知特性和体验,何乐不为呢?


距离 iPhone 横空出世已经过去了 9 个年头,iOS 的版本号也跨入了两位数。在我们回顾过去四五年 iOS 系统的发展的时候,不免感叹变化速度之快,迭代周期之短。iOS 7 翻天覆地的全新设计,iOS 8 中 Size Classes 的出现,应用扩展,以及 Cloud Kit 的加入,iOS 9 里的分屏多任务特性等等。Apple 近年都是在 WWDC 发布新的系统和软件,然后在秋季和冬季 (或者来年春季) 召开硬件产品的发布会。WWDC 上每一项软件的更新其实都预示了相应的硬件的方向,相信今年也不会例外。

对于开发者来说,好消息是 iOS 10 中并没有加入太多内容。按照适配的需求,来年的 iOS 开发至少应该可以从 iOS 8 甚至 iOS 9 开始,我们将有时间对之前的版本特性进行更好的梳理,消化和实践。相比于开疆扩土,iOS 10 更专注的是对现有内容的改进,以弥补之前迅速发展所留下的一些问题,这其实正是 Apple 当下所亟需做的事情。

生态整合与 Extension 开发

在 iOS 10 里 Apple 延续了前几年的策略,那就是进行平台整合。全世界现在没有另外一家厂商在掌握了包括桌面,移动到穿戴的一系列硬件设备的同时,还掌控了相应的从操作系统,到应用软件,再到软件商店这样一套完整的布局。Apple 显然也非常明白这个优势意味着什么。所以近年来 Apple 一直强调平台整合,如果你的应用能够同时在 iOS,watchOS 以及 macOS 上工作的话,毫无疑问将会更容易吸引用户以及 Apple 的喜爱。

另外一点则是各个应用之间的整合和交互。不难发现,随着近年来 extension 开发的兴起,Apple 逐渐在从 app 是“用户体验的核心”这个理念中转移,变为用户应该也可以在通知中心,桌面挂件或者手表这样的地方完成必要交互。而应用之间的交互在以前可以说是 iOS 系统的禁区,但是去年随着 Workflow 的成功,Apple 对于应用之间的交互有助于用户生产力的提升有了清晰的认识。今年 SDK 中几个重大更新其实都是围绕这个主题来进行的。

iOS 10 中,Apple 为我们添加了茫茫多 extension 的新模板,以至于在同事之间开玩笑都是我们马上就要丢掉 iOS app 开发者的工作,而转变为 iOS extension 开发者这样了。新加入的扩展的种类和数量都足以说明使用应用扩展以及进行扩展开发在今后 iOS 开发中的重要地位。如果你对扩展开发还一无所知,可以先看看这篇入门文章,里面简单介绍了关于扩展的基本概念,不同开发 target 之间代码共享的方式,以及通用的扩展开发方法等。

苹果恢复大师(iFonebox中文版)
【点击进入】
恢复微信聊天记录,文字,图片,语音。 仅128元,简单3步操作,功能强大!

SiriKit

Siri API 的开放自然是 iOS 10 SDK 中最激动人心也是亮眼的特性。SiriKit 为我们提供一全套从语音识别到代码处理,最后向用户展示结果的流程。Apple 加入了一套全新的框架 Intents.framework 来表示 Siri 获取并解析的结果。你的应用需要提供一些关键字表明可以接受相关输入,而 Siri 扩展只需要监听系统识别的用户意图 (intent),作出合适的响应,修改以及实际操作,最后通过 IntentsUI.framework 提供反馈。整个过程非常清晰明了,但是这也意味着开发者所能拥有的自由度有限。

在 iOS 10 中,我们只能用 SiriKit 来做六类事情,分别是:

语音和视频通话发送消息发送或接收付款搜索照片约车管理健身

如果你的应用恰好正在处理这些领域的问题的话,添加 Intents Extension 的支持会是很棒的选择。它将提高用户使用你的应用的可能性,也能让用户在其他像是地图这样的系统级应用中使用你的服务。

SiriKit 笔记 (待填坑)

User Notifications

通知中心向来是 iOS 上的兵家必争之地。如何提供适时有效的通知,往往决定了用户活跃和留存的可能性。在 iOS 10 上,Apple 对通知进行了加强和革新。现在,为了更好地处理和管理通知,和本地及推送通知相关的 API 被封装到了全新的框架 UserNotifications.framework 中。在 iOS 10 中,开发者的服务器有机会在本地或者远程通知发送给用户之前再进行修改。

另外,在之前加入了 notification action 以及 text input 的基础上,iOS 10 又新增了为通知添加音频,图片,甚至视频的功能。现在,你的通知不仅仅是提醒用户回到应用的入口,更成为了一个展示应用内容,向用户传递多媒体信息的窗口。

User Notifications 笔记 - iOS 10 UserNotifications 框架解析

iMessage Apps

Message 应用大概是 Apple 在宣传 iOS 10 时着力最多的部分了。虽然新的贴纸包,自动转换颜文字,发送全屏效果等功能都很酷炫,但是对于程序开发者来说,可能还是对 iMessage Apps 更感兴趣。Xcode 8 中,Apple 在 iOS Application 模板中添加了一类新的项目类型,Messages Application。同时,模拟器甚至还开发了新的双人对话模式,以供开发者调试这类 app。

虽然名义上是独立 app,但实际上工作的依然是一个 extension。在该扩展中,Messages.framework 将承担与系统的 message 界面交互的主要职责。你通过提供一个自定义的 View Controller,来获取用户在使用你的 message app 时进行对话的上下文,以及发送接收等操作,并做出合适的响应。这个扩展在用来进行直接在 Message 应用中一些自定义共享会很好玩。但是鉴于 Apple 暂时没有打算将 Message.app 跨平台的原因,可能也注定了这只会是一种补充,而无法成为主流。

iMessage Apps 笔记 (待填坑)

IDE 和工具改进

除了整合平台战略思想下的一些 SDK 改变,今年和 iOS 开发者相关的更多的是开发工具的进步和革新了。

Xcode 8

Xcode 8 展现出了很多有意思的新特性,比如更强大的 View Debugging,可以帮助我们追查内存引用问题的 Memory Debugging 等。这些工具十分强大,也将帮助我们在开发过程中及早发现问题,而不要将它们带入在最终产品中去。

在 app 签名方面,Apple 终于意识到了他们在 Xcode 7 中所犯得错误。我想可能不止一个人被证书和描述文件出问题时的 "Fix Issue" 按钮坑过。这个按钮不仅不会修正问题,反而会直接注销现有的开发者证书,然后“自作主张”地重新申请。大多数情况下,这让事情变得更加糟糕。特别是对于新加入的开发者,他们并不理解 Apple 的证书系统,错误的操作和处置,往往让开发环境变得不可挽回。Xcode 8 中,同一个开发者帐号现在允许多个开发证书,而完全重做的 app 签名系统也足够好用,并且避免了误操作的可能性。在兼顾自动配置的基础上,也为大型项目和复杂的 CI 环境提供了足够灵活的配置空间,这绝对值得点赞。

另外 Xcode 终于提供了进行代码编辑器扩展的能力。现在开发者可以创建XCSourceEditorExtension 来对 Xcode 的功能进行扩展了,在没有文档帮助和官方支持的情况下摸索着为 Xcode 制作插件的历史也即将结束。

Xcode 8 笔记 (待填坑)

Swift 3

Swift 开源已经过去半年时间。在 Swift 2.2 中我们已经看到了开源的社区力量对语言产生的深刻影响,而在 Swift 3 中这一影响的效果将更加明显。

最大的变化在于 Foundation 框架的重新导入,可能过一段时间再回头看的话,这将标志着 Swift 与 Objective-C 彻底分家。Foundation 框架中的 API 现在以更符合 Swift 的方式被导入到语言中。大体来说,这些变化包括去除 NS 前缀,将绝大部分 class 转换为 struct (虽然底层还是 copy-on-write 的引用实现,可以参看 ReferenceConvertible 协议的内容),去掉 API 中重复的语义等。如果在当前你还能看出 Swift 和 Objective-C 在使用 Foundation 或者说开发 app 时同根同源的话,Swift 3 正式发布后可能情况会大不相同。

由于引用类型向值类型的转换,也将导致我们在使用 Swift 开发时的思考方式发生变化。以往的 Foundation 框架中类型的可变性是由不可变类型和它的可变类型版本 (比如 NSData 和NSMutableData) 来进行区分的。而在 Swift 3 中,一般来说将只有作为结构体的不可变类型 (比如 Data),对于这类结构体的改变,将会是更安全的基于写时复制的行为,而不再是原来可变对象那样的危险的内存操作。这在很多时候除了保证数据共享时的安全性以外,内部的引用特性也保证了调用速度。实际上,因为减少了不必要的复制 (比如根据一个不可变对象创建相应的可变对象),实际上通过 Swift 3 的 API 使用 Foundation 的速度将比原来更快!

关于 Swift 3 的更多内容,我会在我的《Swifter - 100 个 Swift 必备 tips》一书中通过补充章节的方式进行说明。同时,该书现有的 Swift 2 相关的描述和示例也会按照 Swift 3 的语法规范和特性进行更新,以适应最新版本。您可以访问 swifter.tips 获取这本书的更多相关内容。

Apple 生态和其他

另外影响比较重大消息是,在 iOS 9 引入的 ATS 将在来年更加严格。2017 年起,新提交的 app 将不再被允许进行 http 的访问,所有的 app 内的网络请求必须加密,通过 https 完成。所以如果你家 app 的服务器或者某些访问页面还是 http 的话,需要尽早过度到 https。

另外,watchOS 3 和 tvOS 也有一些新的内容。其中最重要的当属 watchOS 中可以使用 SceneKit 和 SpriteKit。虽然这两个框架本意是做游戏,但是 watch 的小屏幕和低性能可能并不足以支撑我们在这样一个受限平台很好的作品。但是这两个框架可以为交互乏味的 watchOS 提供很好的动画效果的补充,可能会是它们在 watchOS 上更合适的用途。

最后,OS X 改名为 macOS,有些媒体和开发者将其解读为去乔布斯化,其实我更倾向于这是一种强迫症和完美主义的基本需求。不管名字如何改变,Apple 在 iOS,macOS,watchOS 和 tvOS 这四个产品线上的布局已经完成,整个生态现在看来也还十分健康。Apple 在用户权益和隐私上的重视,以及像是在 https 上的推动,无疑都是这个时代前进的动力。


随着iOS10已经发布,大家的App都需要适配iOS10,下面是我总结的一些关于iOS10适配方面的问题,如果有错误,欢迎指出.

1.系统判断方法失效:

在你的项目中,当需要判断系统版本的话,不要使用下面的方法:

[java] view plain copy
 print?
  1. #define isiOS10 ([[[[UIDevice currentDevice] systemVersion] substringToIndex:1] intValue]>=10)  

它会永远返回NO,substringToIndex:1在iOS 10 会被检测成 iOS 1了,
应该使用下面的这些方法:
Objective-C 中这样写:

[java] view plain copy
 print?
  1. #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)  
  2. #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)  
  3. #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)  
  4. #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)  
  5. #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)  

或者使用:

[java] view plain copy
 print?
  1. if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 9, .minorVersion = 1, .patchVersion = 0}]) { NSLog(@"Hello from > iOS 9.1");}  
  2. if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,3,0}]) { NSLog(@"Hello from > iOS 9.3");}  

或者使用:

[java] view plain copy
 print?
  1. if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_9_0) { // do stuff for iOS 9 and newer} else { // do stuff for older versions than iOS 9}  

有时候会缺少一些常量,NSFoundationVersionNumber是在NSObjCRuntime.h中定义的,作为Xcode7.3.1的一部分,我们设定常熟范围从iPhone OS 2到#define NSFoundationVersionNumber_iOS_8_4 1144.17,在iOS 10(Xcode 8)中,苹果补充了缺少的数字,设置有未来的版本.

[java] view plain copy
 print?
  1. #define NSFoundationVersionNumber_iOS_9_0 1240.1  
  2. #define NSFoundationVersionNumber_iOS_9_1 1241.14  
  3. #define NSFoundationVersionNumber_iOS_9_2 1242.12  
  4. #define NSFoundationVersionNumber_iOS_9_3 1242.12  
  5. #define NSFoundationVersionNumber_iOS_9_4 1280.25  
  6. #define NSFoundationVersionNumber_iOS_9_x_Max 1299  

Swift中这样写:

[java] view plain copy
 print?
  1. if NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) {   
  2.          // 代码块  
  3. }  

或者使用

[java] view plain copy
 print?
  1. if #available(iOS 10.0, *) {   
  2.          // 代码块  
  3. else {   
  4.          // 代码块  
  5. }  

2.隐私数据访问问题:

你的项目中访问了隐私数据,比如:相机,相册,联系人等,在Xcode8中打开编译的话,统统会crash,控制台会输出下面这样的日志:

这是因为iOS对用户的安全和隐私的增强,在申请很多私有权限的时候都需要添加描述,但是,在使用Xcode 8之前的Xcode还是使用系统的权限通知框.
要想解决这个问题,只需要在info.plist添加NSContactsUsageDescription的key, value自己随意填写就可以,这里列举出对应的key(Source Code模式下):

[java] view plain copy
 print?
  1.    
  2. NSPhotoLibraryUsageDescription   
  3. App需要您的同意,才能访问相册   
  4.    
  5. NSCameraUsageDescription   
  6. App需要您的同意,才能访问相机   
  7.    
  8. NSMicrophoneUsageDescription   
  9. App需要您的同意,才能访问麦克风   
  10.    
  11. NSLocationUsageDescription   
  12. App需要您的同意,才能访问位置   
  13.    
  14. NSLocationWhenInUseUsageDescription   
  15. App需要您的同意,才能在使用期间访问位置   
  16.    
  17. NSLocationAlwaysUsageDescription   
  18. App需要您的同意,才能始终访问位置   
  19.    
  20. NSCalendarsUsageDescription   
  21. App需要您的同意,才能访问日历   
  22.    
  23. NSRemindersUsageDescription   
  24. App需要您的同意,才能访问提醒事项   
  25.    
  26. NSMotionUsageDescription App需要您的同意,才能访问运动与健身   
  27.    
  28. NSHealthUpdateUsageDescription   
  29. App需要您的同意,才能访问健康更新    
  30.    
  31. NSHealthShareUsageDescription   
  32. App需要您的同意,才能访问健康分享   
  33.    
  34. NSBluetoothPeripheralUsageDescription   
  35. App需要您的同意,才能访问蓝牙   
  36.    
  37. NSAppleMusicUsageDescription   
  38. App需要您的同意,才能访问媒体资料库  

如果不起作用,可以请求后台权限,类似于这样:

[java] view plain copy
 print?
  1. UIBackgroundModes  
  2.    
  3.    
  4. location  
  5. ...  

或者在Xcode里选中当前的target,选择Capabilities,找到Background Modes,打开它,在里面选择对应权限

3.UIColor的问题

官方文档中说:大多数core开头的图形框架和AVFoundation都提高了对扩展像素和宽色域色彩空间的支持.通过图形堆栈扩展这种方式比以往支持广色域的显示设备更加容易。现在对UIKit扩展可以在sRGB的色彩空间下工作,性能更好,也可以在更广泛的色域来搭配sRGB颜色.如果你的项目中是通过低级别的api自己实现图形处理的,建议使用sRGB,也就是说在项目中使用了RGB转化颜色的建议转换为使用sRGB,在UIColor类中新增了两个api:

[java] view plain copy
 print?
  1. - (UIColor *)initWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0);  
  2. + (UIColor *)colorWithDisplayP3Red:(CGFloat)displayP3Red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha NS_AVAILABLE_IOS(10_0);  

4.真彩色的显示

真彩色的显示会根据光感应器来自动的调节达到特定环境下显示与性能的平衡效果,如果需要这个功能的话,可以在info.plist里配置(在Source Code模式下):

[java] view plain copy
 print?
  1. UIWhitePointAdaptivityStyle  

它有五种取值,分别是:

[java] view plain copy
 print?
  1. UIWhitePointAdaptivityStyleStandard // 标准模式  
  2. UIWhitePointAdaptivityStyleReading // 阅读模式  
  3. UIWhitePointAdaptivityStylePhoto // 图片模式  
  4. UIWhitePointAdaptivityStyleVideo // 视频模式  
  5. UIWhitePointAdaptivityStyleStandard // 游戏模式  

也就是说如果你的项目是阅读类的,就选择UIWhitePointAdaptivityStyleReading这个模式,五种模式的显示效果是从上往下递减,也就是说如果你的项目是图片处理类的,你选择的是阅读模式,给选择太好的效果会影响性能.

5.ATS的问题

1.在iOS 9的时候,默认非HTTS的网络是被禁止的,我们可以在info.plist文件中添加NSAppTransportSecurity字典,将NSAllowsArbitraryLoads设置为YES来禁用ATS;
2.从2017年1月1日起,,所有新提交的 app 默认不允许使用NSAllowsArbitraryLoads来绕过ATS的限制,默认情况下你的 app 可以访问加密足够强的(TLS V1.2以上)HTTPS内容;
3.可以选择使用NSExceptionDomains设置白名单的方式对特定的域名开放HTTP内容来通过审核,比如说你的应用集成了第三方的登录分享SDK,可以通过这种方式来做,下面以新浪SDK作为示范(Source Code 模式下):

[java] view plain copy
 print?
  1. NSAppTransportSecurity  
  2.   
  3.  NSExceptionDomains  
  4.    
  5.   sina.cn  
  6.     
  7.    NSThirdPartyExceptionMinimumTLSVersion  
  8.    TLSv1.0  
  9.    NSThirdPartyExceptionRequiresForwardSecrecy  
  10.      
  11.    NSIncludesSubdomains  
  12.      
  13.     
  14.   weibo.cn  
  15.     
  16.    NSThirdPartyExceptionMinimumTLSVersion  
  17.    TLSv1.0  
  18.    NSThirdPartyExceptionRequiresForwardSecrecy  
  19.      
  20.    NSIncludesSubdomains  
  21.      
  22.     
  23.   weibo. com  
  24.     
  25.    NSThirdPartyExceptionMinimumTLSVersion  
  26.    TLSv1.0  
  27.    NSThirdPartyExceptionRequiresForwardSecrecy  
  28.      
  29.    NSIncludesSubdomains  
  30.      
  31.     
  32.   sinaimg.cn  
  33.     
  34.    NSThirdPartyExceptionMinimumTLSVersion  
  35.    TLSv1.0  
  36.    NSThirdPartyExceptionRequiresForwardSecrecy  
  37.      
  38.    NSIncludesSubdomains  
  39.      
  40.     
  41.   sinajs.cn  
  42.     
  43.    NSThirdPartyExceptionMinimumTLSVersion  
  44.    TLSv1.0  
  45.    NSThirdPartyExceptionRequiresForwardSecrecy  
  46.      
  47.    NSIncludesSubdomains  
  48.      
  49.     
  50.   sina.com.cn  
  51.     
  52.    NSThirdPartyExceptionMinimumTLSVersion  
  53.    TLSv1.0  
  54.    NSThirdPartyExceptionRequiresForwardSecrecy  
  55.      
  56.    NSIncludesSubdomains  
  57.      
  58.     
  59.    

4.在iOS 10 中info.plist文件新加入了NSAllowsArbitraryLoadsInWebContent键,允许任意web页面加载,同时苹果会用 ATS 来保护你的app;
5.安全传输不再支持SSLv3, 建议尽快停用SHA1和3DES算法;

6.UIStatusBar的问题:

在iOS10中,如果还使用以前设置UIStatusBar类型或者控制隐藏还是显示的方法,会报警告,方法过期,如下图:


UIStatusBar的警告.png


上面方法到 iOS 10 不能使用了,要想修改UIStatusBar的样式或者状态使用下图中所示的属性或方法:

[java] view plain copy
 print?
  1. @property(nonatomic, readonly) UIStatusBarStyle preferredStatusBarStyle NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to UIStatusBarStyleDefault  
  2. @property(nonatomic, readonly) BOOL prefersStatusBarHidden NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to NO  
  3. - (UIStatusBarStyle)preferredStatusBarStyle NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to UIStatusBarStyleDefault  
  4. - (BOOL)prefersStatusBarHidden NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to NO  
  5. // Override to return the type of animation that should be used for status bar changes for this view controller. This currently only affects changes to prefersStatusBarHidden.  
  6. - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to UIStatusBarAnimationFade  

7.UITextField

在iOS 10 中,UITextField新增了textContentType字段,是UITextContentType类型,它是一个枚举,作用是可以指定输入框的类型,以便系统可以分析出用户的语义.是电话类型就建议一些电话,是地址类型就建议一些地址.可以在#import 文件中,查看textContentType字段,有以下可以选择的类型:

[java] view plain copy
 print?
  1. UIKIT_EXTERN UITextContentType const UITextContentTypeName                      NS_AVAILABLE_IOS(10_0);  
  2. UIKIT_EXTERN UITextContentType const UITextContentTypeNamePrefix                NS_AVAILABLE_IOS(10_0);  
  3. UIKIT_EXTERN UITextContentType const UITextContentTypeGivenName                 NS_AVAILABLE_IOS(10_0);  
  4. UIKIT_EXTERN UITextContentType const UITextContentTypeMiddleName                NS_AVAILABLE_IOS(10_0);  
  5. UIKIT_EXTERN UITextContentType const UITextContentTypeFamilyName                NS_AVAILABLE_IOS(10_0);  
  6. UIKIT_EXTERN UITextContentType const UITextContentTypeNameSuffix                NS_AVAILABLE_IOS(10_0);  
  7. UIKIT_EXTERN UITextContentType const UITextContentTypeNickname                  NS_AVAILABLE_IOS(10_0);  
  8. UIKIT_EXTERN UITextContentType const UITextContentTypeJobTitle                  NS_AVAILABLE_IOS(10_0);  
  9. UIKIT_EXTERN UITextContentType const UITextContentTypeOrganizationName          NS_AVAILABLE_IOS(10_0);  
  10. UIKIT_EXTERN UITextContentType const UITextContentTypeLocation                  NS_AVAILABLE_IOS(10_0);  
  11. UIKIT_EXTERN UITextContentType const UITextContentTypeFullStreetAddress         NS_AVAILABLE_IOS(10_0);  
  12. UIKIT_EXTERN UITextContentType const UITextContentTypeStreetAddressLine1        NS_AVAILABLE_IOS(10_0);  
  13. UIKIT_EXTERN UITextContentType const UITextContentTypeStreetAddressLine2        NS_AVAILABLE_IOS(10_0);  
  14. UIKIT_EXTERN UITextContentType const UITextContentTypeAddressCity               NS_AVAILABLE_IOS(10_0);  
  15. UIKIT_EXTERN UITextContentType const UITextContentTypeAddressState              NS_AVAILABLE_IOS(10_0);  
  16. UIKIT_EXTERN UITextContentType const UITextContentTypeAddressCityAndState       NS_AVAILABLE_IOS(10_0);  
  17. UIKIT_EXTERN UITextContentType const UITextContentTypeSublocality               NS_AVAILABLE_IOS(10_0);  
  18. UIKIT_EXTERN UITextContentType const UITextContentTypeCountryName               NS_AVAILABLE_IOS(10_0);  
  19. UIKIT_EXTERN UITextContentType const UITextContentTypePostalCode                NS_AVAILABLE_IOS(10_0);  
  20. UIKIT_EXTERN UITextContentType const UITextContentTypeTelephoneNumber           NS_AVAILABLE_IOS(10_0);  
  21. UIKIT_EXTERN UITextContentType const UITextContentTypeEmailAddress              NS_AVAILABLE_IOS(10_0);  
  22. UIKIT_EXTERN UITextContentType const UITextContentTypeURL                       NS_AVAILABLE_IOS(10_0);  
  23. UIKIT_EXTERN UITextContentType const UITextContentTypeCreditCardNumber          NS_AVAILABLE_IOS(10_0);  

8.UserNotifications(用户通知)

iOS 10 中将通知相关的 API 都统一了,在此基础上很多用户定义的通知,并且可以捕捉到各个通知状态的回调.以前通知的概念是:大家想接受的提前做好准备,然后一下全两分发,没收到也不管了,也不关心发送者,现在的用户通知做成了类似于网络请求,先发一个request得到response的流程,还封装了error,可以在各个状态的方法中做一些额外的操作,并且能获得一些字段,比如发送者之类的.这个功能的头文件是:#import 
主要有以下文件:

[java] view plain copy
 print?
  1. #import   
  2. #import   
  3. #import   
  4. #import   
  5. #import   
  6. #import   
  7. #import   
  8. #import   
  9. #import   
  10. #import   
  11. #import   
  12. #import   
  13. #import   
  14. #import   

9.UICollectionViewCell的的优化

在iOS 10 之前,UICollectionView上面如果有大量cell,当用户活动很快的时候,整个UICollectionView的卡顿会很明显,为什么会造成这样的问题,这里涉及到了iOS 系统的重用机制,当cell准备加载进屏幕的时候,整个cell都已经加载完成,等待在屏幕外面了,也就是整整一行cell都已经加载完毕,这就是造成卡顿的主要原因,专业术语叫做:掉帧.
要想让用户感觉不到卡顿,我们的app必须帧率达到60帧/秒,也就是说每帧16毫秒要刷新一次.

iOS 10 之前UICollectionViewCell的生命周期是这样的:

1.用户滑动屏幕,屏幕外有一个cell准备加载进来,把cell从reusr队列拿出来,然后调用prepareForReuse方法,在这个方法里面,可以重置cell的状态,加载新的数据;2.继续滑动,就会调用cellForItemAtIndexPath方法,在这个方法里面给cell赋值模型,然后返回给系统;3.当cell马上进去屏幕的时候,就会调用willDisplayCell方法,在这个方法里面我们还可以修改cell,为进入屏幕做最后的准备工作;4.执行完willDisplayCell方法后,cell就进去屏幕了.当cell完全离开屏幕以后,会调用didEndDisplayingCell方法.

iOS 10 UICollectionViewCell的生命周期是这样的:

1.用户滑动屏幕,屏幕外有一个cell准备加载进来,把cell从reusr队列拿出来,然后调用prepareForReuse方法,在这里当cell还没有进去屏幕的时候,就已经提前调用这个方法了,对比之前的区别是之前是cell的上边缘马上进去屏幕的时候就会调用该方法,而iOS 10 提前到cell还在屏幕外面的时候就调用;2.在cellForItemAtIndexPath中创建cell,填充数据,刷新状态等操作,相比于之前也提前了;3.用户继续滑动的话,当cell马上就需要显示的时候我们再调用willDisplayCell方法,原则就是:何时需要显示,何时再去调用willDisplayCell方法;4.当cell完全离开屏幕以后,会调用didEndDisplayingCell方法,跟之前一样,cell会进入重用队列.
在iOS 10 之前,cell只能从重用队列里面取出,再走一遍生命周期,并调用cellForItemAtIndexPath创建或者生成一个cell.
在iOS 10 中,系统会cell保存一段时间,也就是说当用户把cell滑出屏幕以后,如果又滑动回来,cell不用再走一遍生命周期了,只需要调用willDisplayCell方法就可以重新出现在屏幕中了.
iOS 10 中,系统是一个一个加载cell的,二以前是一行一行加载的,这样就可以提升很多性能;

iOS 10 新增加的Pre-Fetching预加载

这个是为了降低UICollectionViewCell在加载的时候所花费的时间,在 iOS 10 中,除了数据源协议和代理协议外,新增加了一个UICollectionViewDataSourcePrefetching协议,这个协议里面定义了两个方法:

[java] view plain copy
 print?
  1. - (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray *)indexPaths NS_AVAILABLE_IOS(10_0);  
  2.   
  3. - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray *)indexPaths  NS_AVAILABLE_IOS(10_0);  

在ColletionView prefetchItemsAt indexPaths这个方法是异步预加载数据的,当中的indexPaths数组是有序的,就是item接收数据的顺序;
CollectionView cancelPrefetcingForItemsAt indexPaths这个方法是可选的,可以用来处理在滑动中取消或者降低提前加载数据的优先级.
注意:这个协议并不能代替之前读取数据的方法,仅仅是辅助加载数据.
Pre-Fetching预加载对UITableViewCell同样适用.

10. UIRefreshControl的使用

在iOS 10 中, UIRefreshControl可以直接在UICollectionView和UITableView中使用,并且脱离了UITableViewController.现在RefreshControl是UIScrollView的一个属性.
使用方法:

[java] view plain copy
 print?
  1. UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];  
  2.     [refreshControl addTarget:self action:@selector(loadData) forControlEvents:UIControlEventValueChanged];  
  3.     collectionView.refreshControl = refreshControl;  
0
0 0
原创粉丝点击