3D Touch相关(一) —— 基于3D Touch的Peek 和 Pop(一)

版本记录

版本号 时间
V1.0 2018.10.11 星期四

前言

3D Touch是一种立体触控技术,被苹果称为新一代多点触控技术,是在Apple Watch上采用的Force Touch,屏幕可感应不同的感压力度触控。3D Touch,苹果iPhone 6s以后的机型中出现的新功能,看起来类似 PC 上的右键。有Peek Pop 两种新手势。2015年9月10日,苹果在新品发布会上宣布了3D Touch功能。Force Touch和3Dtouch其实是基于同一种技术,且都基于苹果的Taptic引擎,但是不管你承不承认或者有没有意识到,3D Touch的确更优于Force Touch。接下来这个专题我们就看一下3D Touch相关的内容。

开始

首先看一下写作环境。

Swift 4.2, iOS 12, Xcode 10

自Apple推出3D Touch以及iPhone 6S以来,用户已经能够使用新的,备用的,基于触摸的交互来访问应用程序中的功能。 通过给屏幕提供不同程度的压力,用户可以使用Peek预览页面,然后Pop到预览页面。 通过使用3D Touch增强您的应用程序,您可以为用户提供更加身临其境的专业体验。

以下是您将在本文中执行的操作:

  • 在Storyboard中实现Peek和Pop。
  • 以编程方式处理Peek和Pop。
  • Peeking时自定义内容大小。
  • 在Peeking时提供action

注意:虽然iOS模拟器确实支持3D Touch,但您的计算机或触控板必须启用强制触控。 即使启用了强制触控功能,手势仍然很棘手。 如果可能的话,我建议您使用支持3D Touch的设备。

打开已经准备好的备用工程,看一下sb中的内容。

Build并运行如下显示:


Adding Peek and Pop - 添加Peek和Pop

打开Main.storyboard并找到All Geotifications场景的segueShowGeotification,并通过选中Preview&Commit Segues的复选框启用Peek&Pop

构建并运行应用程序。 确保您在应用中至少有一个地理位置的位置。 选择“书签”图标,导航到All Geotifications。 使用3D Touch接合表格中的第一行。 如果您不熟悉使用3D Touch,请在不同程度的压力下进行操作,看看Peek与Pop需要多少压力。

如果您正在寻找3D Touch的快速实现,利用Storyboard的内置功能是一个轻松的胜利。


Custom Handling - 自定义处理

使用Storyboard将3D Touch集成到您的应用程序非常简单,您可能会遇到一些情况,您需要更好地控制在peek or pop期间发生的事情 - 或者您甚至不使用Interface Builder的情况。 但是,这不需要您付出太多努力。

首先,您要将3D Touch添加到地图上所有Geotification pinsMKPinAnnotationView中。

打开GeotificationsViewController.swift,并在mapView(_:viewFor :)中,在以下行下面:

annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

添加下面代码

if let annotationView = annotationView,
  // 1.
  traitCollection.forceTouchCapability == .available {
  // 2. 
  registerForPreviewing(with: self, sourceView: annotationView)
}

在以编程方式实现3D Touch之前,您需要做两件事:

  • 1) 检查设备上是否有3D Touch非常重要。 视图控制器的trait collection使用forceTouchCapability属性提供了一种简单的方法。
  • 2) 如果您的设备支持3D Touch,您只需调用registerForPreviewing(with:sourceView :)。 在这里,您将视图控制器作为委托,并将annotation view作为源视图。 这意味着,当您使用3D Touch来启用annotation view时,它将成为Peek或预览控制器的源。

接下来,将以下扩展名添加到文件末尾:

// MARK: - UIViewController Previewing Delegate

extension GeotificationsViewController: UIViewControllerPreviewingDelegate {
  func previewingContext(_ previewingContext: UIViewControllerPreviewing,
                         viewControllerForLocation location: CGPoint) 
    -> UIViewController? {
     // 1.
    guard let annotationView = previewingContext.sourceView as? MKPinAnnotationView,
      let annotation = annotationView.annotation as? Geotification,
      let addGeotificationViewController = storyboard?
        .instantiateViewController(withIdentifier: "AddGeotificationViewController")
        as? AddGeotificationViewController else { return nil }
    addGeotificationViewController.geotification = annotation
    addGeotificationViewController.delegate = self
    
    // 2.
    addGeotificationViewController.preferredContentSize =
      CGSize(width: 0, height: 360)
    return addGeotificationViewController  }
  
  func previewingContext(_ previewingContext: UIViewControllerPreviewing,
                         commit viewControllerToCommit: UIViewController) {
    // 3.
    navigationController?.show(viewControllerToCommit, sender: nil)
  }
}

为了完成3D Touch的工作,您需要采用UIViewControllerPreviewingDelegate。因为您已注册annotation view以与3D触摸交互,previewContext(_:viewControllerForLocation :)里就是你将在Peeks和Pops期间展示的视图控制器。

  • 1) previewingContext使您可以访问触摸源。如果您注册了多个源视图,则可以区分这些视图。一旦知道源视图是map annotation,就可以创建AddGeotificationViewController
  • 2) 默认情况下,预览的大小将填充设备的大部分屏幕。在这种情况下,有太多的空白,减少预览的大小看起来会更好。在这里,您只需更改addGeotificationViewControllerpreferred Content Size的高度。 iOS会自动为您处理宽度。
  • 3) 最后,您需要提供有关Peek将Pop的信息。在previewingContext(_:commit)中,您可以处理用户应该如何导航到previewed的视图控制器,这里只需pushing到导航堆栈即可。

Build并运行。点击annotation’s pin,然后在注释视图上启动3D Touch。您现在将看到您的预览更适合其内容视图。

使用Storyboard处理Peeking时,您也可以完成相同的效果。 打开ListTableViewController.swift并用以下内容替换prepare(for:sender)

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "ShowGeotification" {
    guard let addViewController =
      segue.destination as? AddGeotificationViewController,
      let cell = sender as? UITableViewCell,
      let indexPath = tableView.indexPath(for: cell) else { return }
    addViewController.geotification =
      GeotificationManager.shared.geotifications[indexPath.row]
    addViewController.delegate = self
    addViewController.preferredContentSize = CGSize(width: 0, height: 360)
  }
}

Build并运行。 然后,导航回Geotifications列表。 现在,当您使用3D Touch接合单元格时,您将看到预览的视图控制器的大小已更改。 无论您是编写UI代码还是使用Storyboard的纯粹主义者,您都会发现它并不需要做太多工作。


Adding Actions - 添加动作

虽然使用3D Touch预览和导航到视图很酷,但您可以做更多的事情来为此功能增加价值。 您可能在电子邮件中的3D触摸式回复或转发过程中遇到过操作,或者查看Apple Music中的某首歌曲可用的所有选项时。 为自己添加这些称为UIPreviewActions的选项仍然与添加3D Touch的前面步骤一样简单。

打开AddGeotificationViewController.swift并将以下协议方法添加到AddGeotificationsViewControllerDelegate

func addGeotificationViewController(_ controller: AddGeotificationViewController,
                                      didSelect action: UIPreviewAction,
                                      for previewedController: UIViewController)

如果调用preview action,将在AddGeotificationViewController的委托上调用此方法。

现在,在属性列表之后,将以下属性和方法添加到AddGeotificationViewController

override var previewActionItems: [UIPreviewActionItem] {
  let editAction = UIPreviewAction(title: "Edit", style: .default) {
    [weak self] (action, controller) in
    self?.handle(action: action, and: controller)
  }
  let deleteAction = UIPreviewAction(title: "Delete", style: .destructive) {
    [weak self] (action, controller) in
    self?.handle(action: action, and: controller)
  }
  return [editAction, deleteAction]
}

private func handle(action: UIPreviewAction, and controller: UIViewController) {
  delegate?.addGeotificationViewController(self, didSelect: action, for: controller)
}

为了让用户在Peek期间查看操作,正在预览的视图控制器必须重写previewActionItems并返回UIPreviewActionItems数组。 在这里,您添加了EditDelete操作。 他们的两个处理程序都调用handle(action:and:),它调用你之前添加的委托方法。 要处理这些操作,您需要在两个位置实现此方法。

首先,打开GeotificationsViewController.swift并将以下代码添加到AddGeotificationsViewControllerDelegate扩展:

func addGeotificationViewController(_ controller: AddGeotificationViewController,
                                    didSelect action: UIPreviewAction,
                                    for previewedController: UIViewController) {
  switch action.title {
  case "Edit":
    navigationController?.show(previewedController, sender: nil)
  case "Delete":
    guard let addGeotificationViewController = previewedController
      as? AddGeotificationViewController,
      let geotification = addGeotificationViewController.geotification else { return }
    remove(geotification)
  default:
    break
  }
}

接下来,打开ListTableViewController.swift,并将以下代码添加到AddGeotificationsViewControllerDelegate扩展:

func addGeotificationViewController(_ controller: AddGeotificationViewController,
                                    didSelect action: UIPreviewAction,
                                    for previewedController: UIViewController) {
  switch action.title {
  case "Edit":
    navigationController?.show(previewedController, sender: nil)
  case "Delete":
    guard let addGeotificationViewController = previewedController
      as? AddGeotificationViewController,
      let geotification = addGeotificationViewController.geotification else { return }
    GeotificationManager.shared.remove(geotification)
    tableView.reloadData()
  default:
    break
  }
}

这两种方法都提供了处理您添加到Peek中的两个操作的方法。 如果调用Edit操作,则推送视图控制器。 调用Delete时,将从地图和存储中删除Geotification

Build并运行。 要查看预览操作,请在看到预览时启动3D Touch并向上滑动。 您应该注意到预览顶部有一个白色箭头,表示存在操作。 一旦看到动作,您就可以自由地将手指从屏幕上抬起。


Grouping Preview Actions - 分组预览操作

如果您希望以不同方式对预览操作进行分组,则可以使用UIPreviewActionGroup。 这使您可以通过隐藏单个操作后面的更多操作来提供有关您的操作如何相互关联的更多上下文。

要尝试此操作,请打开AddGeotificationViewController.swift,并将previewActionItems替换为以下内容:

override var previewActionItems: [UIPreviewActionItem] {
  let editAction = UIPreviewAction(title: "Edit", style: .default) {
    [weak self] (action, controller) in
    self?.handle(action: action, and: controller)
  }
  
  let deleteAction = UIPreviewAction(title: "Delete", style: .destructive) {
    [weak self] (action, controller) in
    self?.handle(action: action, and: controller)
  }
  let cancelAction = UIPreviewAction(title: "Cancel", style: .default) {
    [weak self] (action, controller) in
    self?.handle(action: action, and: controller)
  }
  let group = UIPreviewActionGroup(title: "Delete...",
                                   style: .destructive,
                                   actions: [cancelAction, deleteAction])
  return [editAction, group]
}

通过将deleteActioncancelAction添加到group的操作,您将在选择Delete时获得一组额外的选项。

Build并运行应用程序。 当您选择Delete时,您现在将看到第二个预览操作,即原始Delete。 如果您不想删除地理定位,这将使您有机会改变主意。

有了这个,您已经完成了3D Touch在应用程序的添加。 看看您是否可以找到要添加到Peek的新操作,或者为自己实现3D Touch的地方。


源码

1. Swift源码

1. AppDelegate.swift
import UIKit
import CoreLocation
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  let locationManager = CLLocationManager()
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    locationManager.delegate = self
    locationManager.requestAlwaysAuthorization()
    let options: UNAuthorizationOptions = [.badge, .sound, .alert]
    UNUserNotificationCenter.current()
      .requestAuthorization(options: options) { success, error in
        if let error = error {
          print("Error: \(error)")
        }
    }
    
    return true
  }
  
  func applicationDidBecomeActive(_ application: UIApplication) {
    application.applicationIconBadgeNumber = 0
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
  }
  
  func handleEvent(for region: CLRegion!) {
    // Show an alert if application is active
    if UIApplication.shared.applicationState == .active {
      guard let message = note(from: region.identifier) else { return }
      window?.rootViewController?.showAlert(withTitle: nil, message: message)
    } else {
      // Otherwise present a local notification
      guard let body = note(from: region.identifier) else { return }
      let notificationContent = UNMutableNotificationContent()
      notificationContent.body = body
      notificationContent.sound = UNNotificationSound.default
      notificationContent.badge = UIApplication.shared.applicationIconBadgeNumber + 1 as NSNumber
      let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
      let request = UNNotificationRequest(identifier: "location_change",
                                          content: notificationContent,
                                          trigger: trigger)
      UNUserNotificationCenter.current().add(request) { error in
        if let error = error {
          print("Error: \(error)")
        }
      }
    }
  }
  
  func note(from identifier: String) -> String? {
    guard let matched = GeotificationManager.shared.geotifications.filter({
      $0.identifier == identifier
    }).first else { return nil }
    return matched.note
  }
}

extension AppDelegate: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(for: region)
    }
  }
  
  func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(for: region)
    }
  }
}
2. GeotificationsViewController.swift
import UIKit
import MapKit
import CoreLocation

class GeotificationsViewController: UIViewController {
  @IBOutlet weak var mapView: MKMapView!
  private var locationManager = CLLocationManager()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.delegate = self
    locationManager.requestAlwaysAuthorization()
    GeotificationManager.shared.geotifications.forEach {
      addToMap($0)
    }
  }
  
  // MARK: - Segues
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "addGeotification" {
      guard let addViewController = segue.destination as? AddGeotificationViewController else { return }
      addViewController.delegate = self
    }
  }
  
  @IBAction func listExit(segue: UIStoryboardSegue) {
    mapView.removeAnnotations(mapView.annotations)
    GeotificationManager.shared.geotifications.forEach {
      addToMap($0)
    }
    if GeotificationManager.shared.geotifications.isEmpty {
      updateGeotificationsCount()
    }
  }
  
  // MARK: Functions that update the model/associated views with geotification changes
  func add(_ geotification: Geotification) {
    GeotificationManager.shared.geotifications.append(geotification)
    addToMap(geotification)
  }
  
  private func addToMap(_ geotification: Geotification) {
    mapView.addAnnotation(geotification)
    addRadiusOverlay(forGeotification: geotification)
    updateGeotificationsCount()
  }
  
  func remove(_ geotification: Geotification) {
    GeotificationManager.shared.remove(geotification)
    mapView.removeAnnotation(geotification)
    removeRadiusOverlay(forGeotification: geotification)
    updateGeotificationsCount()
  }
  
  private func updateGeotificationsCount() {
    let count = GeotificationManager.shared.geotifications.count
    title = "Geotifications: \(count)"
    navigationItem.rightBarButtonItem?.isEnabled = (count < 20)
  }
  
  // MARK: Map overlay functions
  private func addRadiusOverlay(forGeotification geotification: Geotification) {
    mapView?.addOverlay(MKCircle(center: geotification.coordinate, radius: geotification.radius))
  }
  
  private func removeRadiusOverlay(forGeotification geotification: Geotification) {
    // Find exactly one overlay which has the same coordinates & radius to remove
    guard let overlays = mapView?.overlays else { return }
    for overlay in overlays {
      guard let circleOverlay = overlay as? MKCircle else { continue }
      let coord = circleOverlay.coordinate
      if coord.latitude == geotification.coordinate.latitude && coord.longitude == geotification.coordinate.longitude && circleOverlay.radius == geotification.radius {
        mapView?.removeOverlay(circleOverlay)
        break
      }
    }
  }
  
  // MARK: Other mapview functions
  @IBAction func zoomToCurrentLocation(sender: AnyObject) {
    mapView.zoomToUserLocation()
  }
}

// MARK: - Location Manager Delegate
extension GeotificationsViewController: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    mapView.showsUserLocation = status == .authorizedAlways
  }
  
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("Location Manager failed with the following error: \(error)")
  }
}

// MARK: - MapView Delegate
extension GeotificationsViewController: MKMapViewDelegate {
  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    let identifier = "myGeotification"
    if annotation is Geotification {
      var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
      if annotationView == nil {
        annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        annotationView?.canShowCallout = true
        let removeButton = UIButton(type: .custom)
        removeButton.frame = CGRect(x: 0, y: 0, width: 23, height: 23)
        removeButton.setImage(UIImage(named: "DeleteGeotification")!, for: .normal)
        annotationView?.leftCalloutAccessoryView = removeButton
        annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        if let annotationView = annotationView,
          // 1.
          traitCollection.forceTouchCapability == .available {
          // 2.
          registerForPreviewing(with: self, sourceView: annotationView)
        }
      } else {
        annotationView?.annotation = annotation
      }
      return annotationView
    }
    return nil
  }
  
  func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if overlay is MKCircle {
      let circleRenderer = MKCircleRenderer(overlay: overlay)
      circleRenderer.lineWidth = 1.0
      circleRenderer.strokeColor = .purple
      circleRenderer.fillColor = UIColor.purple.withAlphaComponent(0.4)
      return circleRenderer
    }
    return MKOverlayRenderer(overlay: overlay)
  }
  
  func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    if control == view.rightCalloutAccessoryView {
      guard let annotation = view.annotation as? Geotification,
        let addGeotificationViewController = storyboard?.instantiateViewController(withIdentifier: "AddGeotificationViewController") as? AddGeotificationViewController else { return }
      addGeotificationViewController.geotification = annotation
      addGeotificationViewController.delegate = self
      navigationController?.show(addGeotificationViewController, sender: nil)
    } else {
      let geotification = view.annotation as! Geotification
      remove(geotification)
    }
  }
}

// MARK: AddGeotificationViewControllerDelegate
extension GeotificationsViewController: AddGeotificationsViewControllerDelegate {
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didAdd geotification: Geotification) {
    navigationController?.popViewController(animated: true)
    GeotificationManager.shared.add(geotification)
    addToMap(geotification)
  }
  
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didChange oldGeotifcation: Geotification, to newGeotification: Geotification) {
    navigationController?.popViewController(animated: true)
    remove(oldGeotifcation)
    GeotificationManager.shared.add(newGeotification)
    addToMap(newGeotification)
  }
  
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didSelect action: UIPreviewAction, for previewedController: UIViewController) {
    switch action.title {
    case "Edit":
      navigationController?.show(previewedController, sender: nil)
    case "Delete":
      guard let addGeotificationViewController = previewedController as? AddGeotificationViewController,
        let geotification = addGeotificationViewController.geotification else { return }
      remove(geotification)
    default:
      break
    }
  }
}

// MARK: - UIViewController Previewing Delegate
extension GeotificationsViewController: UIViewControllerPreviewingDelegate {
  func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
      // 1.
      guard let annotationView = previewingContext.sourceView as? MKPinAnnotationView,
        let annotation = annotationView.annotation as? Geotification,
        let addGeotificationViewController = storyboard?.instantiateViewController(withIdentifier: "AddGeotificationViewController") as? AddGeotificationViewController else { return nil }
      addGeotificationViewController.geotification = annotation
      addGeotificationViewController.delegate = self
      
      // 2.
      addGeotificationViewController.preferredContentSize = CGSize(width: 0, height: 360)
      return addGeotificationViewController
  }
  
  func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
    // 3.
    navigationController?.show(viewControllerToCommit, sender: nil)
  }
}
3. ListTableViewController.swift
import UIKit

class ListTableViewController: UITableViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "ShowGeotification" {
      guard let addViewController = segue.destination as? AddGeotificationViewController,
        let cell = sender as? UITableViewCell,
        let indexPath = tableView.indexPath(for: cell) else { return }
      addViewController.geotification = GeotificationManager.shared.geotifications[indexPath.row]
      addViewController.delegate = self
    }
  }
  
}

// MARK: - Table view data source
extension ListTableViewController {
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return GeotificationManager.shared.geotifications.count
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "GeotificationCell", for: indexPath)
    let geotification = GeotificationManager.shared.geotifications[indexPath.row]
    cell.textLabel?.text = geotification.title
    cell.detailTextLabel?.text = geotification.subtitle
    return cell
  }
}

// MARK: - Table View Delegate
extension ListTableViewController {
  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
  
  // Override to support editing the table view.
  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    guard indexPath.row < GeotificationManager.shared.geotifications.count else { return }
    if editingStyle == .delete {
      tableView.beginUpdates()
      GeotificationManager.shared.remove(GeotificationManager.shared.geotifications[indexPath.row])
      tableView.deleteRows(at: [indexPath], with: .fade)
      tableView.endUpdates()
    }
  }
}

// MARK: - AddGeotificationsViewControllerDelegate

extension ListTableViewController: AddGeotificationsViewControllerDelegate {
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didAdd geotification: Geotification) {}
  
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didChange oldGeotifcation: Geotification, to newGeotification: Geotification) {
    navigationController?.popViewController(animated: true)
    GeotificationManager.shared.remove(oldGeotifcation)
    GeotificationManager.shared.add(newGeotification)
  }
  
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didSelect action: UIPreviewAction, for previewedController: UIViewController) {
    switch action.title {
    case "Edit":
      navigationController?.show(previewedController, sender: nil)
    case "Delete":
      guard let addGeotificationViewController = previewedController as? AddGeotificationViewController,
        let geotification = addGeotificationViewController.geotification else { return }
      GeotificationManager.shared.remove(geotification)
      tableView.reloadData()
    default:
      break
    }
  }
}
4. AddGeotificationViewController.swift
import UIKit
import MapKit
import CoreLocation

protocol AddGeotificationsViewControllerDelegate {
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didAdd geotification: Geotification)
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didChange oldGeotifcation: Geotification, to newGeotification: Geotification)
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didSelect action: UIPreviewAction, for previewedController: UIViewController)
}

class AddGeotificationViewController: UITableViewController {
  @IBOutlet var addButton: UIBarButtonItem!
  @IBOutlet var zoomButton: UIBarButtonItem!
  @IBOutlet weak var eventTypeSegmentedControl: UISegmentedControl!
  @IBOutlet weak var radiusTextField: UITextField!
  @IBOutlet weak var noteTextField: UITextField!
  @IBOutlet weak var mapView: MKMapView!
  
  var delegate: AddGeotificationsViewControllerDelegate?
  var geotification: Geotification?
  
  override var previewActionItems: [UIPreviewActionItem] {
    let editAction = UIPreviewAction(title: "Edit", style: .default) { [weak self] (action, controller) in
      self?.handle(action: action, and: controller)
    }
    
    let deleteAction = UIPreviewAction(title: "Delete", style: .destructive) { [weak self] (action, controller) in
      self?.handle(action: action, and: controller)
    }
    let cancelAction = UIPreviewAction(title: "Cancel", style: .default) { [weak self] (action, controller) in
      self?.handle(action: action, and: controller)
    }
    let group = UIPreviewActionGroup(title: "Delete...", style: .destructive, actions: [cancelAction, deleteAction])
    return [editAction, group]
  }
  
  private func handle(action: UIPreviewAction, and controller: UIViewController) {
    delegate?.addGeotificationViewController(self, didSelect: action, for: controller)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.rightBarButtonItems = [addButton, zoomButton]
    addButton.isEnabled = false
    if let geotification = geotification {
      setup(geotification)
    }
  }
  
  private func setup(_ geotification: Geotification) {
    title = "Edit Geotification"
    eventTypeSegmentedControl.selectedSegmentIndex = geotification.eventType == .onEntry ? 0 : 1
    radiusTextField.text = String(Int(geotification.radius))
    noteTextField.text = geotification.note
    mapView.setCenter(geotification.coordinate, animated: false)
    addButton.title = "Save"
    addButton.isEnabled = true
  }
  
  @IBAction func textFieldEditingChanged(sender: UITextField) {
    addButton.isEnabled = !radiusTextField.text!.isEmpty && !noteTextField.text!.isEmpty
  }
  
  @IBAction func onCancel(sender: AnyObject) {
    navigationController?.popViewController(animated: true)
  }
  
  @IBAction private func onAdd(sender: AnyObject) {
    let coordinate = mapView.centerCoordinate
    let radius = Double(radiusTextField.text!) ?? 0
    let identifier = NSUUID().uuidString
    let note = noteTextField.text ?? ""
    let eventType: Geotification.EventType = (eventTypeSegmentedControl.selectedSegmentIndex == 0) ? .onEntry : .onExit
    if let geotification = geotification {
      let oldGeotification = geotification
      geotification.coordinate = coordinate
      geotification.radius = radius
      geotification.note = note
      geotification.eventType = eventType
      delegate?.addGeotificationViewController(self, didChange: oldGeotification, to: geotification)
    } else {
      let clampedRadius = min(radius, CLLocationManager().maximumRegionMonitoringDistance)
      let geotification = Geotification(coordinate: coordinate, radius: clampedRadius, identifier: identifier, note: note, eventType: eventType)
      delegate?.addGeotificationViewController(self, didAdd: geotification)
    }
  }
  
  @IBAction private func onZoomToCurrentLocation(sender: AnyObject) {
    mapView.zoomToUserLocation()
  }
}
5. Geotification.swift
import UIKit
import MapKit
import CoreLocation

class Geotification: NSObject, Codable, MKAnnotation {
  enum EventType: String {
    case onEntry = "On Entry"
    case onExit = "On Exit"
  }
  
  enum CodingKeys: String, CodingKey {
    case latitude, longitude, radius, identifier, note, eventType
  }
  
  var coordinate: CLLocationCoordinate2D
  var radius: CLLocationDistance
  var identifier: String
  var note: String
  var eventType: EventType
  
  var title: String? {
    if note.isEmpty {
      return "No Note"
    }
    return note
  }
  
  var subtitle: String? {
    let eventTypeString = eventType.rawValue
    return "Radius: \(radius)m - \(eventTypeString)"
  }
  
  init(coordinate: CLLocationCoordinate2D, radius: CLLocationDistance, identifier: String, note: String, eventType: EventType) {
    self.coordinate = coordinate
    self.radius = radius
    self.identifier = identifier
    self.note = note
    self.eventType = eventType
  }
  
  // MARK: Codable
  required init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let latitude = try values.decode(Double.self, forKey: .latitude)
    let longitude = try values.decode(Double.self, forKey: .longitude)
    coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    radius = try values.decode(Double.self, forKey: .radius)
    identifier = try values.decode(String.self, forKey: .identifier)
    note = try values.decode(String.self, forKey: .note)
    let event = try values.decode(String.self, forKey: .eventType)
    eventType = EventType(rawValue: event) ?? .onEntry
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(coordinate.latitude, forKey: .latitude)
    try container.encode(coordinate.longitude, forKey: .longitude)
    try container.encode(radius, forKey: .radius)
    try container.encode(identifier, forKey: .identifier)
    try container.encode(note, forKey: .note)
    try container.encode(eventType.rawValue, forKey: .eventType)
  }
  
}
6. GeotificationManager.swift
import Foundation
import CoreLocation

struct PreferencesKeys {
  static let savedItems = "savedItems"
}

class GeotificationManager: NSObject {
  static let shared = GeotificationManager.init()
  
  var geotifications: [Geotification] = []
  private let locationManager = CLLocationManager()
  
  private override init() {
    super.init()
    locationManager.delegate = self
    guard let savedData = UserDefaults.standard.data(forKey: PreferencesKeys.savedItems) else {
      return
    }
    let decoder = JSONDecoder()
    if let savedGeotifications = try? decoder.decode(Array.self, from: savedData) as [Geotification] {
      geotifications = savedGeotifications
    }
  }
  
  public func add(_ geotification: Geotification) {
    geotifications.append(geotification)
    startMonitoring(geotification: geotification)
    saveAllGeotifications()
  }
  
  public func remove(_ geotification: Geotification) {
    guard let index = GeotificationManager.shared.geotifications.index(of: geotification) else { return }
    geotifications.remove(at: index)
    stopMonitoring(geotification: geotification)
    saveAllGeotifications()
  }
  
  private func saveAllGeotifications() {
    let encoder = JSONEncoder()
    do {
      let data = try encoder.encode(geotifications)
      UserDefaults.standard.set(data, forKey: PreferencesKeys.savedItems)
    } catch {
      print("error encoding geotifications")
    }
  }
  
  private func region(with geotification: Geotification) -> CLCircularRegion {
    let region = CLCircularRegion(center: geotification.coordinate, radius: geotification.radius, identifier: geotification.identifier)
    region.notifyOnEntry = (geotification.eventType == .onEntry)
    region.notifyOnExit = !region.notifyOnEntry
    return region
  }
  
  private func startMonitoring(geotification: Geotification) {
    if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
      print("Geofencing is not supported on this device!")
      return
    }
    
    if CLLocationManager.authorizationStatus() != .authorizedAlways {
      let message = """
      Your geotification is saved but will only be activated once you grant
      Geotify permission to access the device location.
      """
      print(message)
    }
    
    let fenceRegion = region(with: geotification)
    locationManager.startMonitoring(for: fenceRegion)
  }
  
  private func stopMonitoring(geotification: Geotification) {
    for region in locationManager.monitoredRegions {
      guard let circularRegion = region as? CLCircularRegion, circularRegion.identifier == geotification.identifier else { continue }
      locationManager.stopMonitoring(for: circularRegion)
    }
  }
}

extension GeotificationManager: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
    print("Monitoring failed for region with identifier: \(region!.identifier)")
  }
}
7. Utilities.swift
import UIKit
import MapKit

// MARK: Helper Extensions
extension UIViewController {
  func showAlert(withTitle title: String?, message: String?) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
    alert.addAction(action)
    present(alert, animated: true, completion: nil)
  }
}

extension MKMapView {
  func zoomToUserLocation() {
    guard let coordinate = userLocation.location?.coordinate else { return }
    let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000)
    setRegion(region, animated: true)
  }
}

后记

本篇主要讲述了基于3D Touch的Peek 和 Pop,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容