版本記錄
版本號(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 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)
}
在以編程方式實(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ì)更好驮配。在這里娘扩,您只需更改
addGeotificationViewController
的preferred 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ù)組。 在這里者娱,您添加了Edit
和 Delete
操作抡笼。 他們的兩個(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]
}
通過將deleteAction
和cancelAction
添加到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)注~~~