前言
對(duì)ARKit感興趣的同學(xué),可以訂閱ARKit教程專題
源代碼地址在這里
正文
在前三章中,我們學(xué)習(xí)了如何使用ARKit實(shí)現(xiàn)虛擬廣告牌,該虛擬廣告牌首先是通過掃描矩形顯示出來拖陆,然后是掃描QR碼顯示出來。
廣告牌的第一個(gè)版本使用視圖控制器和附加到ARKit平面材料的視圖來顯示圖像輪播和視頻播放器懊亡。
在第二個(gè)修訂版中依啰,我們使用故事板替換了視圖控制器并添加了Web視圖;這使得在ARKit會(huì)話中顯示網(wǎng)站成為可能。我們還學(xué)習(xí)了切換全屏模式店枣。
在本章的最后一章中速警,我們將學(xué)習(xí)如何使用位置功能,通過在用戶靠近感興趣的地方時(shí)自動(dòng)啟用功能來增強(qiáng)用戶體驗(yàn)鸯两。
具體來說闷旧,我們將學(xué)習(xí)到:
- 獲取用戶的位置。
- 使用地理圍欄來檢測(cè)用戶何時(shí)進(jìn)入或離開特定區(qū)域钧唐。
- 使用信標(biāo)進(jìn)行接近檢測(cè)鸠匀。
開始
確定用戶位置
雖然我們可能沒有使用它,但我們可能聽說過Core Location逾柿,用于處理設(shè)備位置和信標(biāo)的iOS框架缀棍。我們將在本項(xiàng)目中實(shí)現(xiàn)它。
啟用核心位置
要使用Core Location并遵守用戶權(quán)限要求机错,我們需要向Info.plist文件添加一些Key爬范。
使用Core Location有兩種方法:
- when in use: Core Location僅在應(yīng)用程序位于前臺(tái)時(shí)才向應(yīng)用程序發(fā)送位置更新。
- always: Core Location隨時(shí)向應(yīng)用程序發(fā)送位置更新弱匪,無論應(yīng)用程序是在前臺(tái)還是后臺(tái)青瀑。
在本項(xiàng)目中,我們需要使用always萧诫。
在iOS 11中已經(jīng)發(fā)生了一些變化斥难。在iOS 10及更早版本中,要求always模式帘饶,我們需要將NSLocationAlwaysUsageDescription鍵添加到Info.plist哑诊,其值應(yīng)包含提示用戶讓用戶決定是否同意。
另一方面及刻,在iOS 11中镀裤,我們需要包含兩個(gè)Key竞阐,要求始終和何時(shí)使用權(quán)限。
我們首先按照下圖作如下操作:
文件以源代碼模式打開暑劝。在文件中看到的第一個(gè)<dict>標(biāo)簽后立即轉(zhuǎn)到第5行骆莹,然后插入以下兩行:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Chapter13 requires access to your phones location to notify when you enter a geofence containing ads</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Chapter13 requires access to your phones location to notify when you enter a geofence containing ads</string>
當(dāng)iOS要求訪問設(shè)備位置的權(quán)限時(shí),這些行指定提示給用戶的文本担猛。
注意:XML代碼已經(jīng)過格式化幕垦,通過在兩個(gè)<string> </ string>標(biāo)記中包含的文本中添加幾個(gè)換行符來提高可讀性。當(dāng)文本顯示給用戶時(shí)會(huì)呈現(xiàn)這些換行符傅联,因此在編寫代碼時(shí)忽略它們會(huì)更好先改,如果使用復(fù)制和粘貼則刪除。
在Info.plist中正確設(shè)置密鑰非常重要纺且。如果Core Location沒有找到它所期望的密鑰盏道,那么它就不會(huì)告訴你它們已經(jīng)丟失了。雖然它會(huì)向Xcode控制臺(tái)發(fā)出警告载碌,但它會(huì)默默地拒絕執(zhí)行任何與位置相關(guān)的活動(dòng)猜嘱。
打開LocationManager.swift;這實(shí)現(xiàn)了LocationManager類。我們將在本章的過程中添加更多功能嫁艇。但是現(xiàn)在朗伶,在初始化程序之后添加此方法:
func initialize() {
// 1
locationManager.delegate = self
// 2
locationManager.activityType = .otherNavigation
// Geofencing requires always authorization
// 3
locationManager.requestAlwaysAuthorization()
// 4
locationManager.startUpdatingLocation()
}
上面代碼作用如下:
- 1: LocationManager是Core Location的CLLocationManager類的實(shí)例。該類公開了一個(gè)根據(jù)該應(yīng)用的需求量身定制的界面步咪。
- 2: Core Location需要知道位置跟蹤的目的论皆,以便更好地滿足應(yīng)用程序的需求。像如下應(yīng)用場(chǎng)景:automotive navigation猾漫,?tness点晴,other navigation和other。你在這個(gè)應(yīng)用程序中使用otherNavigation悯周。
- 3: 這要求用戶獲得永久授權(quán)的許可粒督。如果未授予或拒絕授權(quán),則授權(quán)過程通過委托異步執(zhí)行禽翼,因此下一行不會(huì)產(chǎn)生任何影響屠橄。
- 4: 此行通過指示Core Location發(fā)送位置更新來啟動(dòng)跟蹤過程,除非權(quán)限被拒絕闰挡。
現(xiàn)在我們需要添加下面的方法:
private var locationManager = LocationManager()
找到viewDidLoad()方法锐墙。在將場(chǎng)景設(shè)置為sceneView之后,添加以下代碼行:
// Initialize core location
locationManager.initialize()
locationManager.delegate = self
這將調(diào)用初始化方法并設(shè)置位置管理器委托长酗。
在文件的最后溪北,我們添加一個(gè)extension:
// MARK: - LocationManagerDelegate
extension ViewController: LocationManagerDelegate {
// MARK: Location
func locationManager(_ locationManager: LocationManager, didEnterRegionId regionId: String) {
}
func locationManager(_ locationManager: LocationManager, didExitRegionId regionId: String) {
}
// MARK: Beacons
func locationManager(_ locationManager: LocationManager, didRangeBeacon beacon: CLBeacon) {
}
func locationManager(_ locationManager: LocationManager, didLeaveBeacon beacon: CLBeacon) {
}
}
LocationManagerDelegate是一個(gè)類似于CLLocationManager委托的自定義委托協(xié)議。我們可以將其視為CLLocationManagerDelegate的包裝器,它在LocationManager類中使用刻盐。
實(shí)際上掏膏,LocationManager將實(shí)現(xiàn)CLLocationManagerDelegate中定義的總共九種方法劳翰,其中四種將通過自定義LocationManagerDelegate轉(zhuǎn)發(fā)到ViewController敦锌。
現(xiàn)在,我們需要在采用CLLocationManagerDelegate的擴(kuò)展中的LocationManager中實(shí)現(xiàn)兩個(gè)委托方法佳簸。這是第一個(gè):
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print("Authorization status changed to: \(status)")
}
當(dāng)授權(quán)狀態(tài)從默認(rèn)的notDetermined更改為任何其他可能的值時(shí)乙墙,將調(diào)用此方法:restricted,denied生均,authorizedAlways和authorizedWhenInUse听想。
第二個(gè):
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager failed with error: " + "\(error.localizedDescription)"
}
如果在嘗試檢索位置時(shí)發(fā)生錯(cuò)誤,則會(huì)調(diào)用此方法马胧。實(shí)現(xiàn)此方法很重要汉买,或者Core Location在嘗試使用位置服務(wù)時(shí)會(huì)拋出異常。
添加這兩個(gè)委托方法后佩脊,構(gòu)建并運(yùn)行應(yīng)用程序蛙粘。核心位置將要求我們授權(quán)該應(yīng)用訪問我們的位置。
標(biāo)題下方的描述是我們之前添加到Info.plist的文本威彰。請(qǐng)務(wù)必選擇Always Allow出牧。
地理圍欄
現(xiàn)在已設(shè)置Core Location,我們可以開始跟蹤設(shè)備位置歇盼。不關(guān)心實(shí)際的設(shè)備位置舔痕。當(dāng)我們?cè)谌虑敖榻B時(shí),你會(huì)發(fā)現(xiàn)它應(yīng)該能夠在用戶離目標(biāo)一定距離時(shí)顯示出來豹缀。此功能稱為地理圍欄伯复,它包括監(jiān)視某個(gè)位置周圍的區(qū)域,特別是當(dāng)設(shè)備跨越該區(qū)域的邊界時(shí)邢笙。
項(xiàng)目中有一個(gè)Geo Fencing的按鈕啸如。其目的是啟動(dòng)或停止區(qū)域監(jiān)測(cè)。
開始區(qū)域監(jiān)測(cè)
在ViewController.swift中鸣剪,找到toggleLocationTracking();這已經(jīng)連接到Geo Fencing按鈕的tap事件组底。該方法包含注釋的打印語句;暫時(shí)忽略它們。
在方法的開頭添加此代碼:
isLocationTrackingActive = !isLocationTrackingActive
if isLocationTrackingActive {
} else {
}
isLocationTrackingActive是指示位置跟蹤當(dāng)前是否處于活動(dòng)狀態(tài)的標(biāo)記筐骇。你將填充if /else語句债鸡。
將print語句移動(dòng)到if分支中并取消注釋。它們用于向控制臺(tái)顯示警告铛纬,提醒我們RMK位置是硬編碼的厌均。
仍在if分支內(nèi)部,在print語句之前告唆,添加以下代碼:
// 1 self.toggleLocationTrackingButton.setImage(#imageLiteral(resourceName: "arKit-fence-on"), for: .normal)
// 2
let rmkLocation = Constants.razewareMobileKioskLocation
// 3
let rmkCoordinates = rmkLocation.location
當(dāng)用戶點(diǎn)擊地理圍欄按鈕棺弊,并且正在激活位置跟蹤時(shí):
- 1: 我們更新按鈕圖像以指示它已打開晶密。
- 2: 我們可以讀取RMK所在目標(biāo)點(diǎn)的位置。
- 3: 我們閱讀該位置的坐標(biāo)模她。
RMK位置使用包含名稱和位置的自定義結(jié)構(gòu)在Constants.swift中進(jìn)行靜態(tài)硬編碼稻艰。
現(xiàn)在你有了目標(biāo)位置。下一步是開始監(jiān)視該目標(biāo)周圍的區(qū)域侈净。在print語句之后尊勿,添加以下代碼:
do {
try locationManager.startMonitoring(
location: rmkCoordinates,
radius: Constants.geofencingRadius,
identifier: Constants.razewareMobileKioskIdentifier)
} catch (let error as LocationManager.GeofencingError) {
print( "An error occurred while monitoring a region: \(error)")
} catch (let error) {
print( "Tracking location error: \(error.localizedDescription)")
}
startMonitoring()開始監(jiān)視半徑為Constants.geofencingRadius米的rmlCoordinates位置周圍的區(qū)域,當(dāng)前設(shè)置為300畜侦,并在Constants.razewareMobileKioskIdentifier中指定一個(gè)字符串標(biāo)識(shí)符元扔。
監(jiān)控一個(gè)地區(qū)
監(jiān)視啟動(dòng)在LocationManager類中完成。打開LocationManager.swift旋膳。在initialize方法之后澎语,添加以下內(nèi)容:
func startMonitoring(location: CLLocationCoordinate2D, radius: Double, identifier: String) throws {
// 1
guard CLLocationManager.isMonitoringAvailable( for: CLCircularRegion.self)
else { throw GeofencingError.notSupported }
guard CLLocationManager.authorizationStatus() == .authorizedAlways
else { throw GeofencingError.notAuthorized }
trackedLocation = location
// 2
let region = CLCircularRegion(center: location, radius: radius, identifier: identifier)
// 3
region.notifyOnEntry = true
region.notifyOnExit = true
// 4
locationManager.startMonitoring(for: region)
// 5
// Delay state request by 1 second due to an old bug
// http://www.openradar.me/16986842
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(1), qos: .default, flags: []) {
self.locationManager.requestState(for: region)
}
}
上面代碼作用如下:
-
1:我們首先需要檢查:
??a: 可以使用區(qū)域監(jiān)視,因?yàn)槟承┰O(shè)備可能不支持它验懊。
??b: 用戶授權(quán)始終開啟位置跟蹤擅羞。 - 2: 這是我們要監(jiān)控的區(qū)域。它以位置為中心鲁森,并具有由radius參數(shù)指定的半徑祟滴。
- 3: 我們可以啟用這兩個(gè)文件來指示我們有興趣在設(shè)備進(jìn)入和退出受監(jiān)控區(qū)域時(shí)通知的Core Location。
- 4: 這開始了區(qū)域監(jiān)測(cè)歌溉。
- 5: 在這里垄懂,我們可以請(qǐng)求通過Core Location委托異步傳送的區(qū)域的立即狀態(tài)。
注意:區(qū)域是共享的應(yīng)用程序資源痛垛,這意味著如果我們有多個(gè)CLLocationManager實(shí)例草慧,則每個(gè)實(shí)例都會(huì)在進(jìn)入或退出任何受監(jiān)視區(qū)域時(shí)收到通知。
此外匙头,對(duì)狀態(tài)的請(qǐng)求包含在延遲調(diào)用中漫谷,因?yàn)榕f的錯(cuò)誤導(dǎo)致該方法在同步調(diào)用時(shí)不執(zhí)行任何操作。
現(xiàn)在蹂析,我們需要提供在設(shè)備進(jìn)入和退出受監(jiān)控區(qū)域時(shí)由Core Location調(diào)用的兩個(gè)委托方法舔示。
在LocationManager中,轉(zhuǎn)到底部电抚。在最終擴(kuò)展中惕稻,添加進(jìn)入?yún)^(qū)域時(shí)調(diào)用的委托方法:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let region = region as? CLBeaconRegion { } else {
print("Entered in region: \(region)")
delegate?.locationManager(
self, didEnterRegionId: region.identifier) }
}
我們現(xiàn)在可以忽略if分支;它將在以后使用。 else分支是區(qū)域輸入事件被轉(zhuǎn)發(fā)到LocationManagerDelegate中定義的委托作為locationManager(_:didEnterRegionId :)并在ViewController中實(shí)現(xiàn)蝙叛。雖然它現(xiàn)在已經(jīng)空了俺祠,但我們很快就會(huì)照顧到它。我們將區(qū)域標(biāo)識(shí)符傳遞給該方法。
在didEnterRegion之后蜘渣,添加其鏡面反射didExitRegion方法:
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("Left region \(region)")
delegate?.locationManager( self, didExitRegionId: region.identifier)
}
這類似于通過調(diào)用locationManager(_:didExitRegionId :)來通知何時(shí)設(shè)備離開受監(jiān)視區(qū)域淌铐。
注意:我們可以在一臺(tái)設(shè)備上監(jiān)控多達(dá)20個(gè)區(qū)域。
當(dāng)我們對(duì)requestState做一個(gè)延遲調(diào)用(for :)時(shí)蔫缸,請(qǐng)記住startMonitoring(location:radius:identifier :)嗎腿准?結(jié)果通過另一個(gè)CLLocationManagerDelegate方法異步傳遞。在locationManager(_:didFailWithError :)之后添加它:
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
switch state {
// 1
case .inside:
locationManager(manager, didEnterRegion: region)
// 2
case .outside, .unknown:
break
}
}
上面代碼作用如下:
- 1: 在這里捂龄,我們有興趣了解設(shè)備何時(shí)已經(jīng)位于受監(jiān)控區(qū)域內(nèi)释涛,在這種情況下加叁,我們將委托給locationManager(_:didEnterRegion :)倦沧,就好像它是從受監(jiān)控區(qū)域外部到內(nèi)部的正常過渡一樣。
- 2: 在這里它匕,你說你對(duì)其他案件并不感興趣展融。
你在開始監(jiān)控之后立即調(diào)用requestState(for :)的原因是我們想知道 - 在那個(gè)特定的時(shí)刻 - 如果設(shè)備已經(jīng)在被監(jiān)控區(qū)域內(nèi)。區(qū)域監(jiān)視僅在狀態(tài)發(fā)生變化時(shí)觸發(fā)事件豫柬,即從外部轉(zhuǎn)換到內(nèi)部告希,反之亦然。如果我們已經(jīng)在室內(nèi)或室外烧给,它將不會(huì)觸發(fā)任何事件燕偶。
如果設(shè)備已在內(nèi)部,那么我們需要對(duì)信標(biāo)執(zhí)行某些操作础嫡。稍后會(huì)詳細(xì)介紹指么!
還有一個(gè)委托方法可以實(shí)現(xiàn),至少在調(diào)試方面是有用的榴鼎。在locationManager之后添加它(_:didDetermineState:for :):
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
print("Geofencing monitoring failed for region " + "\(String(describing: region?.identifier))," + "error: \(error.localizedDescription)")
}
當(dāng)startMonitoring(for :)失敗時(shí)調(diào)用此方法伯诬。將錯(cuò)誤消息打印到控制臺(tái)可能有助于我們了解問題何時(shí)發(fā)生以及發(fā)生的原因。
對(duì)區(qū)域變化做出反應(yīng)
當(dāng)設(shè)備穿過受監(jiān)控區(qū)域時(shí)巫财,Core Location會(huì)通知LocationManager盗似。反過來,LocationManager使用LocationManagerDelegate協(xié)議的兩種方法將通知轉(zhuǎn)發(fā)給ViewController平项。我們將使用這兩個(gè)通知來玩信標(biāo)赫舒。但是,我們可以在用戶進(jìn)入某個(gè)區(qū)域時(shí)提醒用戶闽瓢。
在ViewController.swift中接癌,滾動(dòng)到locationManager(_:didEnterRegionId :)并添加以下代碼:
// Notify the user that he’s entered the geofenced zone
let distance = String(format: "%0.0f", Constants.geofencingRadius)
let message = "\(regionId) is less than \(distance) meters. " + "Come say hi and interact with our e-billboard"
let title = regionId
showAlert(with: "geofencing-notification", title: title, message: message)
這會(huì)提示警報(bào)并通知用戶RMK在附近。
停止區(qū)域監(jiān)測(cè)
當(dāng)我們點(diǎn)擊Geo Fencing按鈕時(shí)鸳粉,到目前為止編寫的代碼負(fù)責(zé)啟動(dòng)區(qū)域監(jiān)視并通知設(shè)備何時(shí)穿過區(qū)域扔涧。要關(guān)閉圓圈,我們必須在相反的方向上工作:停止區(qū)域監(jiān)控。
轉(zhuǎn)到toggleLocationTracking()點(diǎn)擊處理程序枯夜。早些時(shí)候弯汰,如果未實(shí)現(xiàn),你就離開了最外層的else分支湖雹。使用以下代碼填充該空白區(qū)域:
// 1
self.toggleLocationTrackingButton.setImage( #imageLiteral(resourceName: "arKit-fence-off"), for: .normal)
// 2
locationManager.stopMonitoringRegions()
上面代碼作用如下:
- 1: 恢復(fù)地理圍欄按鈕的關(guān)閉圖標(biāo)咏闪。
- 2: 停止區(qū)域監(jiān)控。
但是摔吏,該停止方法尚不存在鸽嫂。打開LocationManager.swift并在startMonitoring()之后添加它:
func stopMonitoringRegions() {
trackedLocation = nil
for region in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: region) }
}
我們無需跟蹤已開始監(jiān)控的區(qū)域;它們列在CLLocationManager的monitoredRegions屬性中 - 我們只需遍歷它們并停止每個(gè)屬性。
距離計(jì)算
到目前為止征讲,我們已經(jīng)忽略了獲取實(shí)際的設(shè)備位置据某。但是,這是通過實(shí)現(xiàn)單個(gè)委托方法完成的诗箍。
打開LocationManager并在擴(kuò)展的開頭添加此方法以實(shí)現(xiàn)CLLocationManagerDelegate:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 1
guard let currentLocation = locations.last else { return }
// 2
guard let trackedLocation = trackedLocation else { return }
let location = CLLocation(latitude: trackedLocation.latitude, longitude: trackedLocation.longitude)
// 3
let distance = currentLocation.distance(from: location)
print("Distance: \(distance)")
}
上面代碼作用如下:
1: 委托方法接收一個(gè)數(shù)組癣籽,該數(shù)組包含自上次調(diào)用以來的位置列表,按從最舊到最新的時(shí)間戳排序滤祖。你拿最后一個(gè);即最近的筷狼。
2: 我們獲得包含目標(biāo)位置的CLLocation - RMK。由于它們存儲(chǔ)為CLLocationCoordinate2D結(jié)構(gòu)匠童,因此我們將轉(zhuǎn)換為CLLocation的實(shí)例埂材。
3: 我們計(jì)算兩個(gè)位置之間的距離并將其打印到Xcode控制臺(tái)。
測(cè)試
我們寫了很多代碼√狼螅現(xiàn)在是時(shí)候運(yùn)行應(yīng)用程序來查看它的全部動(dòng)態(tài)俏险。為方便起見,選擇模擬器首昔,然后構(gòu)建并運(yùn)行應(yīng)用程序寡喝。然后,點(diǎn)擊Geo Fencing按鈕勒奇。
除了按鈕圖標(biāo)改變顏色预鬓,顯然沒有任何反應(yīng)。如果尚未顯示赊颠,請(qǐng)打開Xcode控制臺(tái)格二。在執(zhí)行toggleLocationTracking()時(shí)遇到的打印語句會(huì)打印出警告:
WARNING: ensure that the Razeware Mobile Kiosk is at a location near you, otherwise geofencing and beacon detection won't work The current location is: Pisa (43.7153187, 10.4019739)
如前所述,RMK位置已經(jīng)靜態(tài)地設(shè)置在比薩斜塔附近竣蹦。我們可能想知道:我是否必須去那里測(cè)試應(yīng)用程序顶猜?當(dāng)然,請(qǐng)隨意去痘括,但是在這個(gè)項(xiàng)目中測(cè)試地理圍欄的方法比較簡單一些长窄。
在Xcode中滔吠,打開位于res組中的TestLocations.gpx。它應(yīng)該如下所示:
<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<wpt lat="43.72281" lon="10.3958585">
<name>Lungarno, Pisa</name>
<time>2010-01-01T00:00:00Z</time>
</wpt>
<wpt lat="43.7153187" lon="10.4019739">
<name>Piazza dei Miracoli, Pisa</name>
<time>2010-01-01T00:00:30Z</time>
</wpt>
<wpt lat="43.72281" lon="10.3958585">
<name>Lungarno, Pisa</name>
<time>2010-01-01T00:01:00Z</time>
</wpt>
</gpx>
該文件包含由三個(gè)航點(diǎn)定義的路線:起點(diǎn)挠日,目的地和返回起點(diǎn)疮绷。選擇初始位置距離目的地約1,000米的距離,即RMK位置嚣潜。
除了坐標(biāo)和名稱冬骚,每個(gè)航路點(diǎn)也有時(shí)間。絕對(duì)值的重要性并不重要懂算,但相對(duì)差異表示從一個(gè)位置到下一個(gè)位置所需的時(shí)間只冻。如果你仔細(xì)觀察,你會(huì)注意到一個(gè)航點(diǎn)到下一個(gè)航點(diǎn)的差異為30秒计技。
在Xcode中喜德,我們可以通過調(diào)試區(qū)域中的“模擬位置”按鈕選擇此文件。這使得Xcode模擬.gpx文件中定義的路由酸役。
如果你看一下Xcode中的控制臺(tái)面板住诸,你應(yīng)該開始看到距離更新,如下所示:
Distance: 935.436896038748
Distance: 903.575340351579
Distance: 871.696596559219
Distance: 839.800076468077
Distance: 807.885154208941
Distance: 775.951162495185
Distance: 743.997388360152
Distance: 712.023068265481
每次Core Location為剛剛實(shí)現(xiàn)的方法locationManager(_:didUpdateLocations)提供新位置時(shí)涣澡,都會(huì)更新。
一旦距離低于我們正在監(jiān)視的區(qū)域的半徑丧诺,我們應(yīng)該看到如下日志:
Entered in region: CLCircularRegion (identifier:'The Razeware Mobile Kiosk', center:<+43.71531870,+10.40197390>, radius:300.00m)
這證實(shí)了設(shè)備的模擬位置已進(jìn)入地理圍欄區(qū)域入桂。如果稍等一下,當(dāng)設(shè)備退出該區(qū)域時(shí)驳阎,我們會(huì)看到類似的消息抗愁。
Left region CLCircularRegion (identifier:'The Razeware Mobile Kiosk', center:<+43.71531870,+10.40197390>, radius:300.00m)
作為旁注,如果我們想使用iPhone和真實(shí)位置呵晚,我們可以這樣做蜘腌。只有一個(gè)建議:我們可能希望查看數(shù)據(jù)/ Constants.swift并在其中設(shè)置新位置,而不是要求Razeware移動(dòng)信息亭移動(dòng)到我們當(dāng)前位置附近饵隙。
信標(biāo)
既然我們可以檢測(cè)到設(shè)備何時(shí)進(jìn)入或退出某個(gè)區(qū)域撮珠,那么下一步是什么?我們可能已經(jīng)注意到在進(jìn)入時(shí)會(huì)向用戶提示警報(bào)金矛,但僅此一點(diǎn)不應(yīng)該證明這么多工作是正確的芯急。
這是信標(biāo)發(fā)揮作用的地方。但他們是什么驶俊?信標(biāo)是一小塊硬件娶耍,可定期通過藍(lán)牙LE(低能耗)發(fā)出信號(hào)。在最簡單的化身中饼酿,信號(hào)由三個(gè)靜態(tài)數(shù)據(jù)字段組成:Proximity UUID榕酒,Major和Minor胚膊。
就這么簡單。但這對(duì)于接近檢測(cè)也很有用:我們的手機(jī)可以攔截信號(hào)想鹰,識(shí)別信標(biāo)并觸發(fā)向注冊(cè)信標(biāo)的應(yīng)用程序發(fā)出通知澜掩。
注意:如上所述,Proximity杖挣,Major和Minor是靜態(tài)數(shù)據(jù);然而肩榕,它們可以(并且應(yīng)該)在購買信標(biāo)后進(jìn)行更改。更新數(shù)據(jù)的過程取決于供應(yīng)商惩妇。
數(shù)據(jù)可用于任何有助于特定目的的方式;然而株汉,一個(gè)典型的模式是使用鄰近度來識(shí)別我們的公司或應(yīng)用程序,一個(gè)部分的主要部分歌殃,如房間乔妈,建筑物,商店和未成年人氓皱,以區(qū)分同一部分中的信標(biāo)路召。
信標(biāo)是低能耗設(shè)備;出于這個(gè)原因,他們可以在一個(gè)電池上運(yùn)行幾年波材,不間斷地每天24小時(shí)股淡,365天 - 有時(shí)甚至366天 - 廣播他們的數(shù)據(jù)。由于能量低廷区,它們的信號(hào)范圍有限唯灵。因此,它們通常用于近距離檢測(cè)隙轻。
檢測(cè)廣播信號(hào)的設(shè)備可以用一致的近似來確定它與信標(biāo)的距離埠帕。距離分為三個(gè)不同的范圍:
- Far:超過10米遠(yuǎn)。
- Near: 在幾米之內(nèi)玖绿。
- Immediate: 幾厘米遠(yuǎn)敛瓷。
檢測(cè)信標(biāo)
在本項(xiàng)目中,我們將使用信標(biāo)來檢測(cè)何時(shí)接近RMK斑匪,并觸發(fā)自動(dòng)QR碼檢測(cè)呐籽。在iOS中,信標(biāo)由Core Location處理秤标,這是一個(gè)我們應(yīng)該已經(jīng)相當(dāng)熟悉的框架绝淡。
注意:要調(diào)試和測(cè)試信標(biāo), 我們需要一個(gè)信標(biāo)苍姜±谓停可以使用備用iPhone作為信標(biāo)或真正的信標(biāo); App Store中有一些應(yīng)用程序可讓我們將iPhone用作燈塔。我們還需要另一部iPhone來測(cè)試應(yīng)用程序衙猪。
信標(biāo)檢測(cè)不是發(fā)現(xiàn)過程馍乙。當(dāng)任何信標(biāo)出現(xiàn)在雷達(dá)上時(shí)布近,我們不會(huì)要求核心位置通知。
相反丝格,我們要求Core Location告知我們何時(shí)檢測(cè)到特定信標(biāo)撑瞧,由其接近,主要和次要標(biāo)識(shí)显蝌,或者我們也可以在相同的鄰近UUID或接近度和主要信號(hào)下請(qǐng)求一系列信標(biāo)预伺。關(guān)鍵是我們要求Core Location監(jiān)控你已經(jīng)知道的信標(biāo)。
核心位置定義CLBeaconRegion類曼尊,用于指定要監(jiān)視的信標(biāo)酬诀。只有一個(gè)信標(biāo)的區(qū)域已經(jīng)在data/Constants.swift中定義,打包成一個(gè)名為razeadBeacons的數(shù)組骆撇。
我們應(yīng)該將beacon標(biāo)識(shí)替換為我們自己的數(shù)據(jù)瞒御。最好不要重復(fù)使用我們?cè)诖a中找到的相同身份,因此至少應(yīng)該為我們的信標(biāo)生成一個(gè)新的鄰近UUID神郊。
管理信標(biāo)監(jiān)控
打開LocationManager.swift肴裙。在使用MARK: - Beacons標(biāo)識(shí)的擴(kuò)展中,添加以下兩種方法:
// 1
func startMonitoring(beacons: [CLBeaconRegion]) {
for beacon in beacons {
startMonitoring(beacon: beacon)
}
}
// 2
func startMonitoring(beacon: CLBeaconRegion) {
guard CLLocationManager.isRangingAvailable() else {
print("[ERROR] Beacon ranging is not available")
return
}
locationManager.startMonitoring(for: beacon)
}
上面的代碼作用如下:
- 1: 注冊(cè)信標(biāo)區(qū)域陣列涌乳。這循環(huán)遍歷數(shù)組并將注冊(cè)委托給單區(qū)域方法蜻懦。
- 2: 在檢查該應(yīng)用程序運(yùn)行的設(shè)備上是否有該功能后,注冊(cè)單個(gè)信標(biāo)區(qū)域爷怀。
在此處阻肩,添加用于停止監(jiān)視的方法:
func stopMonitoring(beacons: [CLBeaconRegion]) {
for beacon in beacons {
stopMonitoring(beacon: beacon)
}
}
func stopMonitoring(beacon: CLBeaconRegion) {
locationManager.stopMonitoring(for: beacon)
}
我們必須在ViewController.swift中調(diào)用這些方法來分別啟動(dòng)和停止信標(biāo)監(jiān)視。我們將在下一部分中執(zhí)行此操作运授。
當(dāng)檢測(cè)信標(biāo)時(shí),核心位置語言被稱為測(cè)距乔煞,實(shí)際上有兩個(gè)不同的階段:
- Entering the region: 當(dāng)檢測(cè)到屬于該區(qū)域ID的第一個(gè)信標(biāo)時(shí)發(fā)生吁朦。
- Ranging a single beacon: 在單個(gè)信標(biāo)范圍內(nèi)發(fā)生。
第一種情況是通過之前實(shí)現(xiàn)的locationManager(_:didEnterRegion :)委托方法處理的渡贾。如果還記得逗宜,你把if分支留空了。現(xiàn)在用以下代碼填寫:
print("Entered in beacon region: \(region)")
locationManager.startRangingBeacons(in: region)
這會(huì)將消息打印到控制臺(tái)并啟動(dòng)屬于我們輸入的區(qū)域的測(cè)距信標(biāo)空骚。
現(xiàn)在纺讲,當(dāng)信標(biāo)在范圍內(nèi)或發(fā)生錯(cuò)誤時(shí),剩下的就是通知囤屹。在最后一個(gè)LocationManager擴(kuò)展的末尾熬甚,添加以下兩個(gè)委托方法:
// MARK: Beacons
// 1
func locationManager(_ manager: CLLocationManager,didRangeBeacons beacons: [CLBeacon],in region: CLBeaconRegion) {
for beacon in beacons {
delegate?.locationManager(self, didRangeBeacon: beacon)
}
}
// 2
func locationManager(_ manager: CLLocationManager,rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) {
print("Beacon ranging failed for region \(region) " + "with error: \(error.localizedDescription)")
}
上面代碼如下:
- 1: 當(dāng)信標(biāo)數(shù)組中的一個(gè)或多個(gè)信標(biāo)被測(cè)距時(shí),調(diào)用此方法肋坚。每個(gè)信標(biāo)都通過LocationManagerDelegate轉(zhuǎn)發(fā)到ViewController乡括。
- 2: 如果信標(biāo)范圍失敗肃廓,則調(diào)用此方法。要保持一致诲泌,請(qǐng)向控制臺(tái)輸出錯(cuò)誤消息盲赊。
最后一個(gè)修復(fù)的細(xì)節(jié):在stopMonitoring(beacon :)中,我們調(diào)用了stopMonitoring(for :)敷扫。最好還停止屬于該區(qū)域的測(cè)距信標(biāo)哀蘑,因此在方法開頭添加此行代碼,就在stopMonitoring之前:
locationManager.stopRangingBeacons(in: beacon)
手動(dòng)停止區(qū)域和信標(biāo)監(jiān)控
還記得UI中的按鈕以啟動(dòng)地理圍欄嗎葵第?我們可以使用相同的按鈕來停止它 - 這已經(jīng)在其處理程序方法中完成绘迁。但是,需要進(jìn)行一些清理羹幸。
在ViewController.swift中脊髓,滾動(dòng)到toggleLocationTracking()。在最頂層的if語句的else分支中栅受,在locationManager.stopMonitoringRegions()之后将硝,添加:
// 1
locationManager.stopMonitoring(beacons: Constants.razeadBeacons)
// 2
beaconStatusImage.isHidden = true
beaconStatusLabel.isHidden = true
上面的代碼作用如下:
- 1: 如果激活,則停止信標(biāo)監(jiān)視屏镊。
- 2: 隱藏信標(biāo)狀態(tài)圖標(biāo)和標(biāo)簽依疼。
消耗信標(biāo)檢測(cè)
現(xiàn)在,信標(biāo)檢測(cè)是一件事而芥,我們可以觸發(fā)自動(dòng)QR掃描模式律罢。目前,在ViewController中棍丐,當(dāng)用戶點(diǎn)擊屏幕時(shí)误辑,會(huì)運(yùn)行QR代碼掃描嘗試。是時(shí)候修改這種行為了歌逢。這是你需要做的:
- 保留點(diǎn)擊以關(guān)閉視頻播放器功能巾钉。
- 將方法的其余部分(負(fù)責(zé)QR碼檢測(cè))移動(dòng)到新的scanQRCode()方法中。
在touchesBegan(_:with)開頭的if語句的右括號(hào)之后秘案,添加以下兩行:
// 1
}
// 2
private func scanQRCode() {
上述代碼作用如下:
- 1: 關(guān)閉touchesBegun方法砰苍。
- 2: 開始一個(gè)新的scanQRCode方法,它“繼承”以前屬于touchesBegun的代碼阱高。
編譯項(xiàng)目以確保我們?cè)谡_的位置輸入代碼赚导。
響應(yīng)信標(biāo)通知
在響應(yīng)信標(biāo)通知之前,我們應(yīng)確保已開始測(cè)距赤惊。無需檢查代碼 - 我們尚未完成此操作吼旧。設(shè)備進(jìn)入受監(jiān)控區(qū)域后,本應(yīng)用應(yīng)啟動(dòng)測(cè)距信標(biāo)荐捻,這將在ViewController中調(diào)用locationManager(_:didEnterRegionId :)委托方法時(shí)發(fā)生黍少。
在該方法的末尾添加此行:
locationManager.startMonitoring( beacons: Constants.razeadBeacons)
要執(zhí)行自動(dòng)QR掃描寡夹,我們需要重復(fù)調(diào)??用之前創(chuàng)建的scanQRCode()方法,并在后續(xù)調(diào)用之間保持一定的時(shí)間間隔厂置。這個(gè)要求聞起來像一個(gè)計(jì)時(shí)器 - 這實(shí)際上就是你要使用的菩掏。 ViewController已經(jīng)包含了處理計(jì)時(shí)器的大部分代碼。我們需要將其與信標(biāo)范圍連接起來昵济。
找到locationManager(_:didRangeBeacon :)委托方法;當(dāng)新信標(biāo)在范圍內(nèi)時(shí)調(diào)用它智绸。添加此實(shí)現(xiàn):
// 1
beaconStatusImage.isHidden = false
beaconStatusLabel.isHidden = false
// 2
switch beacon.proximity {
case .immediate:
beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-1")
case .near:
beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-2")
case .far:
beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-3")
case .unknown:
beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-4") }
// Start auto scan, but only if the app is in the foreground
// and there is no active billboard
// 3
if UIApplication.shared.applicationState == .active && (billboard == nil || billboard?.hasBillboardNode == false) {
// 4
startAutoscanTimer()
}
上面的代碼作用如下:
- 1: 默認(rèn)情況下隱藏有一個(gè)圖標(biāo)和標(biāo)簽适篙,用于報(bào)告信標(biāo)測(cè)距狀態(tài)奕谭。因?yàn)樵谶@種委托方法中,信標(biāo)已被范圍化纹蝴,使它們可見海铆。
- 2: 為圖標(biāo)選擇合適的圖像迹恐。
- 3: 如果應(yīng)用程序在前臺(tái),并且當(dāng)前沒有顯示廣告牌......
- 4: ...然后啟動(dòng)計(jì)時(shí)器卧斟,啟用自動(dòng)QR檢測(cè)殴边。
相反,當(dāng)燈塔離開時(shí)珍语,我們需要做相反的事情锤岸。將以下實(shí)現(xiàn)添加到locationManager(_:didLeaveBeacon):
// 1
stopAutoscanTimer()
// 2
beaconStatusImage.isHidden = true
beaconStatusLabel.isHidden = true
上面代碼作用如下:
- 1: 這會(huì)停止計(jì)時(shí)器,禁用自動(dòng)QR掃描板乙。
- 2: 這會(huì)隱藏信標(biāo)狀態(tài)圖標(biāo)和標(biāo)簽是偷。
當(dāng)用戶退出該區(qū)域時(shí),我們希望執(zhí)行相同的操作募逞。將相同的代碼添加到locationManager(_:didExitRegionId :):
stopAutoscanTimer()
beaconStatusImage.isHidden = true
beaconStatusLabel.isHidden = true
最后一個(gè)位置應(yīng)該取消定時(shí)器蛋铆,當(dāng)我們手動(dòng)點(diǎn)擊Geo Fencing按鈕停止檢測(cè)時(shí)。這由toggleLocationTracking()方法處理放接。找到它戒职,并在else分支的底部,隱藏圖標(biāo)和標(biāo)簽后透乾,添加以下代碼行:
stopAutoscanTimer()
如果計(jì)時(shí)器處于活動(dòng)狀態(tài),則會(huì)停止計(jì)時(shí)
定時(shí)器和QR碼檢測(cè)
當(dāng)計(jì)時(shí)器為紅色時(shí)磕秤,我們想要開始QR碼檢測(cè)嘗試乳乌。找到didFireTimer(timer :)方法,只需添加:
scanQRCode()
測(cè)試
要測(cè)試信標(biāo)市咆,我們需要使用物理設(shè)備汉操。我們可以使用啟動(dòng)項(xiàng)目附帶的.gpx文件來模擬路徑,從而可以測(cè)試設(shè)備何時(shí)進(jìn)入和離開受監(jiān)控區(qū)域蒙兰。
我們還需要一個(gè)信標(biāo)或備用iPhone磷瘤,我們可以在其上安裝將其變?yōu)樾艠?biāo)的應(yīng)用程序芒篷。在App Store中有幾個(gè)這樣的應(yīng)用程序。
準(zhǔn)備好后采缚,運(yùn)行應(yīng)用程序针炉,然后通過Xcode調(diào)試區(qū)域中的Simulate Locations按鈕加載.gpx文件。等待虛擬位置距離RMK 300米范圍內(nèi)扳抽。
如果不想運(yùn)行模擬路線篡帕,那么有兩種選擇:
- 選項(xiàng)#1:手動(dòng)重新配置應(yīng)用以使用我們附近的真實(shí)位置:
1: 我們可以使用Google地圖找到坐標(biāo),這些坐標(biāo)在我們將地圖置于當(dāng)前位置或至少靠近它之后顯示在網(wǎng)址中贸呢。
2: 獲得坐標(biāo)后镰烧,打開Constants.swift,然后將坐標(biāo)替換為實(shí)際坐標(biāo)楞陷。 - 選項(xiàng)#2:通過.gpx文件避免模擬路由怔鳖,但改為使用靜態(tài)虛擬位置進(jìn)行配置:
1: 在Xcode中,找到res組中包含的TestLocations.gpx固蛾。
2: 右鍵單擊它结执,然后選擇在Finder中顯示。
3: 彈出時(shí)從Finder中選擇.gpx文件魏铅,然后復(fù)制并粘貼昌犹,或右鍵單擊文件并選擇復(fù)制。
4: 將重復(fù)的文件重命名為StaticLocation.gpx览芳,或我們選擇的任何其他名稱斜姥。
5: 在Xcode中,右鍵單擊res組沧竟,選擇Add files to Chapter13”铸敏,然后選擇新創(chuàng)建的文件,并單擊 Add 按鈕悟泵。
6: 打開文件并刪除第一個(gè)和最后一個(gè) <wpt> ... </ wpt>標(biāo)簽杈笔。
7: 現(xiàn)在,我們可以在使用Xcode的模擬位置功能時(shí)選擇此文件糕非。這會(huì)將我們的設(shè)備放在RMK**旁邊蒙具。
無論我們選擇哪種方法,當(dāng)運(yùn)行應(yīng)用程序朽肥,并且設(shè)備位于受監(jiān)控區(qū)域內(nèi)時(shí)禁筏,都會(huì)激活信標(biāo)監(jiān)控。
只要受監(jiān)控的信標(biāo)有范圍衡招,就應(yīng)該看到啟用自動(dòng)檢測(cè)按鈕:
這表示正在進(jìn)行自動(dòng)QR掃描篱昔。此外,工具欄中央會(huì)顯示一個(gè)圖標(biāo),通知信標(biāo)在范圍內(nèi):
- 綠色: 非常近
- 橙色: 近
- 紅色: 遠(yuǎn)
- 灰色: 未知
我們可以使用它來手動(dòng)關(guān)閉廣告牌州刽。請(qǐng)注意空执,創(chuàng)建廣告牌時(shí),應(yīng)用會(huì)自動(dòng)停止地理圍欄和自動(dòng)檢測(cè)穗椅。
如果無法對(duì)信標(biāo)進(jìn)行測(cè)距辨绊,請(qǐng)參閱以下列表:
- 驗(yàn)證信標(biāo)是否已打開。
- 確認(rèn)我們的iPhone已啟用藍(lán)牙房待。
- 確認(rèn)我們的iPhone已啟用藍(lán)牙邢羔。驗(yàn)證iPhone是否位于受監(jiān)控區(qū)域內(nèi),即距離Constants.swift文件中指定的位置最多300米桑孩。
- 在Xcode的控制臺(tái)中拜鹤,檢查錯(cuò)誤消息或警告。
- 使用我們的信標(biāo)供應(yīng)商的應(yīng)用程序(如果有)來驗(yàn)證信標(biāo)是否在范圍內(nèi)流椒。
- 驗(yàn)證信標(biāo)接近度敏簿,主要和次要數(shù)據(jù)是否與Constants.swift文件中指定的數(shù)據(jù)匹配。
上一章 | 目錄 | 下一章 |
---|