1. 程式人生 > >Core Location 電子圍欄:入門

Core Location 電子圍欄:入門

原文:Geofencing with Core Location: Getting Started
作者:Andy Pereira
譯者:kmyhy

更新說明:Andy Pereira 將本教程升級至 Xcode 9.3 和 Swift 4.1。

Geofencing 會在裝置進入/離開指定的電子圍欄時通知應用程式。它可以讓你寫出一些很酷的應用程式,當你從家裡出來時觸發通知,或者在附近出現最愛的商店時,用最近的、最多的訂單提示給使用者。在這個 Geofencing 教程中,你將學習如何在 iOS 和 swift 中使用區域檢測——即 Core Location 的 Region Monitoring API。

另外,你將建立一個基於定位提醒的 app,Geotify,它允許使用者基於物理位置建立提醒。讓我們開始吧!

開始

使用底部的 Dowload Materials 按鈕下載開始專案。它包含了一個用於在地圖上新增/刪除大頭釘簡單 UI。每個大頭釘表示一個指定位置的提醒項,我把它叫做 geotification。

Build & run,你會看到一張空空的地圖。

點選 + 按鈕,新增一個 geotification。app 會另外顯示一張檢視,允許你設定 geotification 的各種屬性。

在本教程中,你將在蘋果的新總部卡布基諾新增一個大頭釘。如果不知道位置,開啟谷歌地圖,用它找到正確的位置。要讓大頭釘定位精確,請放大地圖。

注:要在模擬器上使用 pinch 手勢,請按下 option 鍵,然後用 shift 鍵移動手指的中心點,然後放開 shift 鍵,拖動滑鼠進行捏放。

Radius 表示從指定位置開始距離多少米,在這個位置上 iOS 將觸發通知。該通知可以是您希望在通知中顯示的任何訊息。這個 app 還可以讓使用者通過頂部的 segment control 來指定在圓圈內是用進入還是退出來觸發通知。

在 Radius 上輸入 1000,在 Note 上輸入 Say Hi to Tim!,把第一個電子圍欄通知置於 Upon Entry 上。

填完後點擊 Add 按鈕。你會看到地圖上顯示了一個新的大頭釘,外面還圍了一個圓圈表示的電子圍欄:

點選大頭釘,你會看到通知詳情,比如提醒內容和事件型別。不要點選那個小叉叉,除非你想刪除它!

您可以隨意新增或刪除任意多的地理位置。由於該應用程式使用 UserDefaults 進行持久化,所以在重啟後的位置列表仍然會存在。

配置 Core Location 和許可權

現在,你新增到地圖上的電子圍欄通知還沒有真正的功能,只能看看而已。要解決這個問題,你需要遍歷每個位置,使用 Core Location 監聽它的保護範圍。

在開始監控電子圍欄之前,你需要建立 CLLocationManager 例項並請求相應的許可權。

開啟 GeotificationsViewController.swift 宣告一個 CLLocationManager 常量。可以將它放在 var geotifications: [Geotification] = []: 一句之後。

let locationManager = CLLocationManager()

然後修改 viewDidLoad() 程式碼:

override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  locationManager.delegate = self
  // 2
  locationManager.requestAlwaysAuthorization()
  // 3
  loadAllGeotifications()
}

程式碼分為 3 步:

  1. 將 view controller 設定為 locationManager 的 delegate,這樣 view controller 就會通過代理方法接收到通知。
  2. 呼叫 requestAlwaysAuthorization() 方法,它會顯示一個對話方塊,向用戶請求總是使用定位服務的全新。地理圍欄 app 必須請求一直使用定位服務的許可權,因為哪怕是 app 不執行的期間也要監聽電子圍欄。Info.plist 的 NSLocationAlwaysAndWhenInUseUsageDescription 鍵中包含了要向用戶顯示的資訊。從 iOS 11 開始,所有請求一直允許許可權的 app 同時也要允許使用者選擇“僅在應用期間使用”。Info.plist also 的 NSLocationWhenInUseUsageDescription 鍵用於指定它的資訊。重要的是儘可能簡單地向用戶說明為什麼要使用“一直允許”許可權。
  3. 呼叫 loadAllGeotifications() 方法,讀取之前儲存到 UserDefaults 中的 geotification 列表並載入到 geotifications 陣列中。這個方法還會將 geotification 載入到地圖上的大頭釘中。

當應用程式提示使用者進行授權時,它將顯示 NSLocationAlwaysAndWhenInUseUsageDescription 字串,它向用戶進行了很好的解釋,說明為什麼該應用需要訪問使用者的位置。當你請求授權時,這個 key 是必需的。如果缺失這個 key,系統會忽略請求,並禁用定位服務。

Build & run,你會看到:

你已經完成了 app 的請求授權許可。好了,請點選允許,確保 location manager 能夠在適當的時機收到委託回撥。

在繼續下一步之前,還有一個小問題:使用者當前的位置沒有顯示在地圖圖上!預設情況下,map view 會禁用這個特性,同時,在導航欄左上角的 zoom 按鈕也不會起作用。

幸好這個問題很容易解決——你只需要在使用者同意授權之後開啟 current location屬性。

在 GeotificationsViewController.swift 的CLLocationManagerDelegate 擴充套件中新增委託方法:

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
  mapView.showsUserLocation = (status == .authorizedAlways)
}

location manager 在授權狀態發生改變時呼叫 locationManager(_:didChangeAuthorizationStatus:) 方法,如果使用者同意授權,location manager 會在你初始化了 location mananger 並設定其 delegate 之後呼叫這個方法。

因此這個方法是檢查 app 是否被授權的理想場所。如果使用者已授權,你就可以啟用 mapview 的 showsUserLocation。

Build & run。如果你在真機上執行,你會看到 mapview 上顯示出大頭釘。如果在模擬器執行,請點選 Debug ▸ Location ▸ Apple 選單,就可以看見定位大頭釘了:

另外,現在導航欄上的 zoom 按鈕也可以使用了。

註冊電子圍欄

配置好 location manager 之後,你現在必須讓 app 註冊使用者要監控的電子圍欄範圍。

使用者的電子圍欄資訊儲存在 Geotification 模型中。但是,要對電子圍欄進行監控,Core Location 需要你將它們表示為 CLCircularRegion 物件。要滿足這個條件,你需要建立一個助手方法,將一個 Geonotification 對映為 CLCircularRegion。

開啟 GeotificationsViewController.swift 新增方法:

func region(with geotification: Geotification) -> CLCircularRegion {
  // 1
  let region = CLCircularRegion(center: geotification.coordinate, 
    radius: geotification.radius, 
    identifier: geotification.identifier)
  // 2
  region.notifyOnEntry = (geotification.eventType == .onEntry)
  region.notifyOnExit = !region.notifyOnEntry
  return region
}

這個方法做了這些事情:

  1. 以電子圍欄的中心座標、半徑、以及識別 app 中已註冊的不同電子圍欄的 ID 來初始化一個 CLCircularRegion。初始化過程很簡單,因為在 Geotification 中已經包含了這些資訊。
  2. CLCircularRegion 有兩個 boolean 屬性: notifyOnEntry 和 notifyOnExit。它們代表在裝置進入/離開圍欄時所對應不同的事件。因為 app 只允許一個圍欄監控一種型別的事件,你需要針對 Geotificaiton 物件的 eventType 值來設定這兩個值。

接著,當用戶添加了 geotification 之後,需要用一個方法來啟動對指定 geotification 的監控。

在 GeotificationsViewController 中加入方法:

func startMonitoring(geotification: Geotification) {
  // 1
  if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
    showAlert(withTitle:"Error", message: "Geofencing is not supported on this device!")
    return
  }
  // 2
  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.
      """
    showAlert(withTitle:"Warning", message: message)
  }
  // 3
  let fenceRegion = region(with: geotification)
  // 4
  locationManager.startMonitoring(for: fenceRegion)
}

上述程式碼的執行步驟簡單說明如下:

  1. isMonitoringAvailableForClass(_:) 方法判斷裝置是否支援電子圍欄監控功能。如果不支援,退出並提示使用者。showAlert() 是 Utilities.swift 中的一個助手方法,需要一個顯示時用到的 title 引數和一個 message 引數。
  2. 然後,判斷使用者是否授權 app 使用定位服務。如果未授權,app 不會接收任何電子圍欄通知。但是,你仍然需要使用者能夠儲存圍欄資訊,因為用 Core Location 註冊電子圍欄不需要授權。當用戶後面對 app 進行授權後,電子圍欄監控會自動開啟。
  3. 用工具方法為指定的 geotification 建立 CLCircularRegion 物件。
  4. 然後,註冊該 CLCircularRegion,讓 Core Location 的 location manager 開始監控。

寫完這個方法,你需要另一個方法,當用戶從 app 中刪除 geotification 時停止監控它。

在 GeotificationsViewController.swift,在 startMonitoring(geotificiation:) 方法下新增:

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

這個方法簡單停止 locationManager 對指定 geotification 的監控。

開始和停止方法完成後,你可以用在新增、刪除 geotification 時呼叫它們了。首先是新增部分。

首先開啟 GeotificationsViewController.swift,找到 addGeotificationViewController(_:didAddCoordinate:radius:identifier:note:eventType:) 方法。

addGeotificationViewController 在建立 geotification時呼叫這個委託方法。它負責建立一個新的 Geotification對 象並重新整理地圖和 geotifications 陣列。最後,它會呼叫 saveAllGeotifications(),這個個方法用新的 geotifications 陣列作為引數,將它存到 UserDefaults 中。

現在,修改 addGeotificationViewController(_:didAddCoordinate:radius:identifier:note:eventType:) 方法為:

func addGeotificationViewController(
  _ controller: AddGeotificationViewController, didAddCoordinate coordinate: CLLocationCoordinate2D, 
  radius: Double, identifier: String, note: String, eventType: Geotification.EventType
) {
  controller.dismiss(animated: true, completion: nil)
  // 1
  let clampedRadius = min(radius, locationManager.maximumRegionMonitoringDistance)
  let geotification = Geotification(coordinate: coordinate, radius: clampedRadius, 
    identifier: identifier, note: note, eventType: eventType)
  add(geotification)
  // 2
  startMonitoring(geotification: geotification)
  saveAllGeotifications()
}

主要修改有兩處:

  1. 防止 radius 的值不大於 locationManager 的 maximumRegionMonitoringDistance 屬性,這個屬性是以米為單位的電子圍欄最大半徑。這是重要的,因為如果超過了這個最大值,會導致監控失敗。
  2. 呼叫 startMonitoring(geotification:) 以向 Core Location 註冊新新增的 geotification。

這樣,app 就能夠註冊電子圍欄的監控了。但是有一個限制:因為電子圍欄是系統共享資源,Core Location 限制每個 app 的最大監控電子圍欄數是 20。

出於本教程的目的,對這個限制的解決辦法(在最後面“接下來去哪兒”會有一些討論),是限制使用者能夠新增 geotification 的數目。

在 updateGeotificationCount() 最後新增程式碼:

navigationItem.rightBarButtonItem?.isEnabled = (geotifications.count < 20)

這行程式碼在新增書達到上限時禁用 Add 按鈕。

最後,是刪除 geotification。這個功能是在mapView(_:annotationView:calloutAccessoryControlTapped:) 中進行的,當用戶點選大頭釘上的 delete 按鈕會呼叫這個方法。

在 mapView(_:annotationView:calloutAccessoryControlTapped:)的 remove(geotification) 前面新增一句:

stopMonitoring(geotification: geotification)

這會停止電子圍欄監控,然後刪除 geotification,儲存修改到 UserDefaults。

這樣,你的 app 已經能夠監控和停止監控使用者的電子圍欄了。太好了!

Build & run。你發現沒有任何改變,但 app 已經能夠註冊監控區域了。但是,它還不能響應電子圍欄事件。別急——這是接下來的工作!

響應電子圍欄事件

需要實現幾個關於錯誤處理的委託方法。這會用在萬一有錯誤出現的時候。

在 GeotificationsViewController.swift, 在 CLLocationManagerDelegate 擴充套件中新增:

func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, 
                     withError error: Error) {
  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)")
}

這些委託方法簡單地輸出錯誤資訊以便除錯。

注:在你的生產專案中,你肯定想在錯誤處理時更多做些事情。例如,你不想安靜地輸出日誌,而想通知使用者發生了什麼。

然後,開啟 AppDelegate.swift,你將在這裡監聽並處理電子圍欄的進入/退出事件。

在頭部 import Core Location 框架:

import CoreLocation

在 var window: UIWindow? 下新增新屬性:

let locationManager = CLLocationManager()

將 application(_:didFinishLaunchingWithOptions:) 修改為:

func application(
  _ application: UIApplication, 
  didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil
) -> Bool {
  locationManager.delegate = self
  locationManager.requestAlwaysAuthorization()
  return true
}

你讓 AppDelegate 接收電子圍欄事件。先不管 Xcode 在這裡提示的錯誤警告,等會我們會解決它。你可能奇怪,“為什麼要在 AppDelegate 而不是 view controller 中幹這件事呢?”

iOS 會無時不刻地監控 app 註冊的電子圍欄, 哪怕 app 沒有運行了。如果裝置在 app 不在執行的情況下觸發電子圍欄事件,iOS 會自動開啟後臺中的 app。因此 AppDelegate 是處理這類事件的理想場所,因為 view controller 可能並沒有載入或者就緒。

你還可能奇怪,“為什麼剛剛才建立的 CLLocationMananger 就知道要監控什麼電子圍欄呢?”

你的 app 所註冊的一切電子圍欄都能被 app 中所有的 location mananger 訪問,因此無論你在哪裡初始化 location mananger 都無所謂。很爽吧?:]

接下來就是實現電子圍欄事件的相關委託方法了。在此之前,需要建立一個處理電子圍欄事件的方法。

在 AppDelegate.swift 新增方法:

func handleEvent(for region: CLRegion!) {
  print("Geofence triggered!")
}

這個方法有一個 CLRegion 引數,它只是簡單地列印一個資訊。別急——後面你會實現事件的處理。

然後,在 AppDelegate 的最後新增一個擴充套件:

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

正如方法名所示,當裝置進入某個區域時呼叫 locationManager(_:didEnterRegion:) 方法,當裝置退出某個區域時呼叫 locationManager(_:didExitRegion:) 方法。

兩個方法都會收到一個 CLRegion 引數。你需要判斷它是不是 CLCircularRegion,因為如果 app 監控的是 iBeacon 時它有可能是一個 CLBeaconReion。如果 region 就是一個 CLCircularRegion,再呼叫 handleEvent(for:)。

注:iOS 在判斷到有穿過邊界的行為時觸發電子圍欄事件。如果使用者已經位於某個區域內部,iOS 不產生事件。如果你需要知道裝置是否位於指定區域以內或者以外,你需要使用蘋果提供的 requestStateForRegion(_:) 方法。

現在 app 已經能夠監控電子圍欄事件了,你需要來測試一下。無論它能不能挑動你的神經,這都是一件值得興奮的事情,因為在本教程中,你終於能夠看到點結果了。

最真實的測試方式是在裝置上執行你的 app,新增幾個 geotification 然後拿著手機到處走或者開車溜溜。但是,這並不明智,因為你無法在裝置不插線的情況下,檢視電子圍欄事件觸發的列印日誌。此外,在你提交測試之前,確保這個 app 工作正常是一個好的做法。

幸好,有一個簡單的法子,不需要你離開你溫暖的小窩。Xcode 允許你在專案中使用 GPX 檔案模擬測試位置。在開始專案中已經包含了一個!

開啟 Supporting Files 組下的 SimulatedLocations.gpx,你會看到:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
  <wpt lat="37.3349285" lon="-122.011033">
    <name>Apple</name>
    <time>2014-09-24T14:00:00Z</time>
  </wpt>
  <wpt lat="37.422" lon="-122.084058">
    <name>Google</name>
    <time>2014-09-24T14:00:05Z</time>
  </wpt>
</gpx>

這其實是一個 XML 檔案,包含了兩個路徑點(wpt,waypoint):Google 的山景城和丘珀蒂諾的蘋果公園。你會看到每個路徑點有一個 time 節點。它們之間相差有 5 秒,因此用這個檔案模擬位置時,它會花 5 秒鐘從蘋果走到谷歌。還有兩個 gpx 檔案:Apple.gpx 和 Google.gpx。這是固定位置,你可以它們建立電子圍欄。

要模擬 GPX 檔案中的位置,請 Build & run。當 app 進入主檢視控制器後,回到 Xcode,選擇除錯工具欄的 Location 圖示,然後選擇 SimulatedLocations:

沿著兩個路徑點之間的路徑新增幾個 geotification。如果在註冊電子圍欄之前新增過 geotification,那麼這些 geotification 不會有效,你可以刪除它們重新開始。

關於測試位置,一種好的做法是在每個路徑點上防止一個 geotification。這是可能的一種測試場景:

Google: Radius: 1000m, Message: "Say Bye to Google!", Notify on Exit
Apple: Radius: 1000m, Message: "Say Hi to Apple!", Notify on Entry

注:為了讓新增位置的時候更容易,你可以用額外提供的測試地點。

一旦添加了 geotification,你會在進入/離開電子圍欄時看到控制檯列印訊息。如果你按下 Home 鍵或者鎖屏,讓 app 轉入後臺,每當你穿過電子圍欄時仍然能看到列印訊息,儘管你無法虛擬地驗證這種行為。

注:模擬位置既可以在模擬器也可以在裝置上執行。但是,模擬器不是很精確,無論進入還是退出電子圍欄,地觸發對應的事件的時機不是很一致。在裝置上要好得多,或者更流暢,拿起手機來起來逛逛!

通知使用者電子圍欄事件

app 已經完成大半。這裡,當裝置穿過電子圍欄時通知使用者的工作就留給你了,因此請自行完成這個功能。

要通過委託回撥獲取和 CLCircularRegion 相關的 note 描述字串,你必須從 UserDefaults 中檢索對應的 geotification。這也不足為道,因你您可以使用註冊時分配給 CLCircularRegion 的唯一 ID 來找到正確的 geotification。

在 AppDelegate.swift 新增一句匯入語句:

import UserNotifications

接著,新增一個工具方法:

func note(from identifier: String) -> String? {
  let geotifications = Geotification.allGeotifications()
  guard let matched = geotifications.filter {
    $0.identifier == identifier
  }
  .first else { return nil }
  return matched.note
}

這個工具方法根據 ID 從持久儲存中查詢 geotification 的 note,然後返回。

現在你已經能返回電子圍欄的 note 了,可以編寫當電子圍欄事件觸發的程式碼並將 note 用於提示的訊息。

在 application(_:didFinishLaunchingWithOptions:) 方法 returns 之前新增:

let options: UNAuthorizationOptions = [.badge, .sound, .alert]
UNUserNotificationCenter.current()
  .requestAuthorization(options: options) { success, error in
  if let error = error {
    print("Error: \(error)")
  }
}

最後,新增這個方法:

func applicationDidBecomeActive(_ application: UIApplication) {
  application.applicationIconBadgeNumber = 0
  UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
  UNUserNotificationCenter.current().removeAllDeliveredNotifications()
}

這段程式碼提示使用者開啟遠端通知。此外,還清空了已有的通知。

然後,修改 handleEvent(for:) 方法為:

func handleEvent(for region: CLRegion!) {
  // 如果 app 是 active 的,顯示一個 alert
  if UIApplication.shared.applicationState == .active {
    guard let message = note(from: region.identifier) else { return }
    window?.rootViewController?.showAlert(withTitle: nil, message: message)
  } else {
    // 否則顯示本地通知
    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)")
      }
    }
  }
}

如果 app 是啟用(active)的,上面的程式碼會用 alert controller 方式顯示 note。否則,顯示本地為通知。

Build & run,重複上一節所述的測試過程。當電子圍欄事件觸發,你會看到 alert 顯示出 note 上的備註:

按 Home 鍵或者鎖屏,將 app 切換至後臺。你仍然會繼續收到電子圍欄事件的通知。

這樣,你就擁有一個完整的、基於定位的提醒 app 了。好,請離開座位,把你的 app 帶出去 show 一下吧!

注:在測試 app 時,可能會發現通知發出的時間並不是剛好在你穿過邊界的時候。

這是因為 iOS 在判斷是否穿過邊界之前,有一個緩衝距離,和裝置必須在新位置停留的最短時間。iOS 在內部定義了這些閾值,目的是減少使用者在極度接近電子圍欄邊界的情況下發出的錯誤通知。

此外,這些閾值好像受定位硬體效能的限制。從經驗上講,當在裝置上啟用 Wi-Fi 時,電子圍欄會更加精確。

接下來去哪裡?

恭喜你!你已經可以編寫你自己的電子圍欄 app 了!

你可以從頁尾的 Download Materials 按鈕處下載完成後的專案。

電子圍欄是一種很有用的技術,在市場營銷、資源管理、安全、家長控制、甚至遊戲等領域都有許多實用而深遠的應用,你能達到什麼樣的目標完全取決於你的想象力。更多資訊可以閱讀蘋果的區域監控

我希望你喜歡這篇教程。歡迎在下面留言或提問。

Download Materials