在过去的几天中,我一直忙于开发 try! Swift 会议的官方 App(只剩两周半的时间了,我的天哪 😱)!项目中一大亮点就是,我要实现在 app 中使用 3D Touch 来支持演讲者和展示的内容的弹窗预览(Peek & Pop)。幸运的是,[@allonsykraken](https://twitter.com/allonsykraken)的博文Peek & Pop Spirit Guide让这个任务完成起来比较简单,为 table view 主视图添加 3D Touch 仅仅花费了几分钟时间就搞定了。
然而,在 Q&A 模块中遇到了一些问题。我希望在 cell 中特定的视图上——演讲者的图片——来使用 peek & pop,而不是像其他模块那样,在整个 cell 上使用。
因为这个花费了很长时间才解决,所以我想把它记录下来。(在阅读本文之前,请先阅读Peek & Pop Spirit Guide)
问题所在
为了能够使用 Peak & Pop 功能,我们需要遵循 UIViewControllerPreviewingDelegate 协议——这个协议会告知我们用户使用 3D Touch 功能点击了哪儿,并且在这里返回相应的 ViewController 实例。
因为我的这些图片存在于 cell 中,需要能够区别出用户使用 3D Touch 点击了哪个演讲者的图片,这里我让 cell 遵循 UIViewControllerPreviewingDelegate:
protocol QASessionSpeakerPopDelegate: class { func onCommitViewController(viewController: UIViewController) } extension QASessionTableViewCell: UIViewControllerPreviewingDelegate { func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { let viewsTo3DTouch = [speaker1ImageView, speaker2ImageView, speaker3ImageView] for (index, view) in viewsTo3DTouch.enumerate() where touchedView(view, location: location) { if let speaker = qaSession?.speakers[index] { return viewControllerForSpeaker(speaker) } } return nil } func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) { delegate?.onCommitViewController(viewControllerToCommit) } private func touchedView(view: UIView, location: CGPoint) -> Bool { let locationInView = view.convertPoint(location, fromView: contentView) return CGRectContainsPoint(view.bounds, locationInView) } private func viewControllerForSpeaker(speaker: Speaker) -> UIViewController { let speakerDetailVC = SpeakerDetailViewController() speakerDetailVC.speaker = speaker return speakerDetailVC } } extension QASessionsTableViewController: QASessionSpeakerPopDelegate { func onCommitViewController(viewController: UIViewController) { navigationController?.pushViewController(viewController, animated: true) } }
问题在于,想要使用 Peek & Pop 功能,我们需要使用 registerForPreviewingWithDelegate 来注册视图和设置代理,但是 registerForPreviewingWithDelegate 是 UIViewController 中的方法,所以我们不能够在 cell 的代码中注册视图!
解决办法
问题的关键在于,我们现在需要 view controller 中 注册每一个 cell(而不是像 Schedule 或 Speaker 模块那样直接注册整个 table view)
class QASessionsTableViewController: UITableViewController { override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier(String(QASessionTableViewCell), forIndexPath: indexPath) as! QASessionTableViewCell let qaSession = dataSource.qaSessions[indexPath.section] cell.configure(withQASession: qaSession, delegate: self) if traitCollection.forceTouchCapability == .Available { registerForPreviewingWithDelegate(cell, sourceView: cell.contentView) } return cell } }
更新
[@davedelong 指出](https://twitter.com/davedelong/status/698527490428383232)在 cell 中创建添加到 navigation 中的控制器看起来不是一个明智的选择。 我完全同意他的观点!但是写代码的时候在 deadline 的压迫下,我没有想到更好的实现方式。幸运的是,[@davedelong 提出了一种更好的解决办法](https://twitter.com/NatashaTheRobot/status/698530746449817600),通过这种方式,可以让这些代码保留在应有的 view controller 中!
下面的代码经过了一些重构,希望你能够 get 到其中的精髓!
class QASessionsTableViewController: UITableViewController { var dataSource: QASessionDataSourceProtocol! override func viewDidLoad() { super.viewDidLoad() if traitCollection.forceTouchCapability == .Available { registerForPreviewingWithDelegate(self, sourceView: tableView) } } } extension QASessionsTableViewController: UIViewControllerPreviewingDelegate { func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { if let indexPath = tableView.indexPathForRowAtPoint(location) { let cell = tableView.cellForRowAtIndexPath(indexPath) as! QASessionTableViewCell let viewsTo3DTouch = [cell.speaker1ImageView, cell.speaker2ImageView, cell.speaker3ImageView] for (index, view) in viewsTo3DTouch.enumerate() where touchedView(view, location: location) { let viewRectInTableView = tableView.convertRect(view.frame, fromCoordinateSpace: view.superview!) previewingContext.sourceRect = viewRectInTableView let qaSession = dataSource.qaSessions[indexPath.section] let speaker = qaSession.speakers[index] return viewControllerForSpeaker(speaker) } } return nil } func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) { navigationController?.pushViewController(viewControllerToCommit, animated: true) } private func touchedView(view: UIView, location: CGPoint) -> Bool { let locationInView = view.convertPoint(location, fromView: tableView) return CGRectContainsPoint(view.bounds, locationInView) } private func viewControllerForSpeaker(speaker: Speaker) -> UIViewController { let speakerDetailVC = SpeakerDetailViewController() speakerDetailVC.speaker = speaker return speakerDetailVC } }
下面是效果图——请注意,选中的图片之外的内容才会变得模糊!
结论
所以在某个特定的视图上使用 3D Touch 功能并非难事!一般来说,实现 3D Touch 非常简单和有趣。我强烈推荐您在 app 中添加 3D Touch 的功能。
![peak and pop all the things](https://www.natashatherobot.com/wp-content/uploads/peekpop.jpg) https://www.futantan.com/blog/peek-pop-view-inside-tableviewcell