[翻译]iOS:一个规范深度链接,通知,3Dtouch调起app的工具

原文在这里,水平有限大家,欢迎各位批评指正。



在你的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模式下都需要activityShortcutItemmessageShortcutItem,如果当前用户处于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-touchappicon 查看切换后的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.performActionForShortcutItemShortcutItem交个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中完成openUrlcontinueUserActivity方法:

// 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!

完整的工程在这里

如果你觉得有收获请点击❤️




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值