CoreLocation框架详细解析(十七) —— 基于Core Location地理围栏的实现(二)

版本记录

版本号 时间
V1.0 2020.12.12 星期六

前言

很多的app都有定位功能,比如说滴滴,美团等,他们都需要获取客户所在的位置,并且根据位置推送不同的模块数据以及服务,可以说,定位方便了我们的生活,接下来这几篇我们就说一下定位框架CoreLocation。感兴趣的可以看我写的上面几篇。
1. CoreLocation框架详细解析 —— 基本概览(一)
2. CoreLocation框架详细解析 —— 选择定位服务的授权级别(二)
3. CoreLocation框架详细解析 —— 确定定位服务的可用性(三)
4. CoreLocation框架详细解析 —— 获取用户位置(四)
5. CoreLocation框架详细解析 —— 监控用户与地理区域的距离(五)
6. CoreLocation框架详细解析 —— 确定接近iBeacon(六)
7. CoreLocation框架详细解析 —— 将iOS设备转换为iBeacon(七)
8. CoreLocation框架详细解析 —— 获取指向和路线信息(八)
9. CoreLocation框架详细解析 —— 在坐标和用户友好的地名之间转换(九)
10. CoreLocation框架详细解析(十) —— 跟踪访问位置简单示例(一)
11. CoreLocation框架详细解析(十一) —— 跟踪访问位置简单示例(二)
12. CoreLocation框架详细解析(十二) —— 仿Runkeeper的简单实现(一)
13. CoreLocation框架详细解析(十三) —— 仿Runkeeper的简单实现(二)
14. CoreLocation框架详细解析(十四) —— 仿Runkeeper的简单实现(三)
15. CoreLocation框架详细解析(十五) —— 仿Runkeeper的简单实现(四)
16. CoreLocation框架详细解析(十六) —— 基于Core Location地理围栏的实现(一)

源码

1. Swift

首先看下工程组织结构

然后再看下sb中的内容:

下面就是源码啦

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    let options: UNAuthorizationOptions = [.badge, .sound, .alert]
    UNUserNotificationCenter.current().requestAuthorization(options: options) { _, error in
      if let error = error {
        print("Error: \(error)")
      }
    }
    return true
  }

  func applicationDidBecomeActive(_ application: UIApplication) {
    application.applicationIconBadgeNumber = 0
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
  }
}
2. SceneDelegate.swift
import UIKit
import CoreLocation

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  let locationManager = CLLocationManager()

  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    locationManager.delegate = self
    locationManager.requestAlwaysAuthorization()
  }
}

// MARK: - Location Manager Delegate
extension SceneDelegate: 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)
    }
  }

  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 = .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? {
    let geotifications = Geotification.allGeotifications()
    let matched = geotifications.first { $0.identifier == identifier }
    return matched?.note
  }
}
3. GeotificationsViewController.swift
import UIKit
import MapKit
import CoreLocation

enum PreferencesKeys: String {
  case savedItems
}

class GeotificationsViewController: UIViewController {
  @IBOutlet weak var mapView: MKMapView!

  var geotifications: [Geotification] = []
  lazy var locationManager = CLLocationManager()

  override func viewDidLoad() {
    super.viewDidLoad()

    // 1
    locationManager.delegate = self
    // 2
    locationManager.requestAlwaysAuthorization()
    // 3
    loadAllGeotifications()
  }


  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "addGeotification",
      let navigationController = segue.destination as? UINavigationController,
      let addVC = navigationController.viewControllers.first as? AddGeotificationViewController {
      addVC.delegate = self
    }
  }

  // MARK: Loading and saving functions
  func loadAllGeotifications() {
    geotifications.removeAll()
    let allGeotifications = Geotification.allGeotifications()
    allGeotifications.forEach { add($0) }
  }

  func saveAllGeotifications() {
    let encoder = JSONEncoder()
    do {
      let data = try encoder.encode(geotifications)
      UserDefaults.standard.set(data, forKey: PreferencesKeys.savedItems.rawValue)
    } catch {
      print("error encoding geotifications")
    }
  }

  // MARK: Functions that update the model/associated views with geotification changes
  func add(_ geotification: Geotification) {
    geotifications.append(geotification)
    mapView.addAnnotation(geotification)
    addRadiusOverlay(forGeotification: geotification)
    updateGeotificationsCount()
  }

  func remove(_ geotification: Geotification) {
    guard let index = geotifications.firstIndex(of: geotification) else { return }
    geotifications.remove(at: index)
    mapView.removeAnnotation(geotification)
    removeRadiusOverlay(forGeotification: geotification)
    updateGeotificationsCount()
  }

  func updateGeotificationsCount() {
    title = "Geotifications: \(geotifications.count)"
    navigationItem.rightBarButtonItem?.isEnabled = (geotifications.count < 20)
  }

  // MARK: Map overlay functions
  func addRadiusOverlay(forGeotification geotification: Geotification) {
    mapView.addOverlay(MKCircle(center: geotification.coordinate, radius: geotification.radius))
  }

  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.zoomToLocation(mapView.userLocation.location)
  }

  // MARK: Put monitoring functions below
  func startMonitoring(geotification: Geotification) {
    // 1
    if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
      showAlert(
        withTitle: "Error",
        message: "Geofencing is not supported on this device!")
      return
    }

    // 2
    let fenceRegion = geotification.region
    locationManager.startMonitoring(for: fenceRegion)
  }

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

// MARK: AddGeotificationViewControllerDelegate
extension GeotificationsViewController: AddGeotificationsViewControllerDelegate {
  func addGeotificationViewController(
    _ controller: AddGeotificationViewController,
    didAddGeotification geotification: Geotification
  ) {
    controller.dismiss(animated: true, completion: nil)

    // 1
    geotification.clampRadius(maxRadius:
      locationManager.maximumRegionMonitoringDistance)
    add(geotification)

    // 2
    startMonitoring(geotification: geotification)
    saveAllGeotifications()
  }
}

// MARK: - Location Manager Delegate
extension GeotificationsViewController: CLLocationManagerDelegate {
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    // 1
    let status = manager.authorizationStatus

    // 2
    mapView.showsUserLocation = (status == .authorizedAlways)

    // 3
    if status != .authorizedAlways {
      let message = """
      Your geotification is saved but will only be activated once you grant
      Geotify permission to access the device location.
      """
      showAlert(withTitle: "Warning", message: message)
    }
  }

  func locationManager(
    _ manager: CLLocationManager,
    monitoringDidFailFor region: CLRegion?,
    withError error: Error
  ) {
    guard let region = region else {
      print("Monitoring failed for unknown region")
      return
    }
    print("Monitoring failed for region with identifier: \(region.identifier)")
  }

  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(systemName: "trash.fill"), for: .normal)
        annotationView?.leftCalloutAccessoryView = removeButton
      } 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) {
    // Delete geotification
    guard let geotification = view.annotation as? Geotification else { return }
    stopMonitoring(geotification: geotification)
    remove(geotification)
    saveAllGeotifications()
  }
}
4. AddGeotificationViewController.swift
import UIKit
import MapKit

protocol AddGeotificationsViewControllerDelegate: class {
  func addGeotificationViewController(_ controller: AddGeotificationViewController, didAddGeotification: Geotification)
}

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!

  weak var delegate: AddGeotificationsViewControllerDelegate?

  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.rightBarButtonItems = [addButton, zoomButton]
    addButton.isEnabled = false
  }

  @IBAction func textFieldEditingChanged(sender: UITextField) {
    let radiusSet = !(radiusTextField.text?.isEmpty ?? true)
    let noteSet = !(noteTextField.text?.isEmpty ?? true)
    addButton.isEnabled = radiusSet && noteSet
  }

  @IBAction func onCancel(sender: AnyObject) {
    dismiss(animated: true, completion: nil)
  }

  @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
    let geotification = Geotification(
      coordinate: coordinate,
      radius: radius,
      identifier: identifier,
      note: note,
      eventType: eventType)
    delegate?.addGeotificationViewController(self, didAddGeotification: geotification)
  }

  @IBAction private func onZoomToCurrentLocation(sender: AnyObject) {
    mapView.zoomToLocation(mapView.userLocation.location)
  }
}
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
    let formatter = MeasurementFormatter()
    formatter.unitStyle = .short
    formatter.unitOptions = .naturalScale
    let radiusString = formatter.string(from: Measurement(value: radius, unit: UnitLength.meters))
    return "Radius: \(radiusString) - \(eventTypeString)"
  }

  func clampRadius(maxRadius: CLLocationDegrees) {
    radius = min(radius, maxRadius)
  }

  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 = CLLocationCoordinate2DMake(latitude, 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)
  }
}

extension Geotification {
  public class func allGeotifications() -> [Geotification] {
    guard let savedData = UserDefaults.standard.data(forKey: PreferencesKeys.savedItems.rawValue) else { return [] }
    let decoder = JSONDecoder()
    if let savedGeotifications = try? decoder.decode(Array.self, from: savedData) as [Geotification] {
      return savedGeotifications
    }
    return []
  }
}


// MARK: - Notification Region
extension Geotification {
  var region: CLCircularRegion {
    // 1
    let region = CLCircularRegion(
      center: coordinate,
      radius: radius,
      identifier: identifier)

    // 2
    region.notifyOnEntry = (eventType == .onEntry)
    region.notifyOnExit = !region.notifyOnEntry
    return region
  }
}
6. 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 zoomToLocation(_ location: CLLocation?) {
    guard let coordinate = location?.coordinate else { return }
    let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10_000, longitudinalMeters: 10_000)
    setRegion(region, animated: true)
  }
}

后记

本篇主要讲述了基于Core Location地理围栏的实现,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容