插播一些理論知識
顯示屬性和引用屬性(Attributes and properties)
注:在我翻譯的iOS課程中Attributes翻譯為顯示屬性,properties翻譯為引用屬性。一般的計算機英語中這兩個詞都翻譯作屬性,它們之間的區別很微妙,在我們的教程中所有能夠在Xcode界面上看到,并且可以用鼠標或者輸入值的方式去調整的屬性,稱為Attributes;所有在代碼中需要用點句法引用的屬性稱為properties。所以我將Attributes翻譯為顯示屬性,properties翻譯為引用屬性以示區別。
大多數界面建造器上的指示器中的顯示屬性都可以直接對應到所選對象的引用屬性。例如,一個UILabel有以下顯示屬性:
它們直接對應UILabel的以下引用屬性:
如你所見,它們的名字也許并不完全一致,比如Lines對應numberOfLines,但是你可以輕易的猜到它們的對應關系。
你可以從Xcode的Help菜單中打開Documentation and API Reference,從中可以找到關于UILabel的所有引用屬性的說明。只需要在搜索框中輸入“uilabel”,就可以快速的找到它了:
UILable的文檔中并沒有列出全部顯示屬性對應的引用屬性。例如,在屬性檢查器中有一個分節,叫做“View”。這個分節中的顯示屬性來自UIView,UIView是UILabel的基類。所以你無法在UILabel的文檔中找到它,也許你可以在文檔的“Inherits From(繼承于)”欄目中找到它。
對象和類(Object and classes)
是時候講點新東西了。一直到現在,我把幾乎所有東西都稱為“對象”。然而,為了更好的理解面向對象編程,我們不得不討論一下對象和類。
當你像下面這樣做時:
class ChecklistItem: NSObject {
... }
你確實的定義了一個名叫ChecklistItem的類,并不是一個對象。一個對象是當你實例化一個類的時候產生的:
let item = ChecklistItem()
這個item變量現在包含一個ChecklistItem類的對象。你也可以這樣說:item變量現在包含一個ChecklistItem類的實例。屬于對象和實例的意思是一樣的。
換句話說:ChecklistItem類的實例是變量item的類型。
Swift語言和iOS框架已經有了許多內建的類型,但是你也可以通過制作新的類來添加你自定義的類型。
讓我們用一個例子來說明類和實例(對象)間的區別。
你和我都餓了,所以我們決定吃點冰淇淋(我除了程序以外的最愛)。冰淇淋是一個食物的類,我們馬上就要去吃它了。
冰淇淋類是這個樣子的:
class IceCream: NSObject {
var flavor: String //要什么味道的
var scoops: Int //要幾勺
func eatIt() {
// code goes in here
}
}
你和我走到冰淇淋柜臺前,想要倆個冰淇淋球:
// 這個是你的
let iceCreamForYou = IceCream()
iceCreamForYou.flavor = "Strawberry"
iceCreamForYou.scoops = 2
// 這個是我的
let iceCreamForMe = IceCream()
iceCreamForMe.flavor = "Pistachio"
iceCreamForMe.scoops = 3
yeah,我比你多一勺。
現在這個app有了兩個冰淇淋的實例,一個是為你創建的,一個是為我創建的。這里只有一個類來描述我們吃的是冰淇淋,但是這里有兩個不同的實例。你的是草莓味的,我的是開心果味的。
IceCream是一個模版,它生成的實例有兩個屬性flavor(味道)和scoops(數量:多少勺),還有一個名為eatIt()的方法。
任何由這個模版新生成的實例都包含這兩個實例變量和這個方法,但是它們存在在計算機內存中的不同位置,因此,每個實例中的屬性都可以有不同的值。
如果你對食物不感興趣,你可以把類想象為一個建筑的設計藍圖。它設計了一個建筑,但是它本身并不是建筑。根據一個藍圖,可以建造無數的建筑,它們的顏色或者窗口等都各有不同。
繼承
我們這里要講的并不是繼承一大筆財產。我們討論的是類的繼承,面向對象編程的主要原則之一。
繼承是非常強大的一個功能,它允許在一個類以另一個類為基礎建立。新建立的類擁有基類的所有數據和功能,然后它還可以添加屬于自己的專業功能。
我們還是用剛才的冰淇淋類為例子講解。它是基于NSObject建立的,這是一個iOS框架內建的類。你可以通過代碼中聲明類的那一行知道IceCream類是繼承了NSObject類的:
class IceCream: NSObject {
這就是說IceCream類擁有NSObject類的全部內容,并且有一些自己的小特色,比如名為flavor和scoops的引用屬性,以及eatIt()方法。
NSObject類是iOS框架中大多數類的基類。大多數你遇到的從某個類中創建的對象也是直接或者間接從NSObject中繼承來的,你無法避開NSObject。
你也見過有些類是這樣聲明的:
class ChecklistViewController: UITableViewController
ChecklistViewController其實就是一個擁有自己特色功能的UITableViewController類。它可以做UITableViewController能做的一切,并且還具備你所給予的額外的數據和功能。
繼承是非常便利的一個功能,因為UITableViewController已經可以為你完成許多工作了,它有table view,可以處理標準cell和靜態cell,還可以滾動大量列表,所有的這些都可以繼承到你自定義的類中。
UITableViewController自身是基于UIViewController建立的,UIViewController是基于UIResponder建立的,并且它們最終都是基于NSObject建立的,這就叫做繼承樹:
位于上方的每一個類的功能都比它下面的要強大。
NSObject僅提供一些所有對象都需要的基礎功能,例如,它包含一個alloc方法用于回收內存空間,和基礎的init方法。
UIViewController是所有視圖控制器的基類。如果你要創建一個你自己的視圖控制器,你就可以擴展UIViewController類。擴展的意思就是你通過繼承的方式創建一個類。
你不會想要從0開始為自己的界面和視圖寫代碼。如果這樣做的話,估計一輩子也沒有什么成就。
感謝在蘋果公司工作的大量聰明人,給我們帶來了這么多的基類。使得你可以簡單的通過繼承就可以獲得繼承樹中全部類的功能,你知需要添加一點點自己需要的數據和功能就可以了。
如果你的界面上主要部分是一個列表,那么你就創建一個類,繼承UITableViewController就可以了。同時你也擁有了UIViewController的全部內容,因為它倆也是繼承關系。只是UITableViewController在處理列表方面更加專業。
所以比起自己從0開始寫代碼,干嘛不利用現有的現成的東西呢?君子善假于物。類的繼承可以使你以極小的代價來重用現有的功能,它所節省的時間是無法想象的。想象一下自己從頭開始寫table view的代碼吧,如果是這樣,現在你還在第一節課爬行。
當程序員談到繼承的時候,同時還會有兩個伴隨的術語,就是父類(superclass)和子類(subclass)。
在上面的例子中,UITableViewController是ChecklistViewController的父類,相應的ChecklistViewController是UITableViewController的子類。
在Swift中,一個父類可以有許多子類,但是一個子類只能有一個父類。當然父類也有自己的父類。有許多類都是繼承了UIViewController的,例如:
因為幾乎所有的類都是從NSObject繼承來的,它們構成了一個龐大的繼承樹。所以理解類的層級是非常重要的,這樣你才能夠選擇合適的類作為自己定義的類的父類。
在之后的課程中你會看到,在程序中還會有其他很多類型的層級。
注意一下,在OC當中,所有你自定義的類都至少要繼承NSObject類。但是在Swift中則不需要,在OC中你不能像下面這樣聲明一個類:
class IceCream {
...
}
這時IceCream根本沒有一個基類(父類)。但是在Swift中這樣做是可以的,但是假如你試圖結合iOS框架和IceCream類一起使用就會出問題,因為iOS框架是用OC寫的。
例如,你不能將上面的那個IceCream和NSCoder和NSCoding一起使用,除非IceCream是從NSObject中繼承來的。所以即使你是使用Swift編程,你最好也在聲明每一個類的時候,讓它繼承NSObject。
重寫方法
繼承一個類意味著你的新類可以使用父類的屬性和方法。
如果你創建一個新的類Snack(小吃):
class Snack {
var flavor: String
fun eatIt() {
....
}
}
然后創建IceCream類,繼承這個類:
class IceCream: Snack {
var scoops: Int
}
然后你就可以在你的代碼任意一處做這些事情:
let iceCreamForMe = IceCream()
iceCreamForMe.flavor = "Chocolate"
iceCreamForMe.scoops = 1
iceCreamForMe.eatIt()
即使在IceCream中你并沒有聲明eatIt()和flavor實例變量,代碼依然可以工作。因為IceCream是從Snack中繼承來的,所以它自動獲得了Snack中的方法和實例變量。
如果你吃冰淇淋的吃法非常獨特,那么你也可以在IceCram中聲明自己的eatIt()方法:
class IceCream: Snack {
var scoops: Int
override func eatIt() {
// code goes in here
}
}
現在你調用iceCreamForMe.eatIt()時,被調用的就是新版的eatIt()了。注意一下,當你聲明父類中已經存在的方法時,Swift要求你必須在前面以關鍵字override注明。
還可以像下面這樣重寫方法:
class IceCream: Snack {
var scoops: Int
var isMelted: Bool
override func eatIt() {
if isMelted {
throwAway()
} else {
super.eatIt()
}
}
}
如果冰淇淋已經變質了,那么你就把它丟進垃圾桶,如果沒變質的話,你就調用父類Snack的eatIt()方法吃了它。
就像self是引用當前對象一樣,super關鍵字的意思是引用父類的對象。這就是為什么你在代碼的很多地方都能看到這個關鍵字,它的作用是調用父類的對象來完成你要的功能。
使用方法在類和子類間通信在iOS框架中非常常見,在某種通信下子類可以執行特殊的操作。比如viewDidLoad()和viewWillAppear()。
這些方法是由UIViewController定義并且執行的,但是你自己的視圖控制器子類可以重寫它們。
例如,當界面即將可視化的時候,UIViewController類會調用 viewWillAppear(true)。通常是由UIViewController自己調用viewWillAppear(),但是假如你在自己子類中重寫了這個方法,那么就會調用子類中重寫的這個方法。
通過重寫viewWillAppear(),你就得到了一個機會可以搶在父類前執行操作:
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
// 在父類前執行自己的操作
// 別忘了調用父類的方法!
super.viewWillAppear(animated)
// 在父類的方法后執行自己的代碼
}
}
這就是利用父類能力的方法。一個好的父類設計可以提供這種“掛鉤”,使你可以對某些事件進行響應。
不要忘記調用父類的方法!如果你忽視了這條,那么父類就不會得到自己的通知,很奇怪的bug就會出現。
你也早就在table view的數據源方法中見過重寫方法:
override func tableView(_ tableView: UITableView,didSelectRowAt indexPath: IndexPath){ ... }
父類UITableViewController早就執行了這些方法,所以假如你想要執行自己的自定義版本,那么你就需要重寫這些方法。
??:在這些table view的委托及數據源方法中,通常不需要調用父類的方法。是否需要調用父類,你可以從iOS API的文檔中查看。
在創建子類時,init方法也需要特殊照顧。
如果你不需要改變父類的init方法或者新增init方法,那么很簡單,你不要做任何事就可以了。子類會自動接管父類的init方法。
大多數時候都是如此,然而,假如你需要重寫init方法或者新增自己的init方法,例如,把值放入子類中的新的實例變量。這種情況下,重寫一個init方法是不夠的,你需要重寫它們全部。
在下一個課程中,你會創建一個類GradientView,繼承UIView。這里會使用init(frame)來創建并且初始化GradientView對象。GradientView中會重寫init方法,來設置background color的值:
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.black
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
...
}
因為UIView還有一個init?(coder)方法,所以GradientView需要執行這個方法,即使它不做任何事情。
同時注意一下init(frame)的前面有override關鍵字,但是init?(coder)前面的關鍵字是required。required關鍵字用于強制每個子類總是執行某個特定的init方法。
Swift希望確保子類不會忘記將自己的東西添加到這些必需的init方法中,即使應用程序實際上并沒有使用該init方法,就像GradientView中那樣。Swift過度的關注了這一方面的內容。
繼承init方法的規則確實有些復雜,官方的Swift編程指導中花了大量篇幅講解這塊內容,不幸中的大幸就是,即使你在這里出錯了,Xcode會及時提醒你的。
私隱部分(Private parts)
那么,子類可以使用父類的全部方法嗎?也不盡然。
UIViewController和其他的UIKit類都擁有許多方法是隱藏的。這些神秘的方法可以做很酷的事情,所以你可能會試圖去使用它們。但是它們不屬于官方API的一部分,所以我們這種凡人,還是不要去碰它們的好。
如果你曾經在昏暗的巷子聽到其他開發者用低沉的聲音說道“Private API”,它們說的就是這會事。
如果你知道這些方法的名字,那么理論上你是可以調用它們的,但是不推薦這樣做。它可能會使你的app被拒絕發布,因為蘋果公司會掃描app是否調用了這些Private API。
你不能使用這些API基于兩個理由:
1、它們可能具有非常大的負面作用。
2、它們也許并不是在任何版本的iOS系統中都存在
使用它們的風險是非常大的。
有時,使用這些API是讀取某些設備功能的唯一途徑。如果你用了的話,你的好運也就到這里結束了。幸運的是,對于大多數app而言,官方公開的API已經非常夠用了。
角色扮演(Casts)
順便說一下,Casts的官翻是類型轉換。
代碼中經常會出現一個實例不是被它所屬的類引用,而是它的父類引用。這聽起來有點奇怪,我們來看一個例子。
你現在正在寫的這個app中,有一個UITabBarController,它有三個子頁,每一個都代表一個視圖控制器。第一個子頁的視圖控制器就是CurrentLocationViewController。之后,你會新建另外兩個,第二個是LocationsViewController,第三個是MapViewController。
iOS的設計師在創建UITabBarController時顯然不知道這三個特定的視圖控制器。tab bar controller唯一知道的事情就是,每個子頁的視圖控制器都是繼承自UIViewController的。
所以對tab bar controller而言,它只能看到它們的父類UIViewController。
就tab bar controller而言,它只知道自己有三個UIViewController實例,它并不知道也不關心你所額外添加的東西。
對UINavigationController也是一樣的,對導航控制器而言,任何新的被推到導航棧堆中的視圖控制器,對它而言都是UIViewController的實例。
有時這會帶來點麻煩。當你向導航控制器請求它棧堆中的一個視圖控制器時,它會返回一個到UIViewController對象的引用,即使這并不是這個對象的完整類型。
如果你想像處理自己的子類控制器那樣處理這個UIViewController對象,你就需要對這個UIViewController進行角色扮演(這也是為什么我把Casts翻譯為角色扮演,而不是類型轉換)。
在之前的課程中,你在prepare(for,sender)中執行過這樣的代碼:
let navigationController = segue.destination as! UINavigationController
let controller = navigationController.topViewController
as! ItemDetailViewController
controller.delegate = self
這段代碼中,你想要從導航控制器的棧堆中獲取最上面的視圖控制器,這個視圖控制器是一個ItemDetailViewController,并且設置它的delegate屬性。
然而,導航控制器的topViewController屬性不會給你一個類型為ItemDetailViewController的對象。它返回的是一個UIViewController類型,根本不會包含你所要的delegate屬性。
如果這里不使用‘as! ItemDetailViewController’,而是像下面這樣:
let controller = navigationController.topViewController
那么Xcode就會給出一個報錯。因為Swift推斷這個類型為UIViewController,而這個類型并沒有delegate這個屬性。這個屬性在你創建的子類ItemDetailViewControllers中才有。
雖然你知道topViewController引用了一個ItemDetailViewController,但是Swift并不知道。
為了解決這個問題,你需要把這個對象進行角色扮演,讓他看起來你要的對象。所以你使用了‘as! ItemDetailViewController’,來告訴編譯器,我想要這個對象和ItemDetailViewController一樣。
帶上角色扮演,代碼就成了,你所熟知的樣子:
let controller = navigationController.topViewController
as! ItemDetailViewController
(在Xcode中,你可以把上面的代碼放到一行。使用長度長的,可描述性強的名字是使代碼可讀性更好的絕佳方案,但是如果排版太亂了,也不好看)
但是編譯器不會檢查角色扮演的結果是否是你想要的那個對象,所以如果你寫錯了,app多半就會掛掉。
由于其他原因,角色扮演可能會失敗。例如,你想要的扮演的對象的值為nil。如果可能出現這種情況的話,最好使用as?來使它成為可選型。此時你需要將它的值也存儲為可選型,或者用if let去解包。
注意一下,角色扮演不是萬能的。你不能對Int執行角色扮演,使它成為String型的。角色扮演只能在相互兼容的兩個對象間執行。
總結一下,角色扮演的方式有三種:
1、as?:用于允許失敗的角色扮演。如果一個對象為nil或者與你想要扮演的對象的類型不兼容。此時,角色扮演會失敗,但是沒有關系。這種角色扮演的返回值是可選型,你可以用if let解包。
2、as!:用于一個類和它的子類之間。與隱式解包可選型一樣,你必須確認沒有風險時,才可以使用as!。
3、as:用于絕對不會失敗的情況。Swift有時會保證角色扮演永遠有效,比如NSString和String之間。
到底使用那個,也會會使你陷入選擇困難癥。你可以每次都是用as,然后看看Xcode的建議,會有一個黃色的提醒,你只需要根據建議修改就可以了。