版本记录
版本号 | 时间 |
---|---|
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
场景的segue
,ShowGeotification
,并通过选中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 pins
的MKPinAnnotationView
中。
打开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) 默认情况下,预览的大小将填充设备的大部分屏幕。在这种情况下,有太多的空白,减少预览的大小看起来会更好。在这里,您只需更改
addGeotificationViewController
的preferred 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
数组。 在这里,您添加了Edit
和 Delete
操作。 他们的两个处理程序都调用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]
}
通过将deleteAction
和cancelAction
添加到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,感兴趣的给个赞或者关注~~~