回到LocationDetailsViewController.swift,當(dāng)用戶點(diǎn)擊done按鈕后關(guān)閉這個(gè)界面。
我們來做個(gè)新的挑戰(zhàn):不要立即關(guān)閉這個(gè)界面,如果HUD界面還沒有消失的話,界面就已經(jīng)被關(guān)閉了,那么會(huì)顯現(xiàn)非常蠢。
打開LocationDetailsViewController.swift,添加這一行:
import Dispatch
這是一個(gè)中央調(diào)度框架,Grand Central Dispatch,簡(jiǎn)寫為GCD。這是一個(gè)非常便利,但是比較底層的庫,用于處理異步任務(wù)。用這種異步的任務(wù)通知app等待幾秒鐘再合適不過了。
在done()方法的底部添加以下代碼:
let delayInSeconds = 0.6
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds, execute:
{
self.dismiss(animated: true, completion: nil)
})
這部分代碼的作用是在0.6秒后關(guān)閉Tag Location界面。
核心部分是DispatchQueue.main.asyncAfter()。這個(gè)函數(shù)的最后一個(gè)參數(shù)是一個(gè)閉包。在這個(gè)閉包內(nèi)部你通知視圖控制器把自己關(guān)閉掉。并且這個(gè)行為不是立即發(fā)生的。這就是閉包的特點(diǎn),函數(shù)內(nèi)的其他代碼都是逐行順序執(zhí)行的,而閉包內(nèi)的則不會(huì)。
DispatchQueue.main.asyncAfter()從.now() + delayInSeconds中得到延遲的時(shí)間,在時(shí)間到達(dá)后,執(zhí)行閉包內(nèi)的代碼。
??:我花了些時(shí)間來微調(diào)時(shí)間參數(shù),HUD的完全消失大概需要0.3秒,但是在HUD消失的同時(shí)立即關(guān)閉當(dāng)前界面,給用戶的感知也不好,所以經(jīng)過多次調(diào)整,我把延遲的時(shí)間確定為0.6秒。
運(yùn)行app,點(diǎn)擊Done按鈕,觀察一下效果,一切都感覺非常平滑。
不知道你感覺怎么樣,我的感覺是GCD這部分代碼把整個(gè)done方法弄的亂糟糟的。讓我們重新整理下代碼。
添加一個(gè)新的Swift文件,命名為Functions.swift。
把下面的內(nèi)容都放到新的文件中去:
import Foundation
import Dispatch
func afterDelay(_ seconds: Double, closure: @escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds,execute: closure)
}
這些代碼和剛才done方法中的非常相似,只是被單獨(dú)放到了一個(gè)新的函數(shù)中,afterDelay()。這是一個(gè)自由函數(shù),不是對(duì)象中的方法,它可以在你的所有代碼中隨意使用。
我們來看看這個(gè)方法中的第二個(gè)參數(shù),名字叫做closure。它的類型是 () -> ()。看起來很奇怪,它的意思是,這個(gè)參數(shù)的值是一個(gè)沒有參數(shù)及返回值的閉包。完整的定義是像下面這個(gè)樣子:
(parameter list) -> return type
在我們的應(yīng)用中,參數(shù)和返回值都是空,記做()。也可以寫作Void -> Void,但是我比較喜歡前一種。
所以,無論何時(shí),你在參數(shù)中看到->這個(gè)符號(hào),你就知道這個(gè)參數(shù)是一個(gè)閉包。
afterDelay()將這個(gè)閉包傳遞給DispatchQueue.main.asyncAfter()。
@escaping的意思是,閉包中的代碼不要立即執(zhí)行,swift看到這個(gè)符號(hào)時(shí),就知道應(yīng)該將閉包中的代碼保存起來,在適當(dāng)?shù)臅r(shí)候執(zhí)行。
也許你想知道,為什么我們要費(fèi)這么大事,接下來我就為你展示這一點(diǎn)。
打開LocationDetailsViewController.swift,將done方法修改為:
@IBAction func done() {
let hudView = HudView.hud(inView: navigationController!.view,animated: true)
hudView.text = "Tagged"
afterDelay(0.6, closure: {
self.dismiss(animated: true, completion: nil)
})
}
這就是swift的魅力所在,只要稍微看一下,就基本可以明白代碼的意思。
通過將繁瑣的GCD代碼移到新的函數(shù)中,done方法的可讀性大大的增加了。(原文啰嗦了很多,大意就是這個(gè),本書的作者是我見過的寫技術(shù)文檔中最喜歡賣弄文采的一個(gè),堪稱代碼界的郭小四。)
注意一下,因?yàn)殛P(guān)閉界面的代碼位于閉包中,所以你必須使用self關(guān)鍵字,就像你看到的這樣:
afterDelay(0.6) {
self.dismiss(animated: true, completion: nil)
}
看起來有點(diǎn)無法理解是嗎,閉包被放到參數(shù)的外面去了,Swift有個(gè)特性,如果一個(gè)函數(shù)或者方法的最后一個(gè)參數(shù)是個(gè)閉包,那么可以把這個(gè)閉包寫到參數(shù)外面,緊隨其后,就像上面那樣。
順便說一下,你可以把LocationDetailsViewController.swift文件中的import Dispatch這一句刪掉了。
運(yùn)行app,看看效果是否一致。
值類型和引用類型
Swift中的變量分為值類型和引用類型,它們的規(guī)則也是不同的。
從類中生成的對(duì)象都是引用類型的。其他的諸如:Int, Bool, Float and Double, String, Array,Dictionary以及enums都是值類型。
那么它們具體有些什么不同呢?
當(dāng)你聲明一個(gè)類型為Int或者CLLocationCoordinate2D的變量時(shí),它們就是值類型,編譯器會(huì)把它們的值存儲(chǔ)到一小塊內(nèi)存中保存下來。對(duì)于Int而言是8個(gè)字節(jié),CLLocationCoordinate2D是16個(gè)字節(jié)。
引用類型的變量在這一點(diǎn)上并無不同,比如你創(chuàng)建一個(gè)UILabel outlet,編譯器同樣是用8個(gè)字節(jié)存儲(chǔ)它,但是UILabel中可能存在多行文本,8個(gè)字節(jié)明顯不夠用。
當(dāng)你把值存儲(chǔ)到這兩種類型的變量中時(shí),它們的工作原理分別是這樣的:

值1337被直接存儲(chǔ)在變量number的內(nèi)存中,但是變量label的值是一個(gè)奇怪的數(shù)字。明顯它看起來和text label沒什么關(guān)系。這個(gè)數(shù)字并不是實(shí)際的UILabel對(duì)象,而是指向UILable對(duì)象的內(nèi)存地址。這就是引用類型。
內(nèi)存中的每一個(gè)字節(jié)都有獨(dú)一無二的地址,這里地址58741002引用UILabel對(duì)象在內(nèi)存中的第一個(gè)字節(jié)。
顯然,8個(gè)字節(jié)存儲(chǔ)label中的各種屬性是不夠的。
所以當(dāng)UILabel對(duì)象被創(chuàng)建時(shí),電腦會(huì)把它放到內(nèi)存中的某個(gè)地方,然后返回一個(gè)它的門牌號(hào)給你。
比如你的房子是一個(gè)實(shí)際的對(duì)象,而你的門牌號(hào)就是關(guān)于你的房子這個(gè)對(duì)象的引用,根據(jù)門牌號(hào)就可以找到你的房子。
我們來看幾組例子:
規(guī)則1:當(dāng)你創(chuàng)建一個(gè)常量后,你只能對(duì)它賦值一次:
let coord1 = CLLocationCoordinate2D(latitude: 0, longitude: 0)
coord1.latitude = 37.33233141 // 錯(cuò)誤
var coord2 = CLLocationCoordinate2D(latitude: 0, longitude: 0)
coord2.latitude = 37.33233141 //正確,因?yàn)檫@是一個(gè)變量“var”
但是引用類型常量的值似乎可以改變:
let label = UILabel()
label.text = "Hello, world!" // OK
label.text = "I like change" // OK
這是因?yàn)橐妙愋统A看鎯?chǔ)的是地址,而不是實(shí)際對(duì)象的值。但是你不能改變它存儲(chǔ)的地址,比如下面的語句是錯(cuò)誤的:
let label = UILabel()
let newLabel = UILabel()
label = newLabel //錯(cuò)誤
規(guī)則2:當(dāng)你把值類型的對(duì)象放到一個(gè)新的變量或者常量中去的話,它會(huì)產(chǎn)生一份拷貝,比如:
var a = [1, 2, 3]
let b = a
a.append(4)
print(a) // prints [1, 2, 3, 4]
print(b) // prints [1, 2, 3]
而引用類型則不會(huì)這樣,例如:
var firstLabel = UILabel()
firstLabel.text = "Programmers are the best!"
var secondLabel = firstLabel
secondLabel.text = "I like ice cream"
print(firstLabel.text) // prints “I like ice cream”
為什么secondLabel的文本變更了之后,firstLabel的會(huì)跟著一起變呢?因?yàn)樗鼈兇鎯?chǔ)的并不是lable對(duì)象本身,而是到這個(gè)label的引用,見下圖:

記住,只有從類中創(chuàng)建的對(duì)象是引用類型,其余的都是值類型。
object ownership(對(duì)象所有權(quán))
對(duì)象不是隱士,不會(huì)把自己關(guān)到山上不下來(看完全書我都不知道這句話有啥作用)。你的app中到處充斥著對(duì)象,它們互相間合作緊密。
你的app中的對(duì)象之間的關(guān)系可以用對(duì)象圖表來描述。例如:CurrentLocationViewController和許多對(duì)象有關(guān)聯(lián):

這些是它的實(shí)例變量以及實(shí)例常量,CurrentLocationViewController擁有它們。同時(shí)CurrentLocationViewController也可以被別的對(duì)象擁有,它屬于UITabBarController,而UITabBarController屬于UIWindows。

這只是你app中對(duì)象間關(guān)系的一小部分。不要把這個(gè)概念和類的層級(jí)弄混了。
對(duì)象所有權(quán)是iOS編程中的一個(gè)重要課題。你需要清楚地了解哪些對(duì)象擁有哪些對(duì)象,因?yàn)檫@些對(duì)象的存在取決于它(對(duì)象所有權(quán)) - 你的app的正常運(yùn)行也取決于它。
如果一個(gè)對(duì)象不在有任何其他對(duì)象擁有它,那么這個(gè)對(duì)象會(huì)被釋放掉。另一方面,如果一個(gè)對(duì)象的擁有者太多,可能會(huì)導(dǎo)致它永遠(yuǎn)無法被釋放,最終消耗掉大量的系統(tǒng)內(nèi)存。
所以“擁有”一個(gè)對(duì)象到底是什么意思呢?我們這里引入一個(gè)概念,strong和weak。
大多數(shù)情況下,當(dāng)你聲明一個(gè)擁有引用對(duì)象的變量時(shí),你就創(chuàng)建了一個(gè)strong關(guān)系。這個(gè)變量就假定擁有這個(gè)對(duì)象,它和其他同樣引用這個(gè)對(duì)象的變量一起對(duì)這個(gè)變量負(fù)責(zé)。
var image: UIImage // a strong variable
在weak關(guān)系中,不存在擁有者的概念。一個(gè)變量和一個(gè)對(duì)象之間的關(guān)系是weak的話,無法保證這個(gè)對(duì)象的存活。在聲明weak關(guān)系時(shí),你需要寫明weak關(guān)鍵字:
weak var weakImage: UIImage? // a weak variable
一個(gè)對(duì)象的生命期取決于它有多少個(gè)strong關(guān)系的屬主,而不是weak類型的。
那么weak類型到底有什么用呢?
它的存在主要是用于避免循環(huán)引用,就是兩個(gè)對(duì)象互為屬主,它們就永遠(yuǎn)都不會(huì)被釋放了。直到內(nèi)存被耗盡。
在之前的課程中你學(xué)習(xí)了,將委托方法聲明為weak類型的,來避免這種事的發(fā)生。過會(huì)你會(huì)看到另一種類型的循環(huán)引用。
另一種常見的使用weak的地方是在聲明@IBOutlet屬性時(shí)。這是因?yàn)橐晥D控制器并不是@IBOutlet的真正屬主,它的屬主其實(shí)是視圖控制器的頂層視圖。
如果你使用strong類型的outlet,不會(huì)有任何不良影響,但是假如你使用了weak,那么視圖控制器就會(huì)明白它僅僅是租用了這些屬性,而不是擁有它們。
weak引用有一個(gè)副作用,假如你對(duì)某個(gè)對(duì)象是weak引用,那么當(dāng)這個(gè)對(duì)象被釋放時(shí),你的引用計(jì)數(shù)器會(huì)自動(dòng)為nil。
這也是一件好事,因?yàn)槿绻贿@樣做,你會(huì)指向一個(gè)死去的對(duì)象,如果你試圖使用它,各種各樣的混亂將會(huì)發(fā)生。 (使用這種“僵尸”對(duì)象是沒有弱引用的語言程序崩潰的常見原因。)
這也是為什么weak引用變量必須始終是可選項(xiàng)(無論是?還是!),因?yàn)樗鼈兛赡茉谀硞€(gè)時(shí)刻成為nil。
??:Swift有第三種關(guān)系類型:無主的。 它和weak很像,用來打破循環(huán)。 不同的是,無主變量不會(huì)為nil,因此不必是可選型。 它沒有weak的功能強(qiáng)大,但是你可能在這里或那里碰到無主型變量。 你需要把它們都當(dāng)作是weak型就可以了。
總結(jié)一下:如果你想保持一段時(shí)間的對(duì)象,你應(yīng)該保存一個(gè)strong關(guān)系。 但是,如果你不需要擁有這個(gè)對(duì)象,并且不介意它在某個(gè)時(shí)候離開,那就使用一個(gè)weak或者無主的引用。
讓我們來看看一個(gè)對(duì)象有多個(gè)所有者的情況。
例如,當(dāng)用戶點(diǎn)擊Tag Location按鈕時(shí),Current Location視圖控制器將CLPlacemark對(duì)象傳遞給Location Details視圖控制器。 這兩個(gè)引用都是strong型的,所以從現(xiàn)在開始,Location Details視圖控制器將承擔(dān)共享所有權(quán)。 所以現(xiàn)在CLPlacemark有兩個(gè)所有者。
共享所有權(quán)和對(duì)象關(guān)系的概念僅適用于引用類型。 值類型總是從一個(gè)地方復(fù)制到另一個(gè)地方,所以他們永遠(yuǎn)不會(huì)擁有多個(gè)所有者。
結(jié)構(gòu)也是值類型,比如:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { ...
controller.coordinate = location!.coordinate
controller.placemark = placemark
}
這行代碼把location!.coordinate放入controller.coordinate時(shí)會(huì)生成一份CLLocationCoordinate2D結(jié)構(gòu)的拷貝。每個(gè)視圖控制器都會(huì)擁有各自獨(dú)立的一份關(guān)于GPS坐標(biāo)的拷貝。
對(duì)于placemark而言,則是每個(gè)視圖控制器都有一個(gè)到CLPlacemark對(duì)象的引用。

如果你感到頭暈的話,可以停下來,過幾天再看一遍。
說真的,如果你理解了這些概念,你就可以很好地編程。 如果不是,不要放棄。 這些可能是非常難以理解。 但是只要繼續(xù)下去,一定會(huì)搞懂。
??:如果你想復(fù)制引用類型的對(duì)象,則可以將其聲明為@NSCopying。 這會(huì)創(chuàng)建對(duì)象的副本,并將其放入新變量中。 之后這兩個(gè)變量都指向它們自己的對(duì)象。 @NSCopying更像是Objective-C風(fēng)格的東西,但是你應(yīng)該了解它一下。