用 Swift 開發一個 iOS 地域定向 App

地域定向(Geo targeting)是一種根據用戶的地理位置,如國家、地區、城市等來呈現不同內容的技術。地域定向有著廣泛的使用場景。假設你有一個老顧客,他跑到競爭對手的餐廳里用餐。我們可以向他(她)顯示一些特惠折扣的信息,他重新拉回到你的餐廳里來。如果一個用戶在過去數天內逛過多次汽車經銷店,很可能他想買張新車。這樣,我們就可以將我們的汽車廣告推送給他。這種有的放矢的廣告比隨機投放的廣告要有效得多吧。


在本文,我會演示如何在 iOS 上實現地域定向。我會介紹蘋果傳統的 CLRegion 類。我還將介紹如何對這種不常見的功能進行測試。我們還會演示如何實現複雜的跟蹤邏輯。最後,我將解釋如何創建你「自訂的」region,以及爲什麽要使用「自訂的」 region,它比起 CLRegion 來有什麽優點。通過地域定向,你可以開發出大量基於位置信息的創意 App。

geotarget-demo

Geo Targeting 項目

假設我們想記錄用戶所去過的餐廳。我們開始創建這個項目。首先,創建一個名為GeoTargetingSingle View Application類型的 Swift 項目。

打開 Main.storyboard,在 ViewController 中加入一個 Map View。為 Map View 創建一個 @IBOutlet 連接。編譯器會報一個錯,先不管它,待會我們再解決它。故事版這邊的事情暫時就這樣了,讓我們來編輯 ViewController.swift

首先引入蘋果的 MapKitCoreLocation框架。讓 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()
}

這些程式碼負責完成如下工作:

  1. 創建一份 locationManager 對象,用於偵測用戶位置的變化。
  2. 配置 locationManager 對象,例如,將它的 delegate 設為 ViewController,這樣就可以在 ViewController 內部跟蹤用戶位置並監控 region 事件。 同時,將定位精度設定為最佳精度。
  3. 配置 mapView。將 mapView 的 delegate 設置為 ViewController ,這樣我們就可以在 mapView 中畫出一些額外的東西。然後讓 mapView 顯示用戶的當前位置並使地圖隨用戶位置而動,這有助於我們知道用戶是否進入了某個 region 。你也可以通過故事板來設置 MapView 的 delegate 和 showsUserLocation,我們在程式碼中這樣做,無非是讓你更便於理解而已。
  4. 添加測試數據。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。因此我們需要在這時重新對用戶授權狀態進行檢查。

在檢查授權狀態時:

  1. 如果第一次開啟 App,我們還沒有向用戶申請過權限,我們應當立即這樣做。之所以申請的是「Always」權限,是因為要使用CLRegion必須獲得「Always」權限。
  2. 如果用戶不同意授權,我們會告訴用戶,我們的 App 只有在獲得使用定位服務的權限之後才能很好地工作。 這裡我們調用了 showAlert(title: String) 方法,這是一個簡化方法,用 UIAlertController 來顯示一些信息,并帶有一個 「Cancel」 按鈕。後面會經常用到這個方法。
  3. 如果用戶同意了我們的權限申請,我們就可以開始啟動標準的位置更新服務了。

然後運行 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() 方法中的語句。

  1. 首先要檢查用戶設備當前是否支持 region 範圍監控。如果用戶禁止了位置服務,關閉了後台App 刷新,或者設備處於飛行模式,isMonitoringAvailableForClass 方法將返回 false。
  2. 爲了演示,我們創建了一個虛擬的餐廳位置。在我們的例子中這樣做是 OK 的,但在真實項目中,你應當專門為這個對象創建一個單獨的類。
  3. 然後創建一個 region,以便 App 對其進行監控。我們用餐廳的名字作為 region 的唯一標識,這是因為只有這樣我們才能識別用戶進入了哪個 region。這樣做還有一個好處,我們不需要為 CLRegion 創建強引用,而是將 region 的 identifier 保存起來就可以了。
  4. 爲了好看,我們在 region 的中央加了一顆大頭釘。
  5. 爲了好看,我們還在地圖上畫了一個圓圈,用於代表 region 所包含的範圍。
  6. 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。回到模擬器,用戶的位置會開始移動。

geotarget-simulate-location

幾秒鐘后,你會看到提示 「enter Lorrenzillo’s」,又過幾秒,有會提示 「exit Lorrenzillo’s」。這樣,我們的 region 監控功能就完成了!

geotarget-demo-app

實現複雜的業務邏輯

在某些 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()
}
  1. 將用戶的進入時間保存到 NSDictionary。
  2. 這裡實現了添加/刪除進入時間。
  3. 在 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)
    }
}
  1. 假設用 10 秒鐘來識別是不是一個有效的顧客。同時,我們還需要一個變量,用於保存那些用戶已經停留了足夠長時間的 region,一旦完成所需工作后,我們再刪除這些 region。
  2. 遍歷所有當前正在監控的 region。
  3. 如果顧客已經停留了足夠長時間,我們向用戶顯示指定信息并將 region 標記為「準備刪除」。
  4. 刪除已經停留夠時間的 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 項目示例代碼。

譯者簡介:楊宏焱,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年開始學習蘋果 iOS 開發,精通 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作,著有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》(合作翻譯)等。

原文Building a Geo Targeting iOS App in Swift


美國貨運業 TruckerPath 平台的 iOS程式開發員,持續學習關於手機和網絡的新知識並加以實踐運用。過去四年發表了超過10隻Apps,當中兩隻更被App Store選為精選推介。請到 LinkedIn 和 GitHub 與 Eugene 聯絡。

blog comments powered by Disqus
Shares
Share This