简介
NSUserActivity并不是一个新的概念,在iOS8中就已经使用它来做Handoff,在iOS9中User Activities变的可以搜索,并且可以在每个Activity里加上Index用的Metadata。但是只能用在用户访问过的或者看见过的内容中。
一旦某些内容被记录进NSUserActivity,就可以在Spotlight和Safari中同时被搜索到。而且还能通过设置'Eligible For Public Indexing'来让这些被Index的内容传到Apple的云端Cloud Index里,从而实现每个用户都能搜索到这个内容。同时Apple也强调了隐私的保护。并不是所有内容都是Public的,同一个内容需要在云端被Index超过一个限额(具体多少没有公布),才会最后成为Public的内容。所以用户不用担心自己看到的内容成为公众都能搜索的内容。
NSUserActivity基本内容
NSUserActivity对象提供了一种轻量级的方式捕获APP的状态并存储可以在之后使用。我们可以使用activity对象捕获用户正在操作的信息,比如:查看APP内容、编辑文本、查看网页、看video等。当系统启动我们的APP之后,activity对象是可以获取的,APP能够使用activity对象的信息恢复activity对象到合理的状态。Spotlight也可以使用activity对象来为用户提高搜索结果。
在关键时刻创建NSUserActivity对象并注册它们到系统。例如:我们可能在用户打开网页、或APP移到后台、或用户在APP执行一些重要的任务的时候创建activity对象。用户的activity对象并不打算跟踪APP中的每一个任务,所以我们不应该使用activity对象用于一些小的编辑或者次要的修改。相反,当用户想之后继续使用或在其它设备上使用,我们应该使用activity对象。我们也可以使用它们为Spotlight 提供更好的搜索结果。
当创建一个用户的activity对象,做以下事情:
1:使用合理的activity type来创建并初始化用户的activity对象。
2:设置用户activity对象的title
3:使用一个或者多个下列属性来配置任务的对象:
isEligibleForHandoff
isEligibleForSearch
isEligibleForPublicIndexing
4:配置activity对象的相关属性
5:对于用户 activity 对象,如果是配置用于搜索和公开位置,可以配置contentAttributeSet, keywords, webpageURL 等属性以至于Spotlight 能够索引对象。
6:调用becomeCurrent() 方法来注册用户 activity 对象
3:使用一个或者多个下列属性来配置任务的对象:
isEligibleForHandoff
isEligibleForSearch
isEligibleForPublicIndexing
4:配置activity对象的相关属性
5:对于用户 activity 对象,如果是配置用于搜索和公开位置,可以配置contentAttributeSet, keywords, webpageURL 等属性以至于Spotlight 能够索引对象。
6:调用becomeCurrent() 方法来注册用户 activity 对象
当我们创建了NSUserActivity对象,我们使用了具体的字符串标识了activity的类型。该activity类型字符串是reverse-DNS格式。例如:当用户打开一个网页,我们可以指定activity的字符串为com.myCompany.myApp.OpenWebPage。我们必须在Info.plist文件中包含NSUserActivityTypes 作为key值对应的activity的类型。系统根据该key的信息来确定你的APP是否有能力处理给定的用户activity对象。并且支持Handoff,支持SiriKit,支持提高搜索结果等。
下面看一个简单demo,直接使用NSUserActivity实现搜索功能
demo非常简单,当运行程序之后,显示的是一个包含几首歌的列表,点击进入是一个详情界面。然后我们可以Command-Shift-H回到home界面,然后滑动到搜索界面,搜索对应的title等关键字就可以看到相应的搜索结果。当点击搜索结果会调到APP的详情界面。原文地址为:这里
首先看一下AppDelegate.swift文件:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
let mainController = (window?.rootViewController as! UINavigationController).viewControllers.first
mainController?.restoreUserActivityState(userActivity)
return true
}
}
主要是关注func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping([Any]?) -> Void) -> Bool方法,当接受到数据相关联的用户activity时,该方法将会被调用。该方法提供了我们机会执行具体的任务来更新APP。如果我们并没有实现该方法中实现了该方法或者返回值为false,iOS将尝试为APP创建文本来打开URL,并且iOS知道APP并不能够处理当前activity。如果返回true表示APP能够处理当前activity。简单一点理解操作就是:当点击搜索之后的内容,会触发该方法,在这里我们可以进行相应的任务处理,demo中执行了页面的跳转。
ListViewController.swift文件 ,即列表页面
import UIKit
class ListViewController: UIViewController {
fileprivate var songList: [SongInfo] = []
//存储搜索item的标识符
private var searchSongIdentifier: Int?
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//添加数据,SongInfo是一个模型类,仅仅是包含了3个属性和初始化方法
songList.append(SongInfo(song: "Bob Dylan - Like a Rolling Stone", album: "Highway 61 Revisited (1965)", style: "Rock"))
songList.append(SongInfo(song: "John Lennon - Imagine", album: "Imagine (1971)", style: "Rock, Pop"))
songList.append(SongInfo(song: "Nirvana - Smells Like Teen Spirit", album: "Nevermind (1991)", style: "Rock"))
}
override func viewWillAppear(_ animated: Bool) {
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: false)
}
super.viewWillAppear(animated)
}
//当执行segue过渡时,会触发该方法,该方法主要是获取选中的位置,便于后续的搜索,以及页面跳转
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
var songId: Int?
if let index = tableView.indexPathForSelectedRow?.row{
songId = index
}else{
songId = searchSongIdentifier
}
//设置对应的歌曲信息和位置
let controller = segue.destination as! DetailedViewController
controller.songInfo = songList[songId!]
controller.songIndex = songId!
}
//判断activity中的userInfo字典中是否包含index作为key所对应的值,如果存在,存储对应的值到searchSongIdentifier,并进行跳转
override func restoreUserActivityState(_ activity: NSUserActivity) {
if let index = activity.userInfo?["index"] as? Int{
searchSongIdentifier = index
self.performSegue(withIdentifier: "showDetail", sender: self)
}
}
}
//MARK: - UITableViewDataSource
extension ListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return songList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let CellIdentifier = "MyCell"
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier)
cell!.textLabel!.text = songList[(indexPath as NSIndexPath).row].song
return cell!
}
}
该页面实现简单,主要是关注prepare和restoreUserActivityState方法,在直接点击列表后,会触发prepare方法,跳转到详情页面,并传递所需的数据。当用户点击搜索结果之后,由于触发func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping([Any]?) -> Void) -> Bool方法,方法中我们执行了restoreUserActivityState方法,这里会对获取index进行相应的判断,一旦获取对应的值,就跳转到对应的详情界面,即我们之前选中的页面。
DetailedViewController.swift文件
import UIKit
class DetailedViewController: UIViewController,NSUserActivityDelegate {
var songInfo: SongInfo!
var songIndex: Int?
@IBOutlet weak var songLabel: UILabel!
@IBOutlet weak var albumLabel: UILabel!
@IBOutlet weak var styleLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
songLabel.text = songInfo.song
albumLabel.text = songInfo.album
styleLabel.text = songInfo.style
//创建NSUserActivity对象,activityType代表activity的类型,值为reverse-DNS format。该值与nfo.plist值保持一致
let activity = NSUserActivity(activityType: "com.appsfoundation.search.song")
//activity的名称,当搜索的时候,我们可以看到对应的title
activity.title = songInfo.song
//在设置title之后,我们可以搜索title,为了提高搜索效果,所有设置一系列局部关键字帮助用户在搜索结果中找到activity
var keywords = songInfo.song.components(separatedBy: "")
keywords.append(songInfo.album)
keywords.append(songInfo.style)
activity.keywords = Set(keywords)
//该布尔值确定是否能够使用Handoff在其它设备使用该activity
activity.isEligibleForHandoff = true
//是否应该被添加到设备index
activity.isEligibleForSearch = true
//是否能够被所有的IOS公众用户访问
activity.isEligibleForPublicIndexing = true
//设置代理
activity.delegate = self
//是否activity需要被更新,如果为真,在activity被发送之前触发代理方法
activity.needsSave = true
//为了避免再indexing之前被释放,需要复制当前创建的activity带全局的userActivity(在UIResponder类中声明)。在创建之后,我们必须添加activity到设备的索引用于搜索结果
userActivity = activity
//标记userActivity为当前使用的activity
userActivity?.becomeCurrent()
}
//通知代理用户的activity将被存储,存储用户点击的index,用于点击搜索的内容定位具体的详情页面
func userActivityWillSave(_ userActivity: NSUserActivity) {
userActivity.userInfo = ["index" : songIndex!]
}
}
该界面非常简单,主要是创建NSUserActivity对象,并设置相应的属性,在代理方法中使用userActivity中的userInfo字典存储由上级传入选中的位置index,方便在搜索之后点击搜索结果重新定位当前界面。
相关界面效果如下: