第三章:Classes and Structs 類和結構體
到現在你已經瞭解了整型,字符串,數組和字典,但是僅有他們還是不夠的。Swift和其他面向對象編程的語言一樣,也提供了通過定義類來保存數據以及調用類裏的方法。如果你熟悉Object-C,Java,c#等其他編程語言,那你肯定已經接觸過class這個概念了。
Swift也允許你自定義結構體,簡稱結構。和其他如c一樣的語言不同,Swift中的結構可以像類一樣保存數據,調用方法。
靠,那類和結構還有毛的不同?這樣的疑問對於理解他們的概念是非常重要的!在本章,你將利用一個使用了類和結構的應用來學習他們的創建以及部署過程。
這個新的應用將帶你去硅谷尋找寶藏。說不定下一次你參加WWDC的時候,你就可以用這個app找到寶藏也說不定~~~
別yy了,趕緊學習吧!
Getting started - 開始
爲了讓你能直接學習瞭解,所以我已經幫你新建了一個項目,因爲這一章的重點是類和結構,所以你沒必要花時間去建一個操作項目。直接進入主題更快一些不是!
原書提供的資源代碼都不可用了,所以我會重新都生成一遍項目(又是一個工作量,悲了個劇),在提供的資源文件夾中第三章中可查看。打開工程項目你會發現是很簡單的一個項目,裏面就是一個帶有地圖view的導航欄控制器。
運行代碼可看到顯示了中國區域的地圖:
The class concept - 類的概念
如果你有過Object-C或者其他面向對象的編程經驗,那你肯定對類十分的瞭解。但是爲了着重區分類class和結構structs的差異,所以我們再來溫習下類的概念吧!
在面向對象的編程中,你主要是通過類的實例或對象進行控制。對象有相關的數據以及操作這些數據的方法。一個對象的數據主要是數值或者字符串這樣的原始數據,也可能包含有其他對象的引用。
一系列的對象通常可以建立成一個有着層級結構的模型。這涉及到生物分類學中的一個經典案例。比如說貓,青蛙,烏龜以及貓頭鷹全是動物(主要是四足脊柱動物),他們有着他們相同的屬性(年齡,體重,物種)但也有些自己獨有的(貓的毛皮的顏色和質感,貓頭鷹的翅膀)。
你可以在面向對象的語言中用層級結構的類來描述他們的關係。比如說他們有個擁有着共同屬性的叫animal的類。不同的子類如貓,青蛙,烏龜以及貓頭鷹可以繼承自這個animal類。每個animal子類都可以表現出父類的行爲,繼承父類的方法和數據。
那麼結構又有什麼不同呢?在Swift中,類和結構都可以幫你模型化數據,他們都可以保存數據並且有着處理這些數據的方法,但是又各有特點。在這章你會發現他們的不同的地方,以便讓你知道什麼時候用類,什麼時候用結構。(我勒個去,纔要進入正題,看着這麼一丟丟,翻譯起來和嚼蠟一樣…)
My first class - 我的第一個類
當前這個應用做的事情還很少,只是展示了你在地球的哪個區域。很明顯你還需要知道one piece在哪疙瘩。
你在硅谷(改爲成都好呢還是改爲中國好,糾結)的寶藏有這以下幾個特點:
1.HstoryTreasure:歷史文物寶藏是包含了和年相關的歷史遺蹟
2.FactTreasure:真相寶藏包含了和寶藏相關的線索。
3.HQTreasure:寶藏基地是硅谷的公司總部(繼續糾結中,用首都?還是成都?)
每個這些寶藏都有着和位置相關的信息。如果你想到了用類的層次結構來劃分。恭喜你,你上道了!
你可以將上述描述的情況用下列對象圖進行表示:
上面的圖表展示了類的層次結構。每個矩形框都代表了一個類,裏面包含了名字以及一些相關的數據。用這種方法可以看到,每個treasures(寶藏類)都有兩個“what(和寶藏相關的信息)”和“location(寶藏的位置)”屬性。
現在可以來操作看看了!
Creating the class - 創建類
在工程中添加一個新的類點擊xcode的File\New\File… ,選擇iOS\Source\Swift File ,點擊next並命名爲Treasure 。
你現在能看到一個空白的Swift文件生成。如果你是從Object-C轉過來的,你可能就傻眼了。Swift不需要單獨的一個header頭文件,也不需要一個.m的實現文件。你也不需要用import導入你的代碼。看上去是有點奇怪,但用久了肯定會覺得方便不少。
import讓你可以訪問其他庫裏面的類,結構以及方法等。新生成的文件第一行就爲你導入了整個的Foundation 框架。
如果你是Object-C的開發員的話,一定對Foundation框架很熟悉。這個框架裏包含了常用的對象,比如最基礎的NSObject類等。
Swift在Foundation框架中提供了自己對類的基本實現,比如前面章節中提到的字符串,數組和字典。然而你會在本章以及本書中發現,import導入對Swift的開發仍然是非常重要的。
先敲些代碼吧:
class Treasure {
let what: String
let latitude: Double
let longitude: Double
}
這個基本的例子聲明瞭一個類:在關鍵字class後面給你想要定義的類命名,然後在括號裏定義你的類。你可以在類裏用var也可以用let聲明你的屬性,就像在方法中聲明局部變量一樣。var和let的區別便是確保你的代碼在上下文中是否是可修改的。
因爲編譯器無法推導出他的類型,所以你需要聲明下,如果你需要初始化值得話,也可以直接在類型後面進行初始化。
小技巧:如果你要初始化值得話,那麼你就沒有必要明確的聲明數據的類型,因爲編譯器可以自己推導出來。然而直接聲明類型的好處在於你可以直接一眼掃過去便知道所有屬性數據的類型了。
眼尖的讀者可能已經發現所有的這些變量都是常數,而且都沒有定義初始化值,怎麼可以這樣呢?不是必須初始化嗎?
仔細一看,xcode其實已經報錯!
錯誤提示你需要給你定義的類初始化,因爲類裏面的常數都沒有初始化值。牢記,只要不是可選類型就一定要爲屬性初始化一個有效的值。
在這花括內,幾個屬性的下面添加以下代碼:
init(what: String, latitude: Double, longitude: Double) {
self.what = what
self.latitude = latitude
self.longitude = longitude
}
這幾行代碼在Swift中代表着初始化,類似於Object-C中的init或者c++和java中的構造方法。
這應該是你Swift中敲的第一個函數,恭喜你咯~~
Swift的語法是要求非常一致的。在Swift的函數中參數的名字以及類型必須和你定義的所有變量一致。注意的是必須要有描述的類型,因爲在函數參數的地方沒有值讓他推斷這個參數應該是什麼類型。
初始化只有一個工作,在類初始化實例時自身的屬性必須有值。在這個類中,因爲要給三個變量設值。而所有的類的值都是通過初始化傳入的,所以類的對象必須要在初始化的時候就知道這三個值,並傳入。這是Swift設計的另外一個安全機制。什麼寶藏沒有經緯度值呢,有脾氣你舉個例子出來!所以這三個變量是必須要有值滴。該守得規則還是要守滴。
這個類基本就做的差不多了,有個小地方可能需要再改進下。
A struct-ural improvement - 結構的改進
到目前爲止,一切看上去都挺好,但美中不足的是經度和緯度應該是在一起的。他們應該是一個單元中包含的兩個信息,就像寶藏信息應該是由“什麼寶藏”和“在什麼地方”組成一樣。所以將經緯度包裝在一起而不是保存成兩個Double數據顯然是更好。
struct GeoLocation {
var latitude: Double
var longitude: Double
}
這就是結構方便的地方了。結構可以像類一樣持有數據,也可以持有方法。但結構可以被當做是值的對象,什麼都不用做只需要持有數據就行。(值的對象你可能還不太理解,後面會有例子講解)
繼續在項目中添加一個文件,點擊File\New\File… ,選擇iOS\Source\Swift File並點擊下一步。並命名爲GeoLocation 。
同樣的一個空的Swift文件。添加代碼如下:
struct GeoLocation {
var latitude: Double
var longitude: Double
}
這可能讓你想起類的定義。這裏只有一點不一樣。用的是關鍵詞struct而不是class。這便是在Swift聲明瞭一個結構,簡單吧!!!
你定義了一個用來同時保存經度和緯度的結構。結構裏面的屬性通常用的是變量而不是常數。但是隻要你願意,你也可以用常數定義。
現在在你的Treasure類中部署新建的結構,打開Treasure.swift 並替換掉你定義的經度和緯度。代碼如下:
let location: GeoLocation
注意下你並沒有導入GeoLocation文件但依然可以用。這是因爲在Swift中,應用中的每一個文件都會自動導入到你要用的另一個文件中。即使你寫一個靜態庫或者框架也是如此,其他的文件都可以訪問。
這讓碼農輕鬆不少不是!Object-C的開發員都知道在Object-C代碼中光是用import都要用碼不少代碼。
你也應該注意到現在初始化的地方報錯。這是因爲你應用的經緯度剛剛已經移除了。修復下:
init(what: String, location: GeoLocation) {
self.what = what
self.location = location
}
所有的工作都做完了,而且明顯現在展示地理位置的方式顯得更高明不是。簡單的用一個有意義的結構而不是兩個不明所以的Double。
Reference types vs. value types - 引用類型VS值類型
正如你所見的,結構和類的定義是非常像的。回想下結構是怎麼存儲值的。兩者間的區別並不抽象:類是引用類型,而結構是值類型。這意味着當你傳遞一個類,Swift實際傳遞的是類的引用給另外一個對象。當你傳遞一個結構時,Swift是複製內容給對象。下面我們來演示下。
Object-C的開發員可能會發現在Object-C中類和結構的展現的行爲是一樣的。
思考下下面的代碼以及輸出語句(在playground中試下):
這定義了都包含有一個變量值的結構和類。都先生成一個變量對象,然後再賦值給第二個變量對象,接着修改第二個對象中屬性的值。
注意發現在結構中,僅僅第二個變量值發生了改變,而在類中兩個變量值都發生了改變。這個例子很有代表性。當你將classA分配給classB,Swift用相同的引用將兩個變量的真實指針賦予到相同的實例上。但是當你分配structA到StructB時,Swift僅僅是複製了他在結構中存在的值。
小技巧:在代碼的執行底層,Swift擁有寫入時複製的功能,能夠聰明的知道只有到絕對需要的時候纔開始複製結構的值。這也就是說structB=struct
並沒有立即執行復制的功能。只有當你要改變他的值,在運行的時候纔開始複製
。
下面的示例圖進一步的說明結構複製值與類複製引用的區別:
雖然看上去差異很小,但是類和結構在構建一個常數let時的差異確是非常明顯的。
回想下第一章關於var和let關鍵字定義的變量和常量。當一個實例比如變量,類和結構的操作是相同的。你可以修改他們的屬性或重新給他們分配新的值。當一個實例是常數時,類和結構有着非常大的不同,如下所訴:
1.在常量類中,你可以修改類中的屬性,但是不可以重新分配一個類給常量類
2.在常量結構中,你不可以修改結構的屬性也不可以重新被分配值
上面說的有點抽象,瞧瞧下面的例子:
在Swift中常量結構完全不可進行修改!這也正是爲什麼數組和字典是結構而不是類。
Convenience initializers - 便利初始化
有時用一個簡單的便利方法進行初始化也是不錯的,在這個Treasure案例中,在初始化的時候直接賦值經緯度,讓用戶無需再在GeoLocation中初始化,顯然更好些。
打開Treasure.swift 並將代碼添加到初始化下面:
convenience init(what: String,latitude: Double, longitude: Double)
{
let location = GeoLocation(latitude: latitude, longitude: longitude)
self.init(what: what, location: location)
}
這便是所謂的便利初始化了,convenient關鍵字說明他還沒有完整的初始化自己,而是推遲了本身的初始化。否則便稱爲指定初始化,即手動指定一個初始化方法。
Object-C的開發者可能對於指定初始化比較熟悉,這個機制在Object-C中運用了很長一段時間。Swift在正式的編譯時候會執行,如果沒有在初始化中初始化變量,則Swift直接拋出編譯時錯誤。
你可能還有些奇怪,因爲這個便利初始化中還創建了GeoLocation,但是你並沒有在其結構中聲明一個初始化。結構不需要初始化,Swift在創建他們時,爲每一個參數都定義了一個按順序排序的初始化方法。這使得結構的初始化出乎意料的簡單和易懂。因爲他們直接將數據描述封裝在了結構體中。
Class inheritance - 類的繼承
現在讓我們來了解下類的繼承。回想下可以知道Treasure 類有三個子類:HistoryTreasure, FactTreasure 和 HQTreasure.
打開Treasure.swift 並且將下面的代碼加到文件的底部。
// 1
class HistoryTreasure: Treasure {
let year: Int
// 2
init(what: String, year: Int,latitude: Double, longitude: Double)
{
self.year = year
let location = GeoLocation(latitude: latitude, longitude: longitude)
super.init(what: what, location: location)
}
}
// 3
class FactTreasure: Treasure {
let fact: String
init(what: String, fact: String, latitude: Double, longitude: Double)
{
self.fact = fact
let location = GeoLocation(latitude: latitude, longitude: longitude)
super.init(what: what, location: location)
}
}
// 4
class HQTreasure: Treasure {
let company: String
init(company: String, latitude: Double, longitude: Double) {
self.company = company
let location = GeoLocation(latitude: latitude, longitude: longitude)
super.init(what: company + "總部", location: location)
}
}
讓我們分部分的來剖析下:
1.你可以在聲明的時候表示類的繼承關係,只需要在類名的冒號後面添加父類的名字就可以了。
2.HistoryTreasure有個和寶藏相關額外的信息-年,因此你必須指定一個初始化方法來初始化這個值。如果你沒有指定初始化方法,則Swift會自動調用父類的初始化值,但是父類的初始化中沒有year這個屬性。
類的指定初始化必須指定用父類的初始化,不可以直接使用convenience 的初始化,這就有點尷尬了,你需要複製用來創建地理位置結構的代碼。
如果你是Object-C開發員的話會發現調用父類的super.init()的位置有點奇怪。是放在方法的最後位置,因爲在Swift中,初始化的工作是用來初始化這個類中所有聲明的屬性,然後交給父類。父類方法並不知道子類做了些什麼,有什麼,所以不能放在前面。
3.然後是聲明FactTreasure和HQTreasure。他們和前面的一樣,每個都有自己相關的的數據,所以都需要初始化方法來初始化值。
嘖嘖!你寫了一個類,一個結構和一個完整的繼承結構。你該感到自豪了(啊啊啊,自豪點在哪啊)。編譯並運行下看看代碼能不能正常運行。因爲你還沒有使用過你的新類和結構,所以應用程序的運行和剛剛相比並沒有變化。接着來學習下一步。
Swift and MapKit - Swift和地圖框架
你寫了一個類來保存treasures寶藏,也寫了一個保存有地址信息的結構。是時候將一些寶藏在地圖中顯示出來了。
打開ViewController.swift.裏面就是一個包含了一個地圖view的類。
先來了解下類中的代碼:
override func viewDidLoad() {
super.viewDidLoad()
}
這是你見到的第一個覆蓋函數的例子,控制器調用了一個叫viewDidLoad的方法然後在裏面加載view。你可以在這個地方自定義你的view。
在Swift中要注意的是,如果你在類中要覆蓋一個已經存在的方法,則一定要加上關鍵字override。這樣無論是誰看到都能知道這個方法覆蓋了父類中的方法。
關鍵字override能夠幫助編譯器檢查你的方法是不是正確的。比如,如果你拼錯了一個覆蓋的方法名,則編譯器提示錯誤,因爲父類中不存在。同樣的,如果你不知道是否父類有某個方法,自定義了一個viewDidLoad這個方法,則編譯器會拋出錯誤告訴你這個方法存在了。
你應該也注意到了用關鍵字super來調用了父類的方法。
在@IBOutlet的代碼後面聲明如下的屬性:
var treasures: [Treasure] = []
現在將寶藏顯示在你的地圖中,你將先初始化叫treasures的數組,將下面的代碼添加到viewDidLoad 的下面
self.treasures = [
HistoryTreasure(what: "Google總部", year: 1999, latitude: 37.44451, longitude:-122.163369),
HistoryTreasure(what: "Facebook總部", year: 2005, latitude: 37.444268, longitude:-122.163271),
FactTreasure(what: "斯坦福大學", fact: "成立於1885年的利蘭·斯坦福.", latitude: 37.427474, longitude: -122.169719),
FactTreasure(what: "莫斯科尼", fact: "自2003年以來WWDC的主辦地.", latitude: 37.783083, longitude: -122.404025),
HQTreasure(company: "Apple",latitude: 37.331741, longitude: -122.030333),
HQTreasure(company: "Facebook",latitude: 37.485955, longitude: -122.148555),
HQTreasure(company: "Google",latitude: 37.422, longitude: -122.084),
]
現在將硅谷的所有寶藏數據都放在了self.treasures數組中了。不要糾結爲何應用導航欄寫的是中國尋寶,但是座標全是美國的。表在意細節╮(╯▽╰)╭
Class extensions and computed properties - 類的擴展和計算屬性
現在你需要將這些寶藏地點在地圖中展示出來,將“annotations”大頭針在mapkit中標記出來。annotations需要遵循MKAnnotation協議。
打開Treasure.swift 並在第一行中添加導入代碼:
import MapKit
然後在定義類的代碼下面添加擴展代碼:
extension Treasure: MKAnnotation {
var coordinate: CLLocationCoordinate2D {
return self.location.coordinate
}
var title: String {
return self.what
}
}
將代碼複製進playground會報錯,Swift2.0有了些許修改,報3個錯
1.Treasure類遵循NSObjectProtocol
2.變量要前面用@objc聲明
3.title的類型和協議中定義的類型不一樣。所以修改代碼如下
這就是類的擴展,允許你給類添加一些額外的方法。如果你有過Object-C的開發經驗,你應該知道在Object-C中也是有擴展存在的。在Swift中,顯著的區別在於不僅可以額外添加方法,還可以添加屬性。正是因爲如此,將代碼放在擴展或是放在主類中似乎是沒什麼區別了,但是放在不同的擴展中對於代碼的理解和維護卻有很大的幫助。
你在擴展的方法中實現了MKAnnotation的協議。這個協議定義了你必須有兩個屬性:coordinate和title。你聲明這兩個屬性但是和平常的聲明看上去有些不一樣:儘管他們都是用了var來定義。但是他們包含了一個和函數一樣用於返回值的閉包方法。
他們是計算屬性,不是函數。他們和一般的屬性的不同在於他們的值是每次計算所得而不是依靠傳入的實例變量。除了計算屬性是每次執行相關聯的代碼來訪問屬性外,其他的和一般的屬性使用相同。
目前,這個代碼因爲在訪問一個並不存在的coordinate屬性,所以不能編譯,先不管。
Your first struct extension - 你的第一個擴展結構
打開GeoLocation.swift 並導入以下代碼:
import MapKit
然後在文件的最底部添加擴展代碼
extension GeoLocation {
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: self.latitude,longitude: self.longitude)
}
var mapPoint: MKMapPoint {
return MKMapPointForCoordinate(self.coordinate)
}
}
就像類一樣,結構也是可以擴展的。在上面代碼中,你不需要像類Treasure那樣聲明任何協議。相反,這是個簡單誇張獨立的代碼。好處非常明顯:讓你的邏輯代碼以單元模塊形式分開。在這個示例中,你將座標和地圖管理的代碼分開了。
這裏的兩個計算屬性返回了你聲明的座標(經度和緯度)和地圖的位置(mapPoint)。運行代碼。
一切正常,但是在不將Treasure聲明繼承自NSObject時,此時會報錯。也就是說Swift1.0中,Treasure如果沒有繼承NSObject時,使用協議不會報錯,但是,在運行編譯的時候會報錯。Swift2.0改進了,在靜態編譯,不運行代碼的時候便提示你需要繼承了。
Inheriting from NSObject - 繼承自NSObject
這個錯誤是因爲你使用的協議MKAnnotation繼承自NSObjectProtocol。爲了定義的一致性,所以你的Treasure也必須是要遵循NSObjectProtocol.
Object-C的開發員可能對NSObject非常熟悉。NSObject是Apple Object-C框架代碼中,幾乎所有對象的基類。NSObject是一個遵循了NSObject協議的類。爲什麼這個類即是協議又是類超出了咱們本書的範圍。但是毋庸質疑的是,你知道Treasure是要遵循NSObjectProtocol協議的。
繼承代碼如下:
class Treasure: NSObject
如果你使用MapKit,那麼你將發現經常都會使用到NSObject。通過NSObject的繼承,你可以無縫的讓Swift類使用Object-C的類。是不是很神奇,很拽~~。
Pinning the map - 在地圖上標記大頭針
現在Treasure類遵循了MKAnnotation,你可以將他添加到mapView中了。打開ViewController.swift並在ViewDidLoad的結尾處添加以下代碼:
self.mapView.delegate = self
self.mapView.addAnnotations(self.treasures)
先別急着看xcode上提示你控制器要添加協議。第一行聲明瞭當前控制器是地圖view的代理。這第二行是將地圖的註釋座標全部放入地圖中。這個代理運行控制器去告訴地圖如何展示這些寶藏點。
在文件底部添加類的擴展:
extension ViewController:MKMapViewDelegate{
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
let treasure = annotation as? Treasure
if treasure != nil {
var view = mapView.dequeueReusableAnnotationViewWithIdentifier("pin") as? MKPinAnnotationView
if view == nil {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
view?.canShowCallout = true
view?.animatesDrop = false
view?.calloutOffset = CGPoint(x: -5,y: -5)
view?.rightCalloutAccessoryView = UIButton(type: UIButtonType.DetailDisclosure)
}else{
view?.annotation = annotation
}
return view
}
return nil
}
}
說明:
1.is:和右邊的類型一樣或者是右邊類型的子類
2.as!:當左邊類型是右邊類型的父類時,可強轉爲右邊的子類,不符合便報錯
3.as?:和option可選類型一樣,如果左邊的父類強制轉換爲右邊的子類失敗返回nil
這聲明瞭一個遵循了MKMapViewDelegate協議的控制器,以便他可以做地圖view的代理,下面是代碼的解釋:
1.實現了mapView:viewForAnnotation方法。
2.如果annotation是Treasure類,那麼在地圖上顯示的pin會重複複用標記了唯一標識符“pin”的view。程序會不斷複用這個view,而不是新建一個新的view。如果你對UITableView比較熟悉的話,你便會明白這個複用的概念,和UITableViewCell的複用機制一樣。
3.如果view爲nil,則創建一個新的進行設置。
4.如果annotation存在,則直接修改view上的annotation。
5.最後返回annotationView
謝謝你能和我一起堅持到這,運行你的代碼,然後移動地圖到北美洲附近,放大地圖,你就可以看到你標記的幾個地點了。
所有大頭針標記的地方都是寶藏哦~~~(這樣子說話好有羞恥感…)
The reduce algorithm - 優化代碼
你可能會感到有些喪氣,當你打開應用的時候並不是直接展示寶藏的位置,還需要你挪動地圖位置,然後放大縮小地圖。在你開始尋寶前你還需要找到自己的位置,你說蛋疼不蛋疼。
這個問題很容易解決,打開ViewController.swift然後在viewDidLoad末尾處添加代碼:
// 1
let rectToDisplay = self.treasures.reduce(MKMapRectNull) {
(mapRect: MKMapRect, treasure: Treasure) -> MKMapRect in
// 2
let treasurePointRect = MKMapRect(origin: treasure.location.mapPoint,
size: MKMapSize(width: 0, height: 0))
return MKMapRectUnion(mapRect, treasurePointRect)
// 3
}
// 4
self.mapView.setVisibleMapRect(rectToDisplay, edgePadding: UIEdgeInsetsMake(74, 10, 10, 10), animated: false)
信不信由你,就這五行代碼就直接讓所有的寶藏顯示在你視野範圍內的地圖中了,那他是如何工作的呢:
1.該算法通過使用一個叫reduce的數組函數。reduce數組意味着函數數組的數組每個元素結合形成一個,最後返回一個返回值。在每個步驟中。下一個元素的數組傳遞減少了當前值的值。函數的返回值就變回了當前降低的值。在這種情況下,你看到的最後一個值是MKMapRectNull.
2.在每一步的減少中,你都是在計算只有一個單一寶藏地圖的封閉矩形。
3.然後返回一個由目前整體矩形以及單一寶藏矩形形成的整體矩形。
4.遞減完成時,地圖矩形將會包含所有寶藏的地圖矩形。換句話說,這個矩形就足以將每個寶藏都包含了。
然後設置的可見的地圖矩形,你使用邊緣間距以確保最近的地圖針不會在導航欄下面或太靠近屏幕的邊緣。(其實上面這個方法不用懂,畢竟本章的重點不在此)
不用手動拖動地圖,放大地圖了,是不是帥斃了!
Note:Reduce
是個典型的函數編程,通過從一個集合中返回迭代計算的值。你會在Swift中經常看到這樣的編程代碼。如果你對方法編程感興趣的話,在第七章中有詳細的介紹。
Polymorphism - 多態
當前所有的大頭針都是同一種顏色。如果每個類型的寶藏類型可以用不同顏色來進行區分表示就好了。這一聽就是多態的工作!
多態,即相同的方法會根據不同的子類有各自不同的反應機制,在這個實例中,很明顯你需要一個返回顏色的方法,每個子類都可以根據自身條件返回對應的顏色。
打開Treasure.swift 並在Treasure類中實現:
func pinColor() -> MKPinAnnotationColor {
return MKPinAnnotationColor.Red
}
這是默認實現的方法,如果不覆蓋這個方法則默認返回一個紅色的大頭針。
接着在HistoryTreasure的類中覆蓋此方法
override func pinColor() -> MKPinAnnotationColor {
return MKPinAnnotationColor.Purple
}
再接着在HQTreasure 類中覆蓋此方法
override func pinColor() -> MKPinAnnotationColor {
return MKPinAnnotationColor.Green
}
在FactTreasure中沒有覆蓋此方法,所以他返回一個默認的顏色。
接着打開ViewController.swift 並找到你在MKMapViewDelegate擴展中實現的方法mapView(_:viewForAnnotation:),在return view語句前加入代碼:
view?.pinColor = (treasure?.pinColor())!
這個方法調用了treasure中的pinColor()並返回設置的顏色。這個方法將根據每個不同的子類執行正確的pinColor()的方法。
編譯並運行,看到界面如下:
是不是更好了些,地圖上的大頭針更好看了~~
Dynamic dispatch and final classes - 動態調用和最終類
多態性的方法需要在運行時查找方法。對於pinColor(),根據子類Treasure的實例來判斷是使用覆蓋的父類方法還是直接使用父類的方法。這種行爲叫動態調用。
在Object-C中每個方法的調用都是動態調用。在大量使用動態調用的語言中,你甚至可以在運行的時候添加方法和類!這便讓編譯器無法在運行的時候做很多的優化處理。
在Swift中允許動態調用多態行爲。Swift用的方法類似與C++,而不是Object-C的消息傳遞調用。他通過虛擬表或“vtables”(虛表)來動態調用。
在上面的例子中。當編譯器調用一個在Treasure的變量類型的pinColor(),他知道要用虛表去查找(動態調用),因爲Treasure的類的子類實現了pinColor().
但是如果編譯器發現的是和HQTreasure這樣子類的變量呢?在這個例子中,編譯器依舊會用vtable動態調用來查找,因爲誰知道這個類是不是添加有某個子類在哪個犄角旮旯。在這個應用中,HQTreasure沒有子類。即使開發員知道這種情況,編譯器也不會嘗試去優化直接調用函數,而是依然嘗試動態調用。爲了優化,處理無用的調用,有方法給編譯器一個提示HQTreasure永遠不會有子類來解決這個問題。
可以用關鍵詞final來聲明一個類不可以擁有子類。任何嘗試在final類中創建子類的編譯器都會返回錯誤。這是一個有用的設計,同時也可以顯著的提高性能。一旦你聲明瞭一個final類,編譯器就明確的知道要實現的方法是當前的實例裏面的。比如,如果你讓HQTreasure聲明爲final然後調用HQTreasure實例的方法pinColor(),編譯器會知道要調用的是HQTreasure版本里的方法,不用再到子類層去尋找。
讓我們來演練下這個測試。打開Treasure.swift並修改Treasure的三個子類爲final。
final class HistoryTreasure: Treasure
final class FactTreasure: Treasure
final class HQTreasure: Treasure
將類聲明爲final是一個很好的操作習慣。有助於編譯器在你代碼運行前的優化,嗯哼!讓你的代碼飛起來︿( ̄︶ ̄)︿。
Adding annotations - 添加地圖標註
現在可以在地圖上看到寶藏分佈的地方,但是還什麼都做不了。沒有任何方式可以進一步瞭解詳細的寶藏信息,所以讓我們來給這個標記點添加些標註。
打開Treasure.swift 並在頂部Treasure類的上面的文件處添加協議
@objc protocol Alertable {
func alert() -> UIAlertController
}
這個協議是Treasure子類都需要遵循的。協議中有一個單獨的方法alert。當用戶調用時返回一個控制器類型的UIAlertController。
在最底部添加擴展:
extension HistoryTreasure: Alertable {
func alert() -> UIAlertController {
let alert = UIAlertController(
title: "歷史",
message: "從 \(self.year):\n\(self.what)", preferredStyle: UIAlertControllerStyle.Alert)
return alert
}
}
extension FactTreasure: Alertable {
func alert() -> UIAlertController {
let alert = UIAlertController(
title: "真相",
message: "\(self.what):\n\(self.fact)", preferredStyle: UIAlertControllerStyle.Alert)
return alert
}
}
extension HQTreasure: Alertable {
func alert() -> UIAlertController {
let alert = UIAlertController(
title: "總部",
message: "\(self.company)的總部", preferredStyle: UIAlertControllerStyle.Alert)
return alert
}
}
每一個方法裏都設置了treasure類型的標題和一些其他相關的信息。注意的是我們使用了字符串的嵌入方法!
現在打開ViewController.swift並在MKMapViewDelegate的擴展後面添加以下代碼:
func mapView(mapView: MKMapView,annotationView view: MKAnnotationView,
calloutAccessoryControlTapped control: UIControl)
{
if let treasure = view.annotation as? Treasure {
if let alertable = treasure as? Alertable {
let alert = alertable.alert()
alert.addAction(
UIAlertAction(
title: "OK",
style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
}
這個代理方法在用戶點擊大頭針或者大頭針頂部欄右邊的感嘆號時被調用。
在這個方法中,你給Treasure的每個view添加了註釋,就像剛剛那樣,然後你現在再次檢查他是否符合Alertable的協議,以便你能獲取到對話框alert。然後在這個對話框中添加一個“ok”按鈕,最後展示出來。
運行程序並點擊大頭針,接着點擊註釋上面右邊的詳細按鈕(感嘆號)。觀察這個treasure的對話框,UI應該和下面差不多:
啊嗚~~~~!這就是寶藏的地址了!
Sorting an array - 排序數組
如果你的用戶可以在發現一個寶藏後能立馬發現接下來的一個最近的寶藏是不是很爽。要實現這個方法其實也很容易。
首先,打開GeoLocation.swift 並在GeoLocation結構中定義以下方法:
func distanceBetween(other: GeoLocation) -> Double {
let locationA = CLLocation(latitude: self.latitude, longitude: self.longitude)
let locationB = CLLocation(latitude: other.latitude, longitude: other.longitude)
return locationA.distanceFromLocation(locationB)
}
這個添加在GeoLocation結構中的方法是用來計算自己和另外一個GeoLocation實例間的距離。他用的是地理的核心庫CLLocation的方法來計算的,畢竟要計算地球上兩點間的距離是非常難的。
注意的是結構可以和類一樣持有方法。這是非常有用的,這與結構中只能有變量的如c語言是明顯不一樣的。因爲c結構不能包含有函數,操作他們的行爲通常需要將函數聲明爲全局。例如,當你在CGRect中的操作時,你需要CGRectUnion,CgRectDivide和CGRectGetMidX。這些功能混亂的佈局在全局中,很難在CGRect中找到所有的功能操作。這些操作工作留給了開發者,需要讓他們自己把所有需要內容都導入到頭文件。所以Swift明顯又高一籌。因爲相關的方法都可以在內部結構中全找到。
現在打開ViewController.swift 並找到實現的方法
mapView:annotationView:calloutAccessoryControlTapped:
,在調用presentViewController: 前添加代碼如下:
alert.addAction(UIAlertAction(
title: "最近的",
style: UIAlertActionStyle.Default) {
action in
// 1
var sortedTreasures = self.treasures
sortedTreasures.sortInPlace {
// 2
let distanceA = treasure.location.distanceBetween($0.location)
let distanceB = treasure.location.distanceBetween($1.location)
return distanceA < distanceB }
// 3
mapView.deselectAnnotation(treasure, animated: true)
mapView.selectAnnotation(sortedTreasures[1], animated: true) })
這在alert上另外再添加了一個方法。新的方法做了如下事情:(瞭解下就行,不需要非得懂)
1.你將要排列所有的寶藏信息,所以你創建了一個本地變量用於複製一份源數組信息。這個排序方法的閉包函數需要一個參數,並返回一個前面兩個對象比對的布爾值。
2.接着,你計算當前寶藏與你排序的每一個寶藏信息的距離。注意這裏使用了
你檢查第一個地點的距離與第二個的距離,如果第一個小則返回true。通過這種方式,你的寶藏信息便會以到當前寶藏地點的最短到最長的順序進行排序。
3.最後你點擊後會取消當前的寶藏並會選擇新的寶藏地點。你可能會問爲什麼會選擇最近的地點時選擇的是數組中的第二個元素,因爲的第一個元素永遠是自己。
編譯並運行程序,選擇一個大頭針並點擊調出信息展示,接着點擊“Find Nearest”。這個app會尋找到接下來的最近的一個寶藏地點!
Equality and operator overload - 等號和運算符重載
用戶發現了寶藏地點,但卻沒有記錄保存他們是否來過。用一個簡單的方法來記錄,顯示一個覆蓋在地圖上的標記用來記錄用戶到過地點的路線。如果發現用戶去過了這個 地方,則這個應用彈出提示。
打開ViewController.swift 並在類的頂部添加屬性,就添加在treasures數組屬性的下面:
var foundLocations: [GeoLocation] = []
var polyline: MKPolyline!
第一個屬性用來保存GeoLocation 結構的一系列信息,以便app用來記錄跟蹤用戶發現的寶藏和順序。
第二個屬性保存MKPolyline。這是覆蓋在地圖上的點的集合信息。用戶可以通過這個屬性看到他們發現的每個寶藏的路徑。這個屬性的類型是隱式解包。也就是說,在用戶發現任何寶藏信息前,可以用nil來表示。
接着,找到MKMapViewDelegate 擴展,並在底部添加代碼如下:
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer{
let polylineOverLay = overlay as? MKPolyline
if polylineOverLay != nil {
let renderer = MKPolylineRenderer(polyline:polylineOverLay!)
renderer.strokeColor = UIColor.blueColor()
return renderer
}
//原來的代碼是可以直接返回nil,但是現在返回類型值必須存在,所以這裏隨意模擬一個值
let renderer = MKPolylineRenderer(polyline: polylineOverLay!)
return renderer
}
這個方法用來告訴地圖如何渲染給定的路線。通過使用MKPolyline來關聯叫做MKPolylineRenderer的渲染器。
現在找到
mapView:annotationView:calloutAccessoryControlTapped:
就像你前面剛剛加的對話類型一樣,在兩個彈出的語句見添加代碼:
alert.addAction(UIAlertAction(
title: "發現",
style: UIAlertActionStyle.Default) { action in
self.markTreasureAsFound(treasure) })
這個方法即在用戶發現了寶藏的時候觸發。在viewController的類裏面添加下面方法:
func markTreasureAsFound(treasure: Treasure) { // 1
if let index = self.foundLocations.indexOf(treasure.location)
{ // 2
let alert = UIAlertController( title: "Oops!",message: "你已經來過這裏(在第\(index + 1)步)! 再試一次!",
preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK",
style: .Default,
handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
} else { // 3
self.foundLocations.append(treasure.location)
// 4
if self.polyline != nil { self.mapView.removeOverlay(self.polyline)
}
// 5
var coordinates = self.foundLocations.map { $0.coordinate }
self.polyline = MKPolyline(coordinates: &coordinates,count: coordinates.count)
self.mapView.addOverlay(self.polyline)
}
}
下面介紹了這個方法都做了些什麼:(函數的具體實現不用特別瞭解)
1.首先,你先用indexOf方法判斷locations數組中是否有寶藏信息。這個indexOf會遍歷數組返回可選類型的信息,有值則是對應的索引,如果沒有找到則返回nil。可選類型的這個特點很棒,就像你看到的那樣,代碼是不是特別易讀。
2.如果在locations的數組中已經存在了location。那麼則彈出對話框顯示用戶在這一步發現了寶藏。
3.如果locations數組中沒有這個location,則將其添加到數組中。
4.然後,如果某條路線已經存在了,你需要將他從地圖中移除。如果不移除的話,每次發現都會重新繪製一個新的線條在上面。
5.最後,你創建一個新的MKPolyline並添加到地圖視圖中。注意使用map函數的數組。這個函數爲數組的每個元素都提供了創建一個新的數組結果的閉包函數。上面的實例使用了簡短的閉包函數,因爲Swift可以map函數前實現自動推斷。數組的每個元素都傳遞到閉包函數直到變量爲$0.
這個時候你還會發現在indexOf()使用的地方報錯。猜猜爲什麼。這個方法需要掃描數組的每個元素,它使用相等操作符==進行匹配,你不能對類和結構體使用這個匹配符號。
接着來講講符號重載,先定義Equatable等號協議,因此,你需要讓GeoLocation循着Equatable協議。
需要定義一個如下這種協議
protocol Equatable {
func ==(lhs: Self, rhs: Self) -> Bool
}
你需要實現一個方法。但是這個方法名叫==。這看上去非常奇怪,如果你是Object-C開發員的話,可能會看到過類似的的isEqual。
這是另一個Swift有別於Object-C的地方,有一種被稱爲運算符重載的神奇用法。不是用一種特殊的方法讓你進行相同判斷,而是Swift讓你重載==操作符進行相同判斷。
打開GeoLocation.swift 並在最底部添加:
extension GeoLocation: Equatable { }
func ==(lhs: GeoLocation, rhs: GeoLocation) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
擴展的通常方法是聲明協議的一致性,但是注意==函數沒有在擴展內部。你必須將其聲明爲全局範圍的運算符重載,因爲他們不是部署在某一個類的方法,而是屬於你使用==運算符的任何地方。他們只是與類相關聯,關聯兩個類的實例參數用於比較。實現很簡單:檢查兩個經度和緯度是否相同,並返回布爾值。
Access Control - 訪問控制
目前,所有的方法和變量都被聲明在類和結構中,默認是public。這意味着任何其他地方的代碼都可以進行調用。通過訪問控制修飾符,Swift提供了靈活的方法以便你訪問每個屬性,方法等。
下面是三個級別的訪問控制符:
Public:誰都可以訪問。
Internal:只有在相同的target(庫或app)中的其他代碼可以訪問。這是默認的訪問級別。
Private:只有同一個源文件的代碼可以訪問。
使用訪問控制有助於代碼維護。例如,您可以在你的類中使用很多的輔助方法,但你並不想公開暴露出來,因爲這些不同的狀態類應該對用戶是隱藏的。這些可以被標記爲private用於阻止用戶使用這些方法。
internal的訪問級別很適合在庫中限制。通常你的類有的方法也都是不需要在其他的庫中使用。如果這些方法被暴露在庫外,則其他的使用者就可以看到內部的數據結構之類的。很明顯你不想將這些庫中的方法暴露在外。因爲internal是默認級別的,如果你要訪問外部的庫,你可能需要用public來應用於對應的類,結構,枚舉等,讓其可以被任何類,結構等訪問。
在應用程序中,internal訪問級別也通常是你想使用的(方便,並且是默認的)。應用代碼不會被別的庫中的代碼使用,所以他也不需要訪問本身以外的任何東西。也就是說,單元測試意味着公衆是可以使用的,單元測試通常是一個單獨的目標,要訪問任何隱藏的單元測試都需要先將其聲明爲public。
Object-C開發者樂於學習Swift,私有方法確實是私有方法。不會在運行的時候讓你有任何的後門去訪問不該訪問的私有方法。
讓我們看看在應用中的行動。
修改方法聲明如下:
private func markTreasureAsFound(treasure: Treasure)
編譯並運行,每件事都和剛剛代碼的運行情況一樣。沒錯,確實沒變化!
視圖控制器下也有三個屬性。這些都是本地狀態值,所以不應該從外部訪問。繼續改變他們的聲明如下:
private var treasures: [Treasure] = []
private var foundLocations: [GeoLocation] = []
private var polyline: MKPolyline!
再次編譯運行,依然正常工作,但是外界卻再也無法聯繫到這些屬性!
internal修飾符在應用中有些冗餘,只有在多個應用中分享庫時使用他纔會有意義。然而,想象你創建一個TreasureHunt庫,打包所有的代碼,以便讓另外一個應用可以顯示這個相同的尋寶遊戲app。這種情況下,你需要標記Treasure和GeoLocation爲internal,以便他們不被其他應用調用。只有ViewController類可以被使用。這種情況下通常會重命名TreasureHuntViewController!
訪問權限修飾符對於聲明很有幫助,儘可能使用Private。這樣你只需要公開核心的api使用對象。這樣做可以減少更多的錯誤並提高代碼的維護性。你重構私有方法也可以不用擔心破壞了外部的api。恭喜你,這個應用你建立完了。祝你尋寶好運!
Where to go from here? - 接着幹什麼呢
在這一章中,你瞭解了不少類和結構的內容。你創建了你自己的類的層級並看到了如何擴展類和結構。你瞭解了類和結構之間的差異,以便你能正確的使用。你也學會了動態調用以及算術符的重載等。
此外,你還開發了一個很酷的涉及到硅谷的應用。這也許是你的第一個ios應用程序,如果是這樣,你該給自己個大大的紅花,然後站在勝利的舞臺上。熱淚盈眶,感謝國家,感謝爸媽!感動中國,感動你我!
雖然這一章給你講解了很多的基礎概念,但是這本書的其餘部分還會有涉及到類和結構。你會在更多的真實用例中看到如何使用類和結構,在後面,你還會了解到如何通過使用泛型讓編程更加強大。
經驗豐富的Object-C開發員也會對如何讓Swift與Object-C直接關聯進行混編感興趣,這些我們後面都會有涉及到。