iOS 8 Handoff 指南

Handoff是iOS 8和OS X Yosemite中的一个新特性。它让我们在不同的设备间切换时,可以不间断地继续一个Activity,而不需要重新配置任何设备。

我们可以为在iOS 8和Yosemite上的应用添加Handoff特性。在这篇指南中,我们将学习Handoff的基本功能和如何在非基于文档的app中使用Handoff。

Handoff概览

在开始写代码前,我们需要先来了解一下handoff的一些基本概念。

起步

Handoff不仅可以将当前的activity从一个iOS设备传递到OS X设备,还可以将activity在不同的iOS设备传递。目前在模拟器上还不能使用Handoff功能,所以需要在iOS设备上运行我们的实例。

设备兼容性:iOS

为了查看我们的iOS设备是否支持handoff功能,我们可以查看“设置”->“通用”列表。如果在列表中看到“Handoff与建议的应用程序”,则设备具备Handoff功能。以下截图显示了iPhone 5s(具备Handoff功能)和iPad3(不具备Handoff功能)的对比:

Handoff功能依赖于以下几点:

  1. 一个iCloud账户:我们必须在希望使用Handoff功能的多台设备上登录同一个iCloud账户。
  2. 低功耗蓝牙(Bluetooth LE 4.0):Handoff是通过低功耗蓝牙来广播activities的,所以广播设备和接收设备都必须支持Bluetooth LE 4.0。
  3. iCloud配对:设备必须已经通过iCloud配对。当在支持Handoff的设备上登录iCloud账户后,每台设备都会与其它支持Handoff的设备进行配对。

此时,我们需要确保已经使用同一iCloud账号在两台支持Handoff功能且运行iOS 8+系统的设备上登录了。(译者注:具体配置可以参考在 Chrome(iOS 版)中使用 Handoff)

User Activities

Handoff是基于User Activity的。User Activity是一个独立的信息集合单位,可以不依赖于任何其它信息而进行传输(be handed off)。

NSUserActivity对象表示一个User Activity实例。它封装了程序的一些状态,这些状态可以在其它设备相关的程序中继续使用。

有三种方法和一个NSUserActivity对象交互:

1) 创建一个user activity:原始应用程序创建一个NSUserActivity实例并调用becomeCurrent()以开启一个广播进程。下面是一个实例:

我们可以使用NSUserActivity的userInfo字典来传递本地数据类型对象或可编码的自定义对象以将其传输到接收设备。本地数据类型包括NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, NSUUID和NSURL。通过NSURL可能会有点棘手。在使用NSURL前可以先参考一下下面的“最佳实践”一节。

2) 更新user activity:一旦一个NSUserActivity成为当前的activity,则iOS会在最上层的视图控制器中调用updateUserActivityState(activity:)方法,以让我们有机会来更新user activity。下面是一个实例:

注意我们不要将userInfo设置为一个新的字典或直接更新它,而是应该使用便捷方法addUserInfoEntriesFromDictionary()。

在下文中,我们将学习如何按需求强制刷新user activity,或者是在程序的app delegate级别来获取一个相似功能的回调。

3) 接收user activity:当我们的接收程序以Handoff的方式启动时,程序代理会调用application(:willContinueUserActivityWithType:)方法。注意这个方法的参数不是NSUserActivity对象,因为接收程序在下载并传递NSUserActivity数据需要花费一定的时间。在user activity已经被下载完成后,会调用以下的回调函数:

然后我们可以使用存储在NSUserActivity对象中的数据来重新创建用户的activity。在这里,我们更新我们的应用以继续相关的activity。

Activity类型

当创建一个user activity后,我们必须为其指定一个activity类型。一个activity类型是一个简单的唯一字符串,通常使用反转DNS语义,如com.razeware.shopsnap.view。

每一个可以接收user activity的程序都必须声明其可接收的activity类型。这类似于在程序中声明支持的URL方案(URL scheme)。对于非基于文本的程序,activity类型需要在Info.plist文件中定义,其键值为NSUserActivityTypes,如下所示:

对于支持一个给定activity的程序来说,需要满足三个要求:

  1. 相同的组:两个程序都必须源于使用同一开发者组ID(developer Team ID)的开发者。
  2. 相同的activity类型:发送程序创建某一activity类型的user activity,接收程序必须有相应类型的NSUserActivityTypes入口。
  3. 签约:两个程序必须通过App store来发布或使用同一开发者账号来签约。

现在我们已经学习了user activities和activity类型的基础知识,接下来让我们来看一个实例。

启动工程

本指南的启动工程可以在“启动工程”中下载。下载后,使用Xcode打开工程并在iPhone模拟器中运行。

工程名是ShopSnap,我们可以在这个程序中构建一个简单的购物清单。一个购物项由一个字符串表示,然后我们将购物项存储在一个字符串的数组中。点击+按钮添加一个新的项目到清单中,而轻扫可以移除项目。

我们将在程序中定义两个独立的user activity:

  1. 查看清单。如果用户当前正在查看清单,我们将传输整个数组。
  2. 添加或编译项目。如果用户当前正在添加新的项目,我们将传递一个单一项目的“编辑”activity。

设置开发组

为了让Handoff工作,发送和接收app都必须使用相同的开发组来签约。由于这个示例程序即是发送者也是接收者,所以这很简单!

选择ShopSnap工程,在“通用”选项卡中,在”Team”中选择自己的开发组:

在支持Handoff的设备中编译并运行程序,以确保运行正常,然后继续。

配置activity类型

接下来是配置程序所支持的activity类型。打开”Supporting Files\Info.plist”,点击”Information Property List”旁边的”+“按钮,在”Information Property List”中添加一个新的选项:

键名为”NSUserActivityTypes”,类型设备为数组类型,如下所示:

在NSUserActivityTypes下添加两项并设置类型为字符串。Item 0的值为com.razeware.shopsnap.view,Item 1的值为com.razeware.shopsnap.edit。

这些任意的activity类型对于我们的程序来说是特定和唯一的。因为我们将在程序的不同地方引用它们,所以在独立的文件中将其添加为常量是一种好的实践。

在工程导航中右键点击ShopSnap组,选择”New File \ iOS \ Source \ Swift File”。将文件命名为Constants.swift并确保新类被添加到ShopSnap target中。

在类中添加以下代码:

然后我们就可以使用这两个activity类型的常量。同时我们定义一些用于user activity的userInfo字典的键名字符串。

快速端到端测试

让我们来运行一个快速端到端测试以确保设备可以正确地通信。

打开ListViewController.swift并添加以下两个函数:

我们通过硬编码一个user activity来快速测试,以确保我们可以在另一端正常接收。

上面的代码做了以下两件事:

  1. startUserActivity()是一个辅助函数,它使用一个硬编码的购物清单来创建了一个NSUserActivity实例。然后调用becomeCurrent()来广播这个activity。
  2. 在调用becomeCurrent()后,系统将定期调用updateUserActivityState()。UIViewController从UIResponder类中继承了这个方法,我们应该重写它来更新我们的userActivity的状态。在这里,我们像前面一样使用硬编码来更新购物清单。注意,我们应该使用addUserInfoEntriesFromDictionary方法来修改NSUserActivity的userInfo字典。我们应该总是在方法的结尾调用super.updateUserActivityState()。

注意,我们只需要调用上面的起始方法。在viewDidLoad()起始行下面添加以下代码

开始广播至少需要以上步骤。现在来看看接收者。打开AppDelegate.swift并添加以下代码:

AppDelegate中的这个方法在所有事情都准备好,且一个userActivity被成功传送后调用。在这里我们简单打印userActivity中的userInfo字典。我们返回true来标识我们处理了user activity。

让我们来试试!要想在两台设备中正常工作,还需要做一些协调工作,所以还得仔细跟着。

  1. 在第一台设备上安装并运行程序。
  2. 在第二台设备上安装并运行程序。确保在Xcode中调用程序以便我们能看到打印输出。
  3. 按下电源按钮让第二台设备休眠。在同一台设备上,按下Home键。如果所有事件都正常运行,我们应该可以看到ShopSnap程序的icon显示在屏幕的左下角上。从这里我们可以启动程序,然后在Xcode控制台可以看到以下的日志信息:Received a payload via handoff: { “shopsnap.items.key” = ( “Ice cream”, Apple, Nuts ); }

如果在锁屏下没有看到程序的icon,则在源设备上关闭并重新打开程序。这将强制系统重新广播信息。同时确认一下设备的控制台以查看是否有来自于Handoff的错误消息。

创建一个新的Activity

现在我们有一个基本上可以工作的Handoff程序,是时候来扩展它了。打开ListViewController.swift,更新startUserActivity()方法,这次我们传入实际的购物清单以代码硬编码。使用以下代码来更新方法:

同样,更新ListViewController.swift的updateUserActivityState(activity:)方法,传递购物清单数组:

现在,更新ListViewController.swift中的viewDidLoad(),在从前面的代码中成功获取到清单后开启userActivity,如下所示:

当然,如果程序开始时,清单是空的,则程序不会去广播user activity。我们需要解决这个问题:在用户第一次添加一个购物项到列表时开启user activity。

为了做到这一点,更新ListViewController.swift中代理回调detailViewController(controller:didFinishWithUpdatedItem:)的实现,如下所示:

在此有三种可能:

  1. 用于更新一个已存在的购物项
  2. 用户删除一个存在的购物项
  3. 用户添加一个新的购物项

现存的代码处理了所有的可能性;我们只需要添加一些检测代码,以在有一个非空的清单时开始一个activity。

在两台设备上编译并运行。此时,我们应该可以在一台设备上添加新的项目,然后将其发送给另外一台设备。

收尾

当用户开始添加一个新的项目或编辑一个已存在的项目时,用户可能不是在查看购物清单。所以我们需要停止广播当前activity。同样,当清单中的所有项目被删除时,没有理由去继续广播当前activiry。在ListViewController.swift中添加以下辅助方法:

在stopUserActivity()中,我们废止已存在的NSUserActivity。这让handoff停止广播。

现在有了stopUserActivity(),是时候在适当的地方调用它了。

在ListViewController.swift中,更新prepareForSegue(segue:, sender:)方法的实现,如下所示:

当用户选择一行或者点击添加按钮时,ListViewController准备导航到详情视图。我们废弃当前的清单查看activity。

在同一文件中,更新tableView(_:commitEditingStyle:forRowAtIndexPath:)的实现,如下所示:

当用户从清单中删除一项时,我们需要相应地更新user activity。如果移除清单中的所有项目,我们停止广播。否则,我们设置userActivity的needsSave属性为true。当我们这样做时,系统会立即回调updateUserActivityState(activity:),在这里我们会更新userActivity。

结束这一节之前,还有一种情况需要考虑,用户点击取消按钮,然后从DetailViewController中返回。这触发了一个已存在的场景。我们需要重新开始userActivity。更新unwindDetailViewController(unwindSegue:)的实现,如下所示:

编译并运行,确保所有事情运行正常。尝试添加一些项目到清单中,确保它们在设备间传输。

创建一个编辑Activity

接下来,我们以类似的方式来处理DetailViewController。这一次,我们广播另一个activity类型。

打开DetailViewController.swift并修改textFieldDidBeginEditing(textField:),如下所示:

上面的方法使用项目的字符串的当前内容创建一个“编辑”activity。

当用户继续编辑项目时,我们需要更新user activity。仍然是在DetailViewController.swift中,更新textFieldTextDidChange(notification:)的实现,如下所示:

现在我们已经标记了activity需要更新,接下来实现updateUserActivityState(activity:),以备系统的更新需求:

这里我们简单地更新了当前项为文本输入框中的文本。

编译并运行。此时,如果我们在一个设备中开始添加一个新项或编辑已存在的项目,我们可以将编辑进程同步给另一个设备。

收尾

因为needsSave是一个轻量级的操作,在上面的代码中,你可以根据需要来设置它,然后在每次按键时更新userInfo。

这里有一个小细节你可能已经注意到了。视图控制器在iPad和iPhone的景观模式下中是一个分离视图。这样可以在清单的项目间切换而不需要收起键盘。这种情况发生时,textFieldDidBeginEditing(textField:)方法不会被调用,导致我们的user activity不会更新为新的文本。

为了解决这个问题,更新DetailViewController.swift中item属性的didSet观察者,如下所示:

当用户点击ListViewController中的一个项目时,DetailViewController的item属性被设置。一个简单解决方案是让视图控制器知道,在项目更新时它必须更新activity。

最后,当用户离开DetailViewController时,我们需要废止userActivity,以让编辑activity不再被广播。

在DetailViewController.swift的textFieldShouldReturn(_:)方法的起始位置添加以下代码:

编译并运行程序,确保程序工作正常。接下来,我们将处理接收的activity。

接收Activity

当用户通过Handoff启动程序时,处理接收的NSUserActivity的任务大部分是由程序的delegate来完成的。

假设所有事情运行正常,数据成功传输,iOS会调用application(_:continueUserActivity:restorationHandler:)方法。这是我们与NSUserActivity实例交互的第一次机会。

我们在前面的章节中已经有一个该方法的实现了。现在,我们做如下修改:

我们将userActivity传递给程序的window对象的rootViewController,然后返回true。这告诉系统成功处理了Handoff行为。从这里开始,我们将自己转发调用并恢复activity。

我们在rootViewController中调用的方法是restoreUserActivityState(activity:)。这是在UIResponder中声明的一个标准方法。系统使用这个方法来告诉接收者恢复一个NSUserActivivty实例。

我们现在的任务是沿着视图控制器架构往下,将activity从父视图控制器传递到子视图控制器,直到到达需要使用activity的地方:

根视图控制器是一个TraitOverrideViewController对象,它的任务是管理程序的size classes;它对我们的user activity不感兴趣。打开TraitOverrideViewController.swift并添加以下代码:

在这里,我们获取TraitOverrideViewController的第一个子视图控制器,然后将activity往下传递。这样做是安全的,因为我们知道程序的视图控制器只包含一个子视图控制器。

层级架构中的下一个视图控制器是SplitViewController,在这里事情会变得更有趣一些。

打开SplitViewController.swift并添加以下代码:

SplitViewController知道ListViewController和DetailViewController。如果NSUserActivity是一个列表查看activity类型,则将其传递给ListViewController;否则,如果是一个编辑activity类型,则传递给DetailViewController。

我们将所有的activity传递给正确的对象,现在是时候从这些activity中获取数据了。

打开ListViewController.swift并实现restoreUserActivityState(activity:),如下所示:

在上面的方法中,我们终于可以继续一个查看activity了。因为我们需要维护一个唯一的购物清单时,我们只需要将这些唯一的项目添加到本地列表中,然后保存并更新UI。

编译并运行。现在我们可以看到通过Handoff从另一台设备上同步过来的清单数据了。

编辑activity以类似的方法来处理。打开DetailViewController.swift并实现restoreUserActivityState(activity:),如下所示:

这里获取编辑activity的信息并更新文本域的内容。

编译并运行,查看运行结果!

收尾

当用户在另一台设备上点击程序的icon以表明他们想要继续一个user activity时,系统启动相应的程序。一旦程序启动后,系统调用application(_, willContinueUserActivityWithType:)方法。打开AppDelegate.swift并添加以下方法:

到这里,我们的程序已经下载了NSUserActivity实例及其userInfo有效载荷。现在我们只是简单返回true。这强制程序在每次用户初始Handoff进程时接收activity。如果想要通知用户activity正在处理,则这是个好地方。

到这里,系统开始将数据从一台设备同步到另一台设备上。我们已经覆盖了任务正常运行的所有情况。但是可以想象Handoff的activity在某些情况下会失败。

将以下方法添加到AppDelegate.swift中来处理这种情况:

如果我们接收到除了NSUserCancelledError之外的任何信息,则发生了某些错误,且我们不能恢复activity。在这种情况下,我们显示一个适当的消息给用户。然而,如果用户显示取消Handoff行为,则在这里我们不需要做任何事情,只需要放弃操作。

版本支持

使用Handoff的最佳实践之一是版本化。处理这的一个策略是为每个发送的Handoff添加一个版本号,并且只接收来自这个版本号(或者更早的)handoff。让我们来试试。

打开Constants.swift并添加以下常量:

上面的版本键名和值是我们为这个版本的程序随意挑选的键值对。

如果我们回顾一下上面的章节,系统会定期并自动调用restoreUserActivityState(activity:)方法。这个方法的实现聚集于并限定于实现它的对象的范围内。例如,ListViewController重写了这个方法来更新带有购物清单的userActivity,而DetailViewController的实现是更新当前正在被编辑的项目。

如果涉及到的东西对于userActivity来说是通用的,可用于所有的user activity,如版本号,则处理它的最好的地方就是在AppDelegate中了。

任何时候调用restoreUserActivityState(activity:),系统都会紧接着调用程序delegate的application(application:, didUpdateUserActivity userActivity:)方法。我们使用这个方法来为我们的Handoff添加版本支持。

打开AppDelegate.swift并添加以下代码:

在这里我们简单地使用了程序的版本号来更新了userInfo字典。

仍然是在AppDelegate.swift中,更新application(_:, continueUserActivity: restorationHandler:)的实现,如下所示:

在这里我们检查userAcitivty的版本,只有当版本号与我们知道的相匹配时才传递。编译并运行,确保程序运行正常。

Handoff最佳实践

在结束之前,我们来看看Handoff的最佳实践:

  1. NSURL:在NSUserActivity的userInfo字典中使用NSURL有点棘手。唯一可以安全地在Handoff中传输的NSURLs是使用HTTP/HTTPS和iCloud文档的web网址。我们不能传递本地文件的URL,因为在接收者端,接收者不能正确地转换并映射这些URL。传输文件链接的最好的方式是传递相对路径,然后在接收者端重新构建我们的URL。
  2. 平台特定值:避免使用平台特定值,如滑动视图的内容偏移量;更好的方法是使用相对位置。例如,如果用户查看table view中的一些项目时,在我们的user activity中传递table view最上面的可视项的index path,而不是传递table view可视区域的内容偏移量。
  3. 版本:想想在程序中使用版本和将来的更新。我们可以在程序的未来版本中添加一些新数据格式或者从userInfo字典中移除值。版本让我们可以理好地控制我们的user activity在当前和将来版本的程序中的行为。

下一步是哪

这里是示例工程的最终版本。

如果想了解更多的关于Handoff,流和基于文档的Handoff,则可以查看Handoff的开发文档Apple’s Handoff Programming Guide以获取更多的信息。

如果喜欢这篇文章,则可以下载我们的书iOS 8 by Tutorials,这里塞满了这样的教程。

如果有更多的总量或关于这篇文章的评论,那么可以加入下面的讨论。

收藏 评论

可能感兴趣的话题



直接登录
跳到底部
返回顶部