3D Touch相關(guān)(一) —— 基于3D Touch的Peek 和 Pop(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.10.11 星期四

前言

3D Touch是一種立體觸控技術(shù)兜挨,被蘋果稱為新一代多點(diǎn)觸控技術(shù),是在Apple Watch上采用的Force Touch眯分,屏幕可感應(yīng)不同的感壓力度觸控拌汇。3D Touch,蘋果iPhone 6s以后的機(jī)型中出現(xiàn)的新功能弊决,看起來(lái)類似 PC 上的右鍵噪舀。有Peek Pop 兩種新手勢(shì)。2015年9月10日飘诗,蘋果在新品發(fā)布會(huì)上宣布了3D Touch功能与倡。Force Touch和3Dtouch其實(shí)是基于同一種技術(shù),且都基于蘋果的Taptic引擎昆稿,但是不管你承不承認(rèn)或者有沒有意識(shí)到纺座,3D Touch的確更優(yōu)于Force Touch。接下來(lái)這個(gè)專題我們就看一下3D Touch相關(guān)的內(nèi)容溉潭。

開始

首先看一下寫作環(huán)境净响。

Swift 4.2, iOS 12, Xcode 10

自Apple推出3D Touch以及iPhone 6S以來(lái),用戶已經(jīng)能夠使用新的喳瓣,備用的馋贤,基于觸摸的交互來(lái)訪問應(yīng)用程序中的功能。 通過給屏幕提供不同程度的壓力畏陕,用戶可以使用Peek預(yù)覽頁(yè)面配乓,然后Pop到預(yù)覽頁(yè)面。 通過使用3D Touch增強(qiáng)您的應(yīng)用程序,您可以為用戶提供更加身臨其境的專業(yè)體驗(yàn)扰付。

以下是您將在本文中執(zhí)行的操作:

  • 在Storyboard中實(shí)現(xiàn)Peek和Pop堤撵。
  • 以編程方式處理Peek和Pop。
  • Peeking時(shí)自定義內(nèi)容大小羽莺。
  • 在Peeking時(shí)提供action实昨。

注意:雖然iOS模擬器確實(shí)支持3D Touch,但您的計(jì)算機(jī)或觸控板必須啟用強(qiáng)制觸控盐固。 即使啟用了強(qiáng)制觸控功能荒给,手勢(shì)仍然很棘手。 如果可能的話刁卜,我建議您使用支持3D Touch的設(shè)備志电。

打開已經(jīng)準(zhǔn)備好的備用工程,看一下sb中的內(nèi)容蛔趴。

Build并運(yùn)行如下顯示:


Adding Peek and Pop - 添加Peek和Pop

打開Main.storyboard并找到All Geotifications場(chǎng)景的segue挑辆,ShowGeotification,并通過選中Preview&Commit Segues的復(fù)選框啟用Peek&Pop孝情。

構(gòu)建并運(yùn)行應(yīng)用程序鱼蝉。 確保您在應(yīng)用中至少有一個(gè)地理位置的位置。 選擇“書簽”圖標(biāo)箫荡,導(dǎo)航到All Geotifications魁亦。 使用3D Touch接合表格中的第一行。 如果您不熟悉使用3D Touch羔挡,請(qǐng)?jiān)诓煌潭鹊膲毫ο逻M(jìn)行操作洁奈,看看Peek與Pop需要多少壓力。

如果您正在尋找3D Touch的快速實(shí)現(xiàn)绞灼,利用Storyboard的內(nèi)置功能是一個(gè)輕松的勝利利术。


Custom Handling - 自定義處理

使用Storyboard將3D Touch集成到您的應(yīng)用程序非常簡(jiǎn)單,您可能會(huì)遇到一些情況镀赌,您需要更好地控制在peek or pop期間發(fā)生的事情 - 或者您甚至不使用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)
}

在以編程方式實(shí)現(xiàn)3D Touch之前幔戏,您需要做兩件事:

  • 1) 檢查設(shè)備上是否有3D Touch非常重要玛追。 視圖控制器的trait collection使用forceTouchCapability屬性提供了一種簡(jiǎn)單的方法。
  • 2) 如果您的設(shè)備支持3D Touch,您只需調(diào)用registerForPreviewing(with:sourceView :)痊剖。 在這里韩玩,您將視圖控制器作為委托,并將annotation view作為源視圖陆馁。 這意味著找颓,當(dāng)您使用3D Touch來(lái)啟用annotation view時(shí),它將成為Peek或預(yù)覽控制器的源叮贩。

接下來(lái)击狮,將以下擴(kuò)展名添加到文件末尾:

// 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益老。因?yàn)槟炎?cè)annotation view以與3D觸摸交互彪蓬,previewContext(_:viewControllerForLocation :)里就是你將在Peeks和Pops期間展示的視圖控制器。

  • 1) previewingContext使您可以訪問觸摸源捺萌。如果您注冊(cè)了多個(gè)源視圖档冬,則可以區(qū)分這些視圖。一旦知道源視圖是map annotation桃纯,就可以創(chuàng)建AddGeotificationViewController酷誓。
  • 2) 默認(rèn)情況下,預(yù)覽的大小將填充設(shè)備的大部分屏幕慈参。在這種情況下呛牲,有太多的空白,減少預(yù)覽的大小看起來(lái)會(huì)更好驮配。在這里娘扩,您只需更改addGeotificationViewControllerpreferred Content Size的高度。 iOS會(huì)自動(dòng)為您處理寬度壮锻。
  • 3) 最后琐旁,您需要提供有關(guān)Peek將Pop的信息。在previewingContext(_:commit)中猜绣,您可以處理用戶應(yīng)該如何導(dǎo)航到previewed的視圖控制器灰殴,這里只需pushing到導(dǎo)航堆棧即可。

Build并運(yùn)行掰邢。點(diǎn)擊annotation’s pin牺陶,然后在注釋視圖上啟動(dòng)3D Touch。您現(xiàn)在將看到您的預(yù)覽更適合其內(nèi)容視圖辣之。

使用Storyboard處理Peeking時(shí)掰伸,您也可以完成相同的效果。 打開ListTableViewController.swift并用以下內(nèi)容替換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并運(yùn)行怀估。 然后狮鸭,導(dǎo)航回Geotifications列表合搅。 現(xiàn)在,當(dāng)您使用3D Touch接合單元格時(shí)歧蕉,您將看到預(yù)覽的視圖控制器的大小已更改灾部。 無(wú)論您是編寫UI代碼還是使用Storyboard的純粹主義者,您都會(huì)發(fā)現(xiàn)它并不需要做太多工作惯退。


Adding Actions - 添加動(dòng)作

雖然使用3D Touch預(yù)覽和導(dǎo)航到視圖很酷赌髓,但您可以做更多的事情來(lái)為此功能增加價(jià)值。 您可能在電子郵件中的3D觸摸式回復(fù)或轉(zhuǎn)發(fā)過程中遇到過操作蒸痹,或者查看Apple Music中的某首歌曲可用的所有選項(xiàng)時(shí)春弥。 為自己添加這些稱為UIPreviewActions的選項(xiàng)仍然與添加3D Touch的前面步驟一樣簡(jiǎn)單。

打開AddGeotificationViewController.swift并將以下協(xié)議方法添加到AddGeotificationsViewControllerDelegate

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

如果調(diào)用preview action叠荠,將在AddGeotificationViewController的委托上調(diào)用此方法匿沛。

現(xiàn)在,在屬性列表之后榛鼎,將以下屬性和方法添加到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期間查看操作逃呼,正在預(yù)覽的視圖控制器必須重寫previewActionItems并返回UIPreviewActionItems數(shù)組。 在這里者娱,您添加了EditDelete操作抡笼。 他們的兩個(gè)處理程序都調(diào)用handle(action:and:),它調(diào)用你之前添加的委托方法黄鳍。 要處理這些操作推姻,您需要在兩個(gè)位置實(shí)現(xiàn)此方法。

首先框沟,打開GeotificationsViewController.swift并將以下代碼添加到AddGeotificationsViewControllerDelegate擴(kuò)展:

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
  }
}

接下來(lái)藏古,打開ListTableViewController.swift,并將以下代碼添加到AddGeotificationsViewControllerDelegate擴(kuò)展:

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中的兩個(gè)操作的方法忍燥。 如果調(diào)用Edit操作拧晕,則推送視圖控制器。 調(diào)用Delete時(shí)梅垄,將從地圖和存儲(chǔ)中刪除Geotification厂捞。

Build并運(yùn)行。 要查看預(yù)覽操作队丝,請(qǐng)?jiān)诳吹筋A(yù)覽時(shí)啟動(dòng)3D Touch并向上滑動(dòng)靡馁。 您應(yīng)該注意到預(yù)覽頂部有一個(gè)白色箭頭,表示存在操作机久。 一旦看到動(dòng)作奈嘿,您就可以自由地將手指從屏幕上抬起。


Grouping Preview Actions - 分組預(yù)覽操作

如果您希望以不同方式對(duì)預(yù)覽操作進(jìn)行分組吞加,則可以使用UIPreviewActionGroup。 這使您可以通過隱藏單個(gè)操作后面的更多操作來(lái)提供有關(guān)您的操作如何相互關(guān)聯(lián)的更多上下文。

要嘗試此操作衔憨,請(qǐng)打開AddGeotificationViewController.swift叶圃,并將previewActionItems替換為以下內(nèi)容:

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時(shí)獲得一組額外的選項(xiàng)践图。

Build并運(yùn)行應(yīng)用程序掺冠。 當(dāng)您選擇Delete時(shí),您現(xiàn)在將看到第二個(gè)預(yù)覽操作码党,即原始Delete德崭。 如果您不想刪除地理定位,這將使您有機(jī)會(huì)改變主意揖盘。

有了這個(gè)眉厨,您已經(jīng)完成了3D Touch在應(yīng)用程序的添加。 看看您是否可以找到要添加到Peek的新操作兽狭,或者為自己實(shí)現(xiàn)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,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末箕慧,一起剝皮案震驚了整個(gè)濱河市服球,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颠焦,老刑警劉巖斩熊,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異伐庭,居然都是意外死亡粉渠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門似忧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)渣叛,“玉大人,你說我怎么就攤上這事盯捌〈狙茫” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵饺著,是天一觀的道長(zhǎng)箫攀。 經(jīng)常有香客問我,道長(zhǎng)幼衰,這世上最難降的妖魔是什么靴跛? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮渡嚣,結(jié)果婚禮上梢睛,老公的妹妹穿的比我還像新娘肥印。我一直安慰自己,他們只是感情好绝葡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布深碱。 她就那樣靜靜地躺著,像睡著了一般藏畅。 火紅的嫁衣襯著肌膚如雪敷硅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天愉阎,我揣著相機(jī)與錄音绞蹦,去河邊找鬼。 笑死榜旦,一個(gè)胖子當(dāng)著我的面吹牛幽七,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播章办,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼锉走,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了藕届?” 一聲冷哼從身側(cè)響起挪蹭,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎休偶,沒想到半個(gè)月后梁厉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡踏兜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年词顾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碱妆。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肉盹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疹尾,到底是詐尸還是另有隱情上忍,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布纳本,位于F島的核電站窍蓝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏繁成。R本人自食惡果不足惜吓笙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巾腕。 院中可真熱鬧面睛,春花似錦絮蒿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至亲茅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狗准,已是汗流浹背克锣。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腔长,地道東北人袭祟。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像捞附,于是被迫代替她去往敵國(guó)和親巾乳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容