原文在这里,水平有限大家,欢迎各位批评指正。
在你的App中有没有关于推送的处理?如果你的App不是简单的Hello World的话,我相信你的答案是有推送的处理。 随着3dtouch在越来越多的iOS设备上的应用,3dtouch功能已不单单是有就好的一个特性,那你的App有没有对3dtouch的唤起有相应的处理呢? 还有就是你的app是不是也支持Universial Links?这是一个在大多数现在的app越来越受欢迎的特性,如果你的app不支持,那么你现在可以开始做一些支持该特性的工作了。
关于上面提到的三种功能,你可以在网上找到很多各个功能的教程,但每一项功能都是单独实现的,如果你想集成三种功能,每一种都分别实现,真的是我们想要的么?
在开始之前,我们需要明确一下几点:
- 1. Universal Links是一种拦截在safari中打开的url来打开app指定页面的方法。 Universal Links需要一些后端工作,因此本文只针对深度链接(Deep Links)。深度链接与Unversal Links的工作原理相似,他们的实现原理并没有多大不同,因此如果你需要集成Universal Links的话应该不会太难
- 2. Shortcut 在支持3Dtouch的设备上 Shortcut 是一种通过大力度的按压appIcon 根据选择ShortcutItem来调起app并跳转到相应页面的方法。
- 3. Notifications 当你点击app的通知(不管是远程还是本地通知)时 app都会启动并跳转到相应界面或者执行相应的操作。
由以上三点可知,这三种不同的特性都对应着同一场景--从指定的APP页面启动。
苹果将这些
Launching Options在AppDelegate的didFinishLaunchingWithOptions方法中处理。
然而,在实现这些启动操作的时候,往往会让人困惑,并导致出现大量冗余代码。当app在前台运行而不是启动时这项操作实现起来往往更复杂,因为Shortcut操作,DeepLinks操作、Notification操作的实现在不同的代理方法中,他们看起来没有任何相同之处。
我再次强调下我们需要明确的是:所有的这些特性都是服务于同一个目的--打开App指定的页面
现在的问题是如何以更好的方式让这些功能集成的一块。
建立工程
在开始实现这些启动操作之前,我们需要先列一个需要进入app内页面的表。假设我们这正在做一个公寓预订的App,我们想要很快到达app内的一下页面:
- 我的消息(预览)页面:通过3Dtouch进入
- 消息详情(指定越某个聊天页面):通过推送通知进入
- 新建列表页:在HostProfile模式下通过3Dtouch进入
- 我的活动页:通过3Dtouch进入
- 预订请求页:通过email(深度链接)链接和推送进入
首先,我们建立一个包含ViewController,导航标题和切换Profile按钮的简单工程 ViewController有一个current profile变量和切换profile的机制
我没有用任何软件设计模式,因为这不是本教程的重点,在现实的app中,你需要一个良好的架构,而不是仅仅将profile正确的保存在ViewController中。
enum ProfileType: String {
case guest = "Guest" // default
case host = "Host"
}
class ViewController: UIViewController {
var currentProfile = ProfileType.guest
override func viewDidLoad() {
super.viewDidLoad()
configureFor(profileType: currentProfile)
}
@IBAction func didPressSwitchProfile(_ sender: Any) {
currentProfile = currentProfile == .guest ? .host : .guest
configureFor(profileType: currentProfile)
}
func configureFor(profileType: ProfileType) {
title = profileType.rawValue
}
}复制代码
一个工具管理所有
本文强调的一个目标 --- 不管以什么机制打开app指定页面,我们都视为深度链接。
现在我们已经建好了基本的结构和UI , 下面就组织一下深度链接的元素列表:
enum DeeplinkType {
enum Messages {
case root
case details(id: String)
}
case messages(Messages)
case activity
case newListing
case request(id: String)
}复制代码
本教程中,我们不会包含swift的枚举理论,如果你不清楚swift嵌套枚举和枚举关联值,你可以去这里学习
下一步,我们需要建立一个单例来管理所有深度链接的操作:
let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
}复制代码
添加一个属性,用于存储当前的 DeeplinkType
let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
private var deeplinkType: DeeplinkType?
}复制代码
app根据DeeplinkType
来决定打开那个页面:
let Deeplinker = DeepLinkManager()
class DeepLinkManager {
fileprivate init() {}
private var deeplinkType: DeeplinkType?
// check existing deepling and perform action
func checkDeepLink() {
}
}复制代码
不管时启动还是切换到前台 app都会调用didBecomeActive
代理方法,我们只需要在该方法中检测是否有需要处理得深度链接即可。
func applicationDidBecomeActive(_ application: UIApplication) {
// handle any deeplink
Deeplinker.checkDeepLink()
}复制代码
每次app激活都会检测是否有链接要处理,我们需要建立一个导航类,来根据DeeplinkType
打开指定的页面:
class DeeplinkNavigator {
static let shared = DeeplinkNavigator()
private init() { }
func proceedToDeeplink(_ type: DeeplinkType) {
}
}复制代码
在本教程中,我们仅仅是根据DeeplinkType
不同来展示不同的弹框处理链接的操作:
private var alertController = UIAlertController()
private func displayAlert(title: String) {
alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler: nil)
alertController.addAction(okButton)
if let vc = UIApplication.shared.keyWindow?.rootViewController {
if vc.presentedViewController != nil {
alertController.dismiss(animated: false, completion: {
vc.present(self.alertController, animated: true, completion: nil)
})
} else {
vc.present(alertController, animated: true, completion: nil)
}
}
}复制代码
在proceedToDeeplink
方法中,通过DeeplinkTypes
的切换来决定弹出的alert:
func proceedToDeeplink(_ type: DeeplinkType) {
switch type {
case .activity:
displayAlert(title: "Activity")
case .messages(.root):
displayAlert(title: "Messages Root")
case .messages(.details(id: let id)):
displayAlert(title: "Messages Details \(id)")
case .newListing:
displayAlert(title: "New Listing")
case .request(id: let id):
displayAlert(title: "Request Details \(id)")
}
}复制代码
回到DeeplinkManager中并用DeeplinkNavigator
单列来处理深度链接:
// check existing deepling and perform action
func checkDeepLink() {
guard let deeplinkType = deeplinkType else {
return
}
DeeplinkNavigator().proceedToDeeplink(deeplinkType)
// reset deeplink after handling
self.deeplinkType = nil // (1)
}复制代码
用过
deepLink
后,不要忘记将deepLink
置为nil
(1),否则下次打开app的时候会对同一个deepLink
在处理一次
现在我们要做的就是检测有没有deep link
(Shortcut,Deep Links,或者Notifications)需要处理,将他们转化成DeeplinkType
并交个DeepLinkManager
处理
接下来,我们要建立一些基本的Shortcuts,Deep Links,和Notifications来测试 记住,我们需要DeepLinkManager
去处理这些类型的深度链接,要确保功能的独立性,不要把不同类型的深度链接解析放在一起处理
Shortcuts
创建静态Shortcuts通常是通过info.plist
,如果你需要的话这里有一个简明教程。
在我们的Demo中有些Shortcuts是动态创建的,所以我们全部用代码实现,这样可以更灵活并且易于理解。
第一步,我们需要创建ShortcutParser
类,改类的实例仅仅负责Shortcuts。
class ShortcutParser {
static let shared = ShortcutParser()
private init() { }
}复制代码
在ShortcutParser
中我们创建一个用于注册不同类型shortcuts的registerShortcutsFor
方法:
func registerShortcuts(for profileType: ProfileType) {
let activityIcon = UIApplicationShortcutIcon(templateImageName: "Alert Icon")
let activityShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.activity.rawValue, localizedTitle: "Recent Activity", localizedSubtitle: nil, icon: activityIcon, userInfo: nil)
let messageIcon = UIApplicationShortcutIcon(templateImageName: "Messenger Icon")
let messageShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.messages.rawValue, localizedTitle: "Messages", localizedSubtitle: nil, icon: messageIcon, userInfo: nil)
UIApplication.shared.shortcutItems = [activityShortcutItem, messageShortcutItem]
switch profileType {
case .host:
let newListingIcon = UIApplicationShortcutIcon(templateImageName: "New Listing Icon")
let newListingShortcutItem = UIApplicationShortcutItem(type: ShortcutKey.newListing.rawValue, localizedTitle: "New Listing", localizedSubtitle: nil, icon: newListingIcon, userInfo: nil)
UIApplication.shared.shortcutItems?.append(newListingShortcutItem)
case .guest:
break
}
}复制代码
无论那种profile模式下都需要activityShortcutItem
和messageShortcutItem
,如果当前用户处于user 模式 再添加 newListingShortcutItem
项,对于每一种shorcut都有一个UIApplicationShortcutIcon
(位于shortcut的title一侧)。
当用户在ViewController中切换模式时,我们调用这个方法来根据新的profile配置shortcuts:
func configureFor(profileType: ProfileType) {
title = profileType.rawValue
ShortcutParser.registerShortcuts(for: profileType)
}复制代码
对于静态
shortcuts
最好再appDelegate
里设置shortcuts
,这样就可以在不加载ViewController
的时候就设置号shortcuts
编译并运行app测试下效果:
- 1.大力按压appIcon 查看3Dtouch选项
- 2.切换模式
- 3.
Force-touch
appicon 查看切换后的3Dtouch操作
然而,当我们点击3D-touch选项时,仅仅调起app的默认页,这是因为我们仅仅只是添加了shortcuts
并没有告诉App如何处理这些操作。
切换到AppDelegate
文件,添加处理3D-touch的代理方法
// MARK: Shortcuts
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
}复制代码
这个方法在shortcut
激活app是会检测3D-touch选项!completionHandler
会告诉代理是否处理改touch项,对于3dtouch的处理我们不在代理方法中进行,只需要在DeeplinkManager
中添加以下方法来处理:
@discardableResult
func handleShortcut(item: UIApplicationShortcutItem) -> Bool {
deeplinkType = ... // we will parse the item here
return deeplinkType != nil
} 复制代码
该方法首先将shortcutItem
解析为对应的DeeplinkType
,然后返回一个布尔值来表明解析是否成功。如果成功会讲对应的shortcut
保存在deeplinkType
变量中。
@discardableResult
表明编译器可以忽略该方法的返回值,不报警告。
回到appDelegate
中完成performActionFor
方法中的处理操作:
// MARK: Shortcuts
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
completionHandler(Deeplinker.handleShortcut(item: shortcutItem))
} 复制代码
最后,我们需要将shortcut item 解析成对应的DeeplinkType
我们已经创建了ShortcutParser
类来负责所有shortcut相关的操作处理,在该类中添加以下方法:
func handleShortcut(_ shortcut: UIApplicationShortcutItem) -> DeeplinkType? {
switch shortcut.type {
case ShortcutKey.activity.rawValue:
return .activity
case ShortcutKey.messages.rawValue:
return .messages(.root)
case ShortcutKey.newListing.rawValue:
return .newListing
default:
return nil
}
}复制代码
切换到DeeplinkManager
中并完成handleShortcut
方法:
@discardableResult
func handleShortcut(item: UIApplicationShortcutItem) -> Bool {
deeplinkType = ShortcutParser.shared.handleShortcut(item)
return deeplinkType != nil
} 复制代码
到这对于3Dtouch的操作我们已经基本完成,让我们再来一步一步的回顾下。当我们点击shortcut icon时:
- 1.
shortcut action
触发appDelegate的performActionForShortcutItem
方法 - 2.
performActionForShortcutItem
将ShortcutItem
交个DeeplinkManager
- 3.
DeeplinkManager
会将ShortcutItem
交个ShortcutParser
尝试将ShortcutItem
解析成DeeplinkType
- 4.在
applicationDidBecomeActive
方法中我们检测是否有DeeplinkTypes
需要处理。 - 5.如果存在需要处理的
DeeplinkTypes
(这说明第3步解析成功),让后交个DeeplinkNavigator
做相应的跳转. - 6.一旦短链被处理,
DeeplinkManager
会重置当前的短链为nil
,避免处理两次。
运行app并查看app启动和从后台被调起两种场景下的处理结果。你应该会看到提示对应信息的alert弹窗。
Deep Links
我们将要处理的deeplinks格式如下:
deeplinkTutorial://messages/1
deeplinkTutorial://request/1复制代码
拷贝并存储URLs在你的测试设备的"Notes"上,之后点击任一链接,并没有什么卵用。
如果这是一个
Universal Link
点击链接会再网页中打开该链接。
当我们完成接下来这一部分后,点击该链接会在app内打开的指定页面。
AppDelegate会检测到app通过depplinkURL打开的,并触发openUrl
方法:
// MARK: Deeplinks
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
}复制代码
方法的返回值表明代理是否打开了url
如果你想支持Universal Links
(iOS 9 之后支持),还需要添加以下代理方法:
// MARK: Universal Links
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {
}
}
return false
} 复制代码
ContunueUserActivity
也会通过Spotlighs items
触发(本教程不触及)
和处理3Dtouch模式一样,我们需要创建一个DeeplinkParser
类
class DeeplinkParser {
static let shared = DeeplinkParser()
private init() { }
}复制代码
我们创建一个返回值为DeeplinkType
可选类型,接收一个URL参数的方法来解析Deeplink:
func parseDeepLink(_ url: URL) -> DeeplinkType? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let host = components.host else {
return nil
}
var pathComponents = components.path.components(separatedBy: "/")
// the first component is empty
pathComponents.removeFirst()
switch host {
case "messages":
if let messageId = pathComponents.first {
return DeeplinkType.messages(.details(id: messageId))
}
case "request":
if let requestId = pathComponents.first {
return DeeplinkType.request(id: requestId)
}
default:
break
}
return nil
}复制代码
注:解析方法要依赖于你的deeplinks的结构,我的解决方案仅仅是个示例!
现在只需要将解析器与我们的主link类关联起来,并添加如下方法到DeeplinkManager
:
@discardableResult
func handleDeeplink(url: URL) -> Bool {
deeplinkType = DeeplinkParser.shared.parseDeepLink(url)
return deeplinkType != nil
}复制代码
在appDelegate
中完成openUrl
和continueUserActivity
方法:
// MARK: Deeplinks
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return Deeplinker.handleDeeplink(url: url)
}
// MARK: Universal Links
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {
return Deeplinker.handleDeeplink(url: url)
}
}
return false
}复制代码
还有就是告诉app那些link需要监测。以sourceCode
形式打开info.plist
添加以下内容
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.deeplinkTut.Deeplink</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deeplinkTutorial</string>
</array>
</dict>
</array>复制代码
注:确保XML格式正确.
添加以上内容后以Property List形式打开info.Plist
你会看到以下几行:
这意味着app仅仅检测以deeplinkTutorial
开头的URL。
我们再来回顾下整个流程:
- 1.用户在app外点击了deeplink链接
- 2.appDelegate检测到链接并触发
openUrl
代理方法(或者ContinueUserActivity
代理方法,如果链接是Universal links
) - 3.
openUrl
方法将链接传递给Deeplink Manager
- 4.
DeeplinkManager
会试着用DeeplinkParser
将link解析为Deeplink Type
- 5.在
applicationDidBecomeActive
方法中执行DeeplinkTypes
检测。 - 6.如果存在
DeeplinkType
(意味着步骤4解析成功),利用DeeplinkNavigator
执行相应的跳转。 - 7.一旦链接被处理,
DeeplinkManager
重置当前短链
为nil
.防止二次调用。
运行app并点击保存在Notes
中的链接查看结果:
通知
关于工程推送的配置并不是本教程的重点,详细的推送配置你可以在这里和这里查看
发送APNs通知,你可以用本地服务和PusherAPI(这是一个便捷的方法)
这里仅仅包含推送通知的点击和结果处理。
当app处于关闭状态或者运行在后台时,点击推送banner会触发didReceiveRemoteNotification
代理方法:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
}复制代码
该方法在app处于前台运行时也会被触发,因为我们目前只是考虑打开app内指定的页面,所以处于前台运行时的情况并不在考虑范围内。
为了便于处理推送的通知,我们创建一个NotificationParser
类来专门处理通知相关的操作:
class NotificationParser {
static let shared = NotificationParser()
private init() { }
func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
return nil
}
}复制代码
回到DeeplinkManager
中添加如下相关方法:
func handleRemoteNotification(_ notification: [AnyHashable: Any]) {
deeplinkType = NotificationParser.shared.handleNotification(notification)
}复制代码
最后一步是在NotificationParser
中完成通知的解析方法,解析方法依赖于你定义的推送消息结构,但是基本的解析大同小异:
func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
if let data = userInfo["data"] as? [String: Any] {
if let messageId = data["messageId"] as? String {
return DeeplinkType.messages(.details(id: messageId))
}
}
return nil
}复制代码
如果配置app支持推送并想要看下测试结果,下边是我发送的消息结构:
apns: {
aps: {
alert: {
title: "New Message!",
subtitle: "",
body: "Hello!"
},
"mutable-content": 0,
category: "pusher"
},
data: {
"messageId": "1"
}
}复制代码
本教程用的NodeJS服务和Pusher API 发送通知,需要花几分钟时间了解基本的NodeJS知识和一些复制-粘贴技能来建立推送服务。
运行app并切换到后台,然后发送推送消息,收到后点击消息,你会看到如下结果:
上图发生的过程如下:
- 1.当你点击通知时,app触发
didReceiveRemoteNotification
代理方法 - 2.
didReceiveRemoteNotification
代理方法将推送消息转发给DeeplinkManager
- 3.
DeeplinkManager
调用NotificationParser
的方法来解析Notification User Info为Deeplink Types - 4.applicationDidBecomeActive代理方法执行DeeplinkTypes检测。
- 5.如果存在DeeplinkTypes(第三步解析成功),利用DeeplinkNavigator做相应的跳转。
- 6.一旦短链被处理,DeeplinkManager会将当前短链置为nil,防止再次调用。
使用这种方法我们可以很容易的添加或改变处理项,且不需要太多的重要代码改动。更重要的是你可以用适当的解析器去解析深度链接。比如添加一个“New Request”的通知处理,你只需要改变下NotificationParser中的handleNotification方法如下:
func handleNotification(_ userInfo: [AnyHashable : Any]) -> DeeplinkType? {
if let data = userInfo["data"] as? [String: Any] {
if let messageId = data["messageId"] as? String {
return DeeplinkType.messages(.details(id: messageId))
}
if let requestId = data["requestId"] as? String {
return DeeplinkType.request(.details(id: requestId))
}
}
return nil
}复制代码
注:我们并没有在
didFinishLaunchingWithOptions处理*deeplinks所有的处理操作都在applicationDidBecomeActive中处理
恭喜!现在你的app全部支持shortcuts,Deeplinks和Notifications!
完整的工程在这里
如果你觉得有收获请点击❤️