試著更改下模擬器中的區域,比如從中國改到美國,然后看看會發生什么事。
注意一下,即使把模擬器的區域改為None,也不會報錯,仍然會返回.locationUnknown錯誤代碼,但是你可以忽略它,因為這個報錯并不致命。
小帖士:你也可以在Xcode內調整模擬的地區。如果你的app使用了Core Location,那么你會在調試區域頂部看到一個三角狀的導航圖標,點擊這個圖標就可以切換地區了:
理想情況,除了模擬器意外,你還應該在實際的設備上進行測試,這樣你才可以把握真實情況下會發生什么。
改進返回結果
酷!你已經掌握了如何通過Core Location獲得CLLocation對象,并且提高了app的容錯能力,還有比這更牛掰的嗎?
那么,接下來我們要這樣干:在模擬器中你看到了Core Location不斷的刷新位置信息,即使坐標根本沒有發生改變。這是因為用戶可能會移動,這種情況下GPS坐標就會發生變化。
然而,你要做的并不是導航類app,我們在得到精度匹配的位置信息后,就需要通知location manager停止更新地址信息。
這是非常重要的,因為持續更新位置信息,會非常耗電。我們的app并不需要實時的更新位置信息,所以當獲得精度足夠的位置信息后,就應該停止更新位置信息。
問題在于,精度到什么程度才算是獲得了足夠的精度呢?有個簡單的判斷方法,就是最后一次獲得的位置信息與上一次比較,如果相同,則認為精度已經到達最高了。
改變一下locationManager(didUpdateLocations)方法:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let newLocation = locations.last!
print("didUpdateLocations \(newLocation)")
//1
if newLocation.timestamp.timeIntervalSinceNow < -5 {
return
}
//2
if newLocation.horizontalAccuracy < 0{
return
}
//3
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
//4
lastLocationError = nil
location = newLocation
updateLabels()
}
//5
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
print("*** We're Done!")
stopLocationManager()
}
}
我來為你們一一講解:
注釋1、如果獲得location對象的時間過長,比如說5秒,那么這就是所謂的快照。
此時并不是返回了一個新的位置信息給你,而是返回給你最近一次得到的地址,此時app假定你在最后幾秒內沒有發生比較大的位移。
你要做的就是忽視這類結果,因為它們太舊了。
注釋2、判斷最新的位置精度是否比之前的一次要高,你使用了location對象中的horizontalAccuracy屬性,有時horizontalAccuracy接近于0,這種時候,測量就是無效的,你也應該忽視這個結果。
注釋3、這里你決定最后獲得的這個位置是否比之前的一個更加精準。總體而言,Core Location一開始的精度很差,然后逐漸提高精度,但是這里沒有保證,你也不能假設后面的就比前面的精度高。
注意一下,精度的值越大意味著越不精準,比如精度100米,和精度10米,明顯精度10米的精確度高,但是數值上100比10大。
同時你也檢查了location == nil的情況。回憶一下location是個可選型實例變量,用戶存儲你獲得的CLLocation對象,如果它為nil,那就是說程序可能剛剛運行,還沒有獲得位置對象,這種情況下,我們需要繼續執行獲取位置信息。
所以,如果location為nil,或者最后的一個位置信息的精度比前一個高,我們就執行第四步。否則,則忽視掉位置更新。
注釋4、我們之前見過這一部分代碼。它清除了之前所有的錯誤信息,如果之前有的話。并且將新的CLLocation對象存入到location變量中。
注釋5、如果獲取到的位置的精度,比設定要求的精度還要高,你就可以直接退出location manager了。最初我們設置的是精度10米,這就夠用了,對我們這個app而言。
短路
因為location是個可選型對象,所以你不能直接讀取它的屬性,你必須先對其進行解包。你可以用if let去解包,但是假如你確定這個可選型絕對不會為nil的話,你也可以用感嘆號來強制解包。
這就是你在代碼中的處理方式:
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
location!.horizontalAccuracy這里有一個感嘆號,而不僅僅是 location.horizontalAccuracy(不帶感嘆號)。
但是假如location等于nil,這個強制解包不會導致app掛掉嗎?在這種情況下不會,因為它根本不會執行。
這里的||操作符(或操作符),用于判斷兩個條件之一是否為真。如果location == nil為真,那么后面的條件會被忽略掉根本不會執行,這就叫做短路。當第一個條件為真時,就不需要再校驗后面的條件了。
所以app僅僅會在location不等于nil時才會執行location!.horizontalAccuracy,這時location得到了絕對不會為nil的保證。
運行app。首先設置模擬器的location為none,然后點擊Get My Location按鈕,屏幕上會顯示“Searching...”
將location切換到Apple(不要再次點擊Get My Location)。稍等片刻后,GPS坐標就會在屏幕上顯示出來。
如果你觀察一下調試區域,你大概會看到10幾條location更新,然后就是“*** We're done!”,并且此時location的更新就停止了。
??:也許你的執行結果和上面描述的不一樣。如果屏幕上沒有顯示“Seraching...”而是展示了舊的坐標信息,那么就是模擬器的緩存中保留了上一次獲取的舊的位置信息。
遇到這種問題可以嘗試先退出模擬器,然后再次運行app,如果還是不行,也不要擔心,由它去吧,模擬器有時不是很聰明
作為開發者,你可以從調試區域看到位置更新何時結束,但是用戶是無法看到的。
只要你一獲取到位置信息,Tag Location按鈕立刻會可見,如果用戶此時就保存地址,那么很可能保存的地址不是精度最高的那個。所以我們最好給用戶展示一些信息,可以讓用戶判斷當前的情況。
為了清晰的展示獲取位置信息的進度,你需要在獲取用戶信息進行時,將Get My Location按鈕的標題改變為stop,等獲取位置信息結束后,重新將標題切換為Get My Location。在課程的后面,你會添加一個讀取進度的動畫來清晰的展示搜索沒有結束。
為了切換按鈕的狀態,你需要添加一個configureGetButton()方法。
打開CurrentLocationViewController.swift添加這個方法:
func configureGetButton() {
if updatingLocation {
getButton.setTitle("Stop",for: .normal)
} else {
getButton.setTitle("Get My Location", for: .normal)
}
}
邏輯非常簡單,當app還在搜索位置信息時,按鈕標題顯示為“Stop”,搜索結束后按鈕標題顯示為“Get My Location”。
在以下幾個地方調用configureGetButton()方法:
override func viewDidLoad() {
super.viewDidLoad()
updateLabels()
configureGetButton()
}
@IBAction func getLocation() {
...
startLocationManager()
updateLabels()
configureGetButton()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
...
updateLabels()
configureGetButton()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
...
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
print("*** We're Done!")
stopLocationManager()
configureGetButton()
}
}
只要調用了updateLabels()的地方,都需要調用configureGetButton()。除了在locationManager(didUpdateLocations)中,你在搜索結束時,單獨調用了一下configureGetButton()。
運行app,并且測試一下效果。當點擊Get My Location按鈕后,這個按鈕的標題會改變為“Stop”,當搜索結束后,又會將標題切換回Get My Location。
當按鈕標題顯示為“Stop”時,用戶會非常自然的想要點擊這個按鈕來中斷搜索。尤其是在信號不好,搜索了很長時間的時候。
眼下,當按鈕顯示為“Stop”時,點擊它是沒有任何作用的。你需要改動一下getLocation()按鈕,將getLocation()中的startLocationManager()替換為下面的語句:
if updatingLocation {
stopLocationManager()
} else {
location = nil
lastLocationError = nil
startLocationManager()
}
你再一次使用了updatingLocation來判斷app的運行狀態。
如果按鈕是在搜索期間被點擊,那么你就中斷掉location manager。
注意一下,在開始一次新的搜索前,你需要將舊的位置信息和報錯信息都置空。
運行app,在搜索進行時點擊“Stop”,試試效果。
??:如果Stop顯示的時間過短,短到你都來不及點擊,那么你可以首先將location設置為None,這樣就會搜索很久了。
地址解析
GPS坐標給出的結果僅僅是代表經緯度的數字。比如坐標37.33240904, -122.03051218,能從上面得到的信息很少。
使用一種叫做地址解析的過程,你可以將經緯度坐標轉換成我們熟知的郵政地址。
你將使用CLGeocoder對象來將location數據轉換為可讀的地址信息,并且將這個地址信息通過addressLabel展現在屏幕上。
這是非常簡單的,只需要記住一些規則。你不能一次性發送大量坐標請求轉換。這個地址解析的進程會占用蘋果服務器的一些空間并且消耗一定帶寬,對于一個坐標,我們只發送一次請求。
地址解析需要設備接入互聯網。
打開CurrentLocationViewController.swift,添加以下屬性:
let geocoder = CLGeocoder()
var placemark: CLPlacemark?
var performingReverseGeocoding = false
var lastGeocodingError: Error?
CLGeocoder對象負責執行地址解析,CLPlacemark對象包含地址結果。
placemark變量是可選型是因為當沒有location時,它不會有值,或者獲取到的坐標無法被轉換。(比如非洲撒哈拉大沙漠,可能無法被正常轉換,雖然我沒有親自試過,不過你可以去撒哈拉沙漠試試,如果可以正常轉換,請告知我下。)
當一個地址解析操作發生時,你將performingReverseGeocoding設置為true。
你將地址解析工作放入locationManager(didUpdateLocations)中:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
...
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
print("*** We're Done!")
stopLocationManager()
configureGetButton()
}
//新的代碼從這里開始
if !performingReverseGeocoding {
print("*** Going to geocode")
performingReverseGeocoding = true
geocoder.reverseGeocodeLocation(newLocation, completionHandler: {
placemarks,error in
print("*** Found placemarks:\(placemarks),error:\(error)")
})
}
}
我們的app應該在同一時間僅執行一次地址解析,所以首先你通過performingReverseGeocoding檢查是否有地址解析正在執行。然后才開始地址解析
這段新的代碼看上去非常簡單明了,除了completionHandler這里你不太熟悉。
與location manager不同,CLGocoder不是使用委托來向你傳遞結果,而是通過一種叫做閉包(closure)的東西。
一個閉包就是一個代碼塊,和方法或者函數很像,閉包中的代碼通常不是立即執行,而是保存在某個地方并且在某個稍后的點執行。
閉包是Swift的一個重要特色,你到處都可以看到它(閉包和OC中的塊的概念很類似)
目前這個閉包中僅有一條print語句,這樣你就可以通過打印結果來確認發生了什么。
與locationManager(didUpdateLocations)方法中的其他代碼不同,閉包中的代碼并不是立刻執行。畢竟你要在地址解析結束后才打印地址信息,而前者也許會花幾秒鐘時間。
閉包被CLGeocoder對象保管,直到CLGeocoder找到一個地址或者遇到一個error時,閉包中的代碼才開始執行。
那么為什么CLGeocoder使用了閉包而不是委托呢?
使用委托提供反饋的問題是你必須寫一個或者多個獨立的方法。例如CLLocationManager對象,它的委托方法是:locationManager(didUpdateLocations)和locationManager(didFailWithError)。
通過使用不同的方法你將處理響應的代碼和返回結果的代碼分離開了。使用閉包,你可以把這些代碼放到一起。這樣使得代碼更加緊湊并且易讀。(有些API提供了閉包和委托可以讓你選擇)。
所以當你寫下面的代碼時:
geocoder.reverseGeocodeLocation(newLocation, completionHandler: {
placemarks, error in
// 在這里寫語句
}
你告訴了CLGeocoder對象你想要對location進行地址解析,并且閉包中的代碼應該在地址解析完成后立刻執行。
閉包本身是這個樣子的:
{ placemarks, error in
// put your statements here
}
in前面的關鍵字placemarks和error是閉包的參數,它們的工作原理和方法以及函數中的參數是一樣的。
當地址解析從location對象中找到一個結果后,它喚醒閉包并且執行其中的代碼。placemarks參數包含一個CLPlacemark對象的數組,用于描述地址信息,error變量當無法正常解析時包含報錯的信息。
再重復一次:當調用locationManager(didUpdateLocations)方法時,閉包中的代碼并不是立即執行,而是由CLGeocoder保管,直到地址解析結束后才執行。
委托方法的原則也是一樣的,除了一個使用獨立的方法,一個將代碼放入閉包中。
如果你對閉包不太理解,也沒有關系。以后我們會經常接觸它,你有的是機會熟悉。
運行app,一旦第一個location被找到,你就可以在調試區域看到地址解析的信息了:
didUpdateLocations <+37.33240754,-122.03047460> +/- 65.00m (speed -1.00
mps / course -1.00) @ 7/19/16, 1:43:25 PM Central European Summer Time
didUpdateLocations <+37.33233141,-122.03121860> +/- 50.00m (speed -1.00
mps / course -1.00) @ 7/19/16, 1:43:30 PM Central European Summer Time
*** Going to geocode
*** Found placemarks: Optional([Apple Inc., Apple Inc., Cupertino, CA
95014, United States @ <+37.33202890,-122.02956600> +/- 100.00m, region
CLCircularRegion (identifier:'<+37.33214710,-122.03128175> radius
368.39', center:<+37.33214710,-122.03128175>, radius:368.39m)]), error:
nil
如果你的location選擇為apple,你就可以看到和上面一樣的信息,地址解析對每個地址僅執行一次,當一個精度較高的坐標獲取到以后,才會再次進行地址解析,非常不錯。
??:部分讀者反應,在中國運行代碼時,如果選擇了中國意外的地區,那么解析會報錯,這種情況下你就選擇一個位于中國內的location就可以了。
在閉包中添加以下代碼,直接寫在print語句后面:
self.lastLocationError = error
if error == nil,let p = placemarks, !p.isEmpty {
self.placemark = p.last!
} else {
self.placemark = nil
}
self.performingReverseGeocoding = false
self.updateLabels()
你將error對象存儲起來,以便以在需要的時候引用它,雖然這次你使用了一個新的實例變量lastLocationError。
下面這一行的樣子,你以前沒有見過:
if error == nil,let p = placemarks, !p.isEmpty {
前面你已經學過if let是用來解包可選型的。這里,placemark是個可選型,所以你需要在使用它之前對他進行解包。解包后的placemarks數組被存放到一個叫做p的臨時容器中。
!p.isEmpty的意思是只有當placemark對象存在的時候,才執行if內的代碼。
這一行的正確讀法是:
if there’s no error and the unwrapped placemarks array is not empty {
//如果這里沒有報錯并且解包后的placemarks不為空
當然,Swift并不是英語,所以你要用Swift的術語來表達這些含義。
你也可以把這個單行的語句寫成嵌套的if形式:
if error == nil {
if let p = placemarks {
if !p.isEmpty {
對比一下就會發現,還是寫到一行又簡單又好讀。
我們在這里做了很多防御措施:首先檢查這里不存在報錯,然后確保解包后的placemarks數組中至少存在一個對象,而并不是假定placemarks數組中一定存在對象,好的程序員就要養成這樣的習慣。
如果三個條件都滿足了,沒有error,placemarks不為nil,并且其中至少存在一個CLPlacemark對象,然后你獲取數組中的最后一條CLPlacemark對象:
self.placemark = p.last!
last屬性的作用就是引用數組中的最后一條記錄。因為它是可選型,因為數組中可能不存在對象。所以你要用感嘆號來解包。你還可以用placemarks[placemarks.count - 1]來獲取數組中的最后一個對象,但是不推薦這樣做。
通常數組中只會有一個CLPlacemark對象,但是有一種古怪的情況,就是一個坐標對應多個地址。但是一次只能處理一個地址,所以你挑出最后一個(也就是精度最高的那個)來處理。
如果地址解析報錯,你就將self.placemark設置為nil。注意一下,你沒有對location進行同樣操作。如果這里有一個報錯,你還保有前一個location對象,因為也許它的精度就足夠高了。但是對于地址信息不能這樣處理。
你不能展示舊的地址信息,僅當地址信息與目前的坐標一致,或者根本沒有地址信息時才展示。
對于手機開發,沒有什么事情是確定的。你也許會得到一個坐標,也許不會,假設得到了一個地址,也許精度達不到要求。地址解析必須在手機以某種方式接入網絡才有用,但是你也要做好準備處理沒有網絡的情況。
并且你需要記住,不是所有的GPS坐標都能轉換為地址(比如你身處撒哈拉沙漠中)
??:你注意到沒,在閉包中你對這個視圖控制器的每一個實例變量和方法都使用了self關鍵字。這是Swift要求的。
閉包可以捕獲它使用的變量,self就是其中之一,也許你過會就把這個事給忘了,但是你一定要記住,在閉包中,被捕獲的變量一定要明確的指明它們的屬主。
你之前見到過,在閉包的外面,你可以使用關鍵字self來引用一個實例變量,但這不是強制性的。然而,閉包內省略了self的話就會編譯失敗。
讓我們把地址信息展示給用戶看
改變一下updateLabels()方法:
func updateLabels() {
if let location = location {
latitudeLabel.text = String(format: "%.8f",location.coordinate.latitude)
longitudeLabel.text = String(format: "%.8f", location.coordinate.longitude)
tagButton.isHidden = false
messageLabel.text = ""
//新代碼從這里開始
if let placemark = placemark {
addressLabel.text = string(from: placemark)
} else if performingReverseGeocoding {
addressLabel.text = "Searching for Address..."
} else if lastGeocodingError != nil {
addressLabel.text = "Error Finding Address"
} else {
addressLabel.text = "No Address Found"
}
} else {
...
}
}
因為一旦app獲得一個有效坐標你就會查找它對應的地址,所以你僅僅在第一個if內進行改動。如果你獲得了一個地址信息,你就將它展示給用戶,否則就展示狀態信息。
方法string(placemark)是一個新建的方法,用來將CLPlacemark對象轉換為字符串,我們現在來添加這個方法:
func string(from placemark: CLPlacemark) -> String {
//1
var line1 = ""
//2
if let s = placemark.subThoroughfare {
line1 += s + " "
}
//3
if let s = placemark.thoroughfare {
line1 += s
}
//4
var line2 = ""
if let s = placemark.locality {
line2 += s + " "
}
if let s = placemark.administrativeArea {
line2 += s + " "
}
if let s = placemark.postalCode {
line2 += s
}
//5
return line1 + "\n" + line2
}
我們來逐條講解下:
1、為第一行文本創建了一個新的String型變量
2、如果placemark有一個子街道(subThoroughfare),就把它添加到字符串中。這是一個可選型屬性,所以你首先用if let對其解包。和你知道的一樣子街道是房屋號的別名(歐美的地址特征,國內地址不會有這個屬性,子街道翻譯的也不是很準,明白意思就好)
3、把街道名稱(thoroughfare)添加進字符串。注意一下,子街道名稱和街道名稱間有一個空格。
4、對第二個字符串進行同樣的邏輯處理,這次是添加城市、省份和郵政編碼到字符串中。
5、最后把兩個字符串拼接為一個字符串,\n的意思是換行。
小貼士:如果你想知道一個方法或者一條屬性的意思,可以按住Option鍵,再降鼠標移動到上面點一下,就可以看到詳細信息了:
如果彈出的窗口是空的,那么就打開Xcode的設置面板找到Components子頁,然后點擊Documentation子頁,先安裝上iOS 10 documentation。
這個技巧同樣可以用于你自己定義的變量。Swift的類型推斷是很方便,但是有時候會使你搞不清變量的類型。用這個方法就可以查看變量的詳細信息了:
在getLocation()中,清空placemark和lastGeocodingError變量,這一步的作用是重置它們的初始狀態。在調用startLocationManager()方法的上方,添加如下語句:
placemark = nil
lastGeocodingError = nil
運行app。現在你可以看到地址信息和位置坐標都展示出來了:
街道號碼或者一些其他信息丟失,是非常常見的,不必為此驚訝。因為CLPlacemark中的信息本來就不全,這就是為什么這個對象中的所有屬性都是可選型的。
??:由于某些原因,UK(英聯邦)的郵政編碼會丟失最后幾個字符。上面截圖地址的全部應該是London England W1J 9HP。這貌似是Core Location的一個bug。
練習:如果你自模擬器的Debug菜單中選擇了City Bicycle Ride(環城自行車)或者City Run(環城跑),你應該會在調試區域看到一大堆不同的坐標跳出來(此時模擬器是在模擬不停的切換位置)。然而,屏幕上的地址和坐標并不會跟隨變化,這是為什么呢?
答案:MyLocation app的設計是找到固定位置的精度最高的坐標。你僅僅在一個新的坐標比前一個更高時更新location變量。任何精度較低或者精通相同的坐標都會被忽視,而并不會在意用戶的實際文位置在哪里。
在City Bicycle Ride(環城自行車)或者City Run(環城跑)模式下,app不會對同一個坐標縮緊精度,所以每個坐標的精度都是相同的。這就是說,這個app在移動中會表現很差,但是我們的設計就是用來測量固定位置。
??:如果你在模擬器中切換地區,或者在Xcode的debug區域中切換地區的時候卡住了,那就就重啟模擬器。有時模擬器不愿意切換到新的地區,這時你就要給它點教訓,重啟它。