在本文,我會演示如何在 iOS 上實現地域定向。我會介紹蘋果傳統的 CLRegion
類。我還將介紹如何對這種不常見的功能進行測試。我們還會演示如何實現複雜的跟蹤邏輯。最後,我將解釋如何創建你「自訂的」region,以及爲什麽要使用「自訂的」 region,它比起 CLRegion
來有什麽優點。通過地域定向,你可以開發出大量基於位置信息的創意 App。
Geo Targeting 項目
假設我們想記錄用戶所去過的餐廳。我們開始創建這個項目。首先,創建一個名為GeoTargeting
的 Single View Application
類型的 Swift 項目。
打開 Main.storyboard
,在 ViewController 中加入一個 Map View。為 Map View 創建一個 @IBOutlet
連接。編譯器會報一個錯,先不管它,待會我們再解決它。故事版這邊的事情暫時就這樣了,讓我們來編輯 ViewController.swift
。
首先引入蘋果的 MapKit
和 CoreLocation
框架。讓 ViewController
聲明對兩個相關協議的實現。最終, ViewController.swift
應該是這個樣子:
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
@IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad( )
}
}
接下來要生成一份 locationManager 對象,並配置 mapView。
// 1.創建 locationManager
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad( )
// 2. 配置 locationManager
locationManager.delegate = self;
locationManager.distanceFilter = kCLLocationAccuracyNearestTenMeters;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
// 3. 配置 mapView
mapView.delegate = self
mapView.showsUserLocation = true
mapView.userTrackingMode = .Follow
// 4. 加入測試數據
setupData()
}
這些程式碼負責完成如下工作:
- 創建一份 locationManager 對象,用於偵測用戶位置的變化。
- 配置 locationManager 對象,例如,將它的 delegate 設為 ViewController,這樣就可以在 ViewController 內部跟蹤用戶位置並監控 region 事件。 同時,將定位精度設定為最佳精度。
- 配置 mapView。將 mapView 的 delegate 設置為 ViewController ,這樣我們就可以在 mapView 中畫出一些額外的東西。然後讓 mapView 顯示用戶的當前位置並使地圖隨用戶位置而動,這有助於我們知道用戶是否進入了某個 region 。你也可以通過故事板來設置 MapView 的 delegate 和 showsUserLocation,我們在程式碼中這樣做,無非是讓你更便於理解而已。
- 添加測試數據。setupData 方法稍後再介紹。
現在,應該可以跟蹤到用戶位置了。當然我們還需要詢問用戶以獲得必要的權限。我們在程式碼中會檢測用戶是否同意授予我們這些權限。
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// 1. 還沒有詢問過用戶以獲得權限
if CLLocationManager.authorizationStatus() == .NotDetermined {
locationManager.requestAlwaysAuthorization()
}
// 2. 用戶不同意
else if CLLocationManager.authorizationStatus() == .Denied {
showAlert("Location services were previously denied. Please enable location services for this app in Settings.")
}
// 3. 用戶已經同意
else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
locationManager.startUpdatingLocation()
}
}
讓我們看一下這個方法。首先,我們在 viewDidAppear
方法而不是其它方法中檢查用戶授權狀態,是因為用戶有會去設置程序中修改授權,然後又回到 App。因此我們需要在這時重新對用戶授權狀態進行檢查。
在檢查授權狀態時:
- 如果第一次開啟 App,我們還沒有向用戶申請過權限,我們應當立即這樣做。之所以申請的是「Always」權限,是因為要使用
CLRegion
必須獲得「Always」權限。 - 如果用戶不同意授權,我們會告訴用戶,我們的 App 只有在獲得使用定位服務的權限之後才能很好地工作。 這裡我們調用了
showAlert(title: String)
方法,這是一個簡化方法,用 UIAlertController 來顯示一些信息,并帶有一個 「Cancel」 按鈕。後面會經常用到這個方法。 - 如果用戶同意了我們的權限申請,我們就可以開始啟動標準的位置更新服務了。
然後運行 App。這時,你就會覺得奇怪—— App 沒有彈出提示讓用戶同意使用位置服務的消息啊?我們還應當在 ...-Info.plist
文件中增加一個 key NSLocationAlwaysUsageDescription
,并賦予它一個值,諸如「爲什麽 App 需要【總是】訪問你的位置服務」。這個 key 會在我們調用 requestAlwaysAuthorization()
方法的時候用到。如果不這樣做,系統會忽略一切關於位置的功能調用。這個 key 只是用於向用戶描述你的 App 使用用戶位置的用途。
加完這個 key 之後,再次運行 App。現在系統就會彈出一個消息框,提示用戶授權並顯示我們剛才添加的描述。
當然,除了前面提到的幾種狀態,位置授權狀態還有其它幾種。不過在這個 App 中,我們只需要最基本的幾種狀態就足矣。
接下來我們準備監控 region,首先需要添加 region。
func setupData() {
// 1. 檢查系統是否能夠監視 region
if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion.self) {
// 2.準備 region 會用到的相關屬性
let title = "Lorrenzillo's"
let coordinate = CLLocationCoordinate2DMake(37.703026, -121.759735)
let regionRadius = 300.0
// 3. 設置 region 的相關屬性
let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,
longitude: coordinate.longitude), radius: regionRadius, identifier: title)
locationManager.startMonitoringForRegion(region)
// 4. 創建大頭釘(annotation)
let restaurantAnnotation = MKPointAnnotation()
restaurantAnnotation.coordinate = coordinate;
restaurantAnnotation.title = "\(title)";
mapView.addAnnotation(restaurantAnnotation)
// 5. 繪製一個圓圈圖形(用於表示 region 的範圍)
let circle = MKCircle(centerCoordinate: coordinate, radius: regionRadius)
mapView.addOverlay(circle)
}
else {
print("System can't track regions")
}
}
// 6. 繪製圓圈
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
let circleRenderer = MKCircleRenderer(overlay: overlay)
circleRenderer.strokeColor = UIColor.redColor()
circleRenderer.lineWidth = 1.0
return circleRenderer
}
讓我們逐行分析 setupData()
方法中的語句。
- 首先要檢查用戶設備當前是否支持 region 範圍監控。如果用戶禁止了位置服務,關閉了後台App 刷新,或者設備處於飛行模式,isMonitoringAvailableForClass 方法將返回 false。
- 爲了演示,我們創建了一個虛擬的餐廳位置。在我們的例子中這樣做是 OK 的,但在真實項目中,你應當專門為這個對象創建一個單獨的類。
- 然後創建一個 region,以便 App 對其進行監控。我們用餐廳的名字作為 region 的唯一標識,這是因為只有這樣我們才能識別用戶進入了哪個 region。這樣做還有一個好處,我們不需要為 CLRegion 創建強引用,而是將 region 的 identifier 保存起來就可以了。
- 爲了好看,我們在 region 的中央加了一顆大頭釘。
- 爲了好看,我們還在地圖上畫了一個圓圈,用於代表 region 所包含的範圍。
- 在
MKMapViewDelegate
的委託方法中繪製這個圓圈。
項目創建完成。我們可以開始對 region 進行監控了。
蘋果的 CLRegion
在本文,我們使用地理學的 region 概念。蘋果的 CLRegion
是一個以固定座標為圓心、以指定半徑為半徑的圓形區域。因此,使用 CLRegion
我們只能監控圓形 region。接下來,我們編寫追蹤 region 的代碼。
// 1. 當用戶進入一個 region
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
showAlert("enter \(region.identifier)")
}
// 2. 當用戶退出一個 region
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
showAlert("exit \(region.identifier)")
}
這兩個方法用於通知用戶,他當前正在穿過 region 的邊界。我們用我們簡化的 alert 方法來提示用戶。這兩個方法只會在用戶穿過 region邊界(還要加上系統定義的一個緩衝距離)時觸發。緩衝距離的使用是爲了防止當用戶在靠近邊界時,系統在短時間內頻繁觸發大量的進入/退出事件。
同時,可以跟蹤的 region 數目是有限的。系統給每個 App 的限制是 20 個 region。如果你想跟蹤的 region 數超過了這個限制,你可以只跟蹤用戶當前位置最近的幾個 region。每當用戶的位置移動,就將較遠的 region 移除,而將用戶移動路徑上的新 region 添加到監控中。如果超過限制,location manager 會觸發 monitoringDidFailForRegion
方法,為了體驗更好,你可以在這個方法中進行必要的處理。
我們需要牢牢記住兩點:兩個委託方法,以及使用時的一些限制。
對於簡單的 region 而言,我們需要編寫的方法就這麼多。現在你可以打開車門,驅車駛過你設定的區域。哈,開個玩笑而已。其實我們有一種很方便的方法來測試我們的 App。
如何測試
這個方法使用了 Xcode 提供的功能。我們將用到一個 GPX 文件。GPX 是一個 XML 文件,使用了軟件開發中常見的 GPS 數據格式進行描述。看一個例子,你就明白了。
這個 GPX 中描述了位於我們 region 附近的 3 個點。Xcode會用它們逐一作為用戶移動的位置。移動的速度是一秒鐘一個點。請從這裡下載 my GPX file America.gpx。下載完畢后將它拖到你的項目中。
然後運行 App。回到 Xcode,打開 Debug Area,選擇「模擬位置」,然後選擇 America。回到模擬器,用戶的位置會開始移動。
幾秒鐘后,你會看到提示 「enter Lorrenzillo’s」,又過幾秒,有會提示 「exit Lorrenzillo’s」。這樣,我們的 region 監控功能就完成了!
實現複雜的業務邏輯
在某些 App 來說,簡單的進入/退出事件就夠了。但如果你要跟蹤更複雜的邏輯呢?也許你想跟蹤用戶進入一個 region 后的時間,或者用戶進入 region 的平均速度。在這個簡單的例子里,我們會判斷用戶進入我們的餐廳后的停留時間是否足夠長,以此來判斷他是否是我們一個顧客,以便我們對他進行下一步的詢問。讓我們來修改代碼。
// 1.
var monitoredRegions: Dictionary = [:]
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
showAlert("enter \(region.identifier)")
// 2.1. 記錄進入時間
monitoredRegions[region.identifier] = NSDate()
}
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
showAlert("exit \(region.identifier)")
// 2.2 退出時移除記錄的進入時間
monitoredRegions.removeValueForKey(region.identifier)
}
// 3. 更新 region
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
updateRegions()
}
- 將用戶的進入時間保存到 NSDictionary。
- 這裡實現了添加/刪除進入時間。
- 在 Location manager 的委託方法 didUpdateLocations 中,我們可以檢查用戶是否在 region 中停留了足夠的時間。
func updateRegions() {
// 1.
let regionMaxVisiting = 10.0
var regionsToDelete: [String] = []
// 2.
for regionIdentifier in monitoredRegions.keys {
// 3.
if NSDate().timeIntervalSinceDate(monitoredRegions[regionIdentifier]!) > regionMaxVisiting {
showAlert("Thanks for visiting our restaurant")
regionsToDelete.append(regionIdentifier)
}
}
// 4.
for regionIdentifier in regionsToDelete {
monitoredRegions.removeValueForKey(regionIdentifier)
}
}
- 假設用 10 秒鐘來識別是不是一個有效的顧客。同時,我們還需要一個變量,用於保存那些用戶已經停留了足夠長時間的 region,一旦完成所需工作后,我們再刪除這些 region。
- 遍歷所有當前正在監控的 region。
- 如果顧客已經停留了足夠長時間,我們向用戶顯示指定信息并將 region 標記為「準備刪除」。
- 刪除已經停留夠時間的 region。
你可以在 updateRegions
方法中實現任何複雜邏輯。
定製化 Region
不過,蘋果的 CLRegion 有幾個限制。其中一個限制是,如果 App 只有「While in Use」權限時,無法監控 region。但我們知道,有許多用戶非常介意電池的壽命,他們不希望你的 App 總是跟蹤他們的位置。這樣去和用戶解釋是行不通的:只要他們允許你的 App 「一直」訪問定位功能就能讓他們享受更完美人生。所以有時候你不得不僅在用戶使用你的 App 的時候才能監控 region。我建議創建“自己的”的 region 類,用它來實現和 CLRegion 一樣的介面和囘調方法。這樣,對於那些已經熟悉了 CLRegion 的開發者來說,你定製的 region 類會非常容易上手。
protocol RegionProtocol {
var coordinate: CLLocation {get}
var radius: CLLocationDistance {get}
var identifier: String {get}
func updateRegion()
}
protocol RegionDelegateProtocol {
func didEnterRegion()
func didExitRegion()
}
你可以用上述協議將 CLRegion 所提供的功能遷移到你“自己的”定製化類中。
CLRegion 的另一個限制是它只能跟蹤圓形區域。有時候我們想監控多邊形的區域(矩形、五角形等)或者橢圓區域。這同樣也可以用“定製化的類”的方式實現。你只要在 RegionDelegateProtocol
協議的 didEnterRegion
方法中檢查這些條件即可。
當然,沒必要隨時向用戶顯示一些提示信息。大部份時候,我們會保存這些數據用於以後的數據分析。
你可以在這裡下載完整的 Xcode 項目示例代碼。