# swiftUI 基礎語法
來源:https://www.hackingwithswift.com/100/swiftui
## 第一天 variables, simple data types, and string interpolation
### variables 變數
變數的定義
```swift
var str = "Hello, playground"
```
變數的改值
```swift
str = "Goodbye"
```
### Strings and integers 字串和整數
定義整數變數
```swift
var age = 38
```
如果需要的話可以用底線當千位分隔符
```swift
var money = 8_000_000 // 等於 8,000,000的意思
```
多行字串,用前後各三個引號包住
```swift
"""
This goes
over multiple
lines
"""
```
輸出效果
```
This goes
over multiple
lines
```
如果在行尾加入 \ 顯示的時候不會分行
```swift
"""
This goes \
over just one \
line
"""
```
輸出效果
```
This goes over multiple lines
```
### Doubles and booleans 雙精度浮點數和布林值
在swift 中,浮點數是用 double表示
double是double-precision floating-point number 的簡稱,
floating-point number 是用32bits代表一個數
double顧名思義是用64bits代表一個數
```swift
var pi = 3.141
```
布林值 booleans 可以表示真偽
只有兩種值,true 和 false
```swift
var awesome = true
```
### String interpolation 字串插值
字串中可以塞入變數
方法如下
```swift
var score = 85
var str = "Your score was \(score)"
//輸出: Your score was 85
```
### constant 常數
swift 中可以定義常數
常數和變數的差別就是常數一旦定義就不能再改值了
```swift
let taylor = "swift"
```
### type annotation 型別宣告
在宣告常數或變數時swift 都會認定一個型別
例如
```swift
let str = "swift"
```
那麼str 就是一個string型態的常數
不能再被指定為其他型別
你可以在指定變數或常數時直接把型別寫出來
```swift
let album: String = "Reputation" //字串
let year: Int = 1989 //整數
let height: Double = 1.78 //浮點數
let taylorRocks: Bool = true //布林值
```
## 第二天 Complex data types 複數資料型態
### arrays 陣列
陣列可以儲存一群資料
定義方式如下
```swift
let john = "John Lennon"
let paul = "Paul McCartney"
let george = "George Harrison"
let ringo = "Ringo Starr"
let beatles = [john, paul, george, ringo]
```
可以用index值來取得陣列中的某筆資料
index值從0開始算,陣列的第一筆資料index 是0不是1
```swift
beatles[1] // Paul McCartney
```
取值的時候index值不可以超出既有範圍
### sets 集合
集合跟陣列很像,也是儲存一群資料,但有兩個不同點:
1.集合內的元素沒有順序性,所以沒有index
2.集合內每個元素都獨一無二,不能重複
集合的定義方式如下
```swift
let colors = Set(["red", "green", "blue"])
```
如果你塞了重複的東西進去集合內
他會把重複的東西自動忽略
```swift
let colors2 = Set(["red", "green", "blue", "red", "blue"])
//這個colors2還是只有 red, green blue三項
```
### tuples 元組
這也是跟arrays很像的東西,但還是有點不同:
1.你不能新增或刪除tuple裡面的元素,tuple的大小是固定的
2.你不能改變tuple中元素的資料型態,但你可以改值
3.你可以用index或是名字來取得元素的值,但必須要在定義的範圍內
以下是一個例子
```swift
var name = (first: "Taylor", last: "Swift")
```
你可以這樣取值:
```swift
name.0 // Taylor
name.first //Taylor
```
### dictionaries 字典
字典一樣儲存了一群資料,但是用key:value的方式儲存的
```swift
let heights = [
"Taylor Swift": 1.78,
"Ed Sheeran": 1.73
]
```
可以用key來取值
```swift
heights["Taylor Swift"] //1.78
```
要標註dictionary的資料型態的話可以這樣寫
```swift
let heights: [String:Double] = [
"Taylor Swift": 1.78,
"Ed Sheeran": 1.73
]
```
如果你用了不存在的key取值,dictionary會回給你 nil
就相當於其他語言的None 或 null,什麼都沒有的意思
在某些地方nil返回值可能帶來錯誤
但你可以透過定義default來改變這個預設的返回值
```swift
let favoriteIceCream = [
"Paul": "Chocolate",
"Sophie": "Vanilla"
]
```
如果用
```swift
favoriteIceCream["Charlotte"]
```
會得到 nil,因為沒有"Charlotte這個key"
但如果用
```swift
favoriteIceCream["Charlotte", default: "Unknown"]
```
就會得到 Unknown而不是 nil
### 建立空的collections
上述的容器式資料型態在定義時都可以不給內容
只建立一個空的容器
```swift
var teams = [String: String]() //空字典
var results = [Int]() //空陣列
var words = Set<String>() //空集合
var scores = Dictionary<String, Int>() //空字典另一種寫法
var results = Array<Int>() //空陣列另一種寫法
```
### Enumerations 列舉
enumerate 簡寫為 enum
定義方式如下
```swift
enum Result {
case success
case failure
}
let result4 = Result.failure
```
enum可以列舉有限的狀況
在使用的時候只能從這些狀況去挑選
可以避免意外情況造成的錯誤
enum裡面的case可以在定義更細部的條件
如以下範例
```swift
enum Activity {
case bored
case running(destination: String)
case talking(topic: String)
case singing(volume: Int)
}
let talking = Activity.talking(topic: "football")
```
這樣我們可以知道 talking 變數代表的是在討論足球的話題
有時候你會需要指定數值給enums來讓他們有含義,這讓你可以動態建立並以不同方式使用他們
舉例來說你可以建立一個Planet的enum來讓其中每個case儲存整數值
```swift
enum Planet: Int {
case mercury
case venus
case earth
case mars
}
```
swift會從0開始自動指定一個整數到每一項上面
以上面例子來說
mercury == 0
venus == 1
earth == 2
mars == 3
你可以指定特定的數字到case中,其他的數字會自動被指定
```
enum Planet: Int {
case mercury = 1
case venus
case earth
case mars
}
```
像以上的例子,venus會自動指定為2
earth=3,以此類推
## 第三天 Operators and conditions 運算子與條件
運算符號
```swift
12 + 4 //加
12 - 4 //減
12 * 4 //乘
12 / 4 //除
12 % 4 //取餘數
```
運算子多載 operator overloading
```swift
"+" 這個符號也可以用在字串跟array上面
//字串
let fakers = "Fakers gonna "
let action = fakers + "fake"
//陣列
let firstHalf = ["John", "Paul"]
let secondHalf = ["George", "Ringo"]
let beatles = firstHalf + secondHalf
```
但是你不能把不同型別的資料相加
compound assingment operators
a += b => a = a + b
a *= b => a = a * b
a -= b => a = a - b
a /= b => a = a / b
比較運算子
```swift
a == b // a 等於 b
a > b // a 大於 b
a >= b // a 大於等於b
a < b // a 小於b
a <= b // a 小於等於b
a != b // a 不等於 b
```
### conditions 條件判斷
```swift
let firstCard = 11
let secondCard = 10
if firstCard + secondCard == 2 {
print("Aces – lucky!")
} else if firstCard + secondCard == 21 {
print("Blackjack!")
} else {
print("Regular cards")
}
```
如果firstCard + secondCard 等於2
就會印出 Aces – lucky!
否則如果firstCard + secondCard 等於21
就會印出 Blackjack!
否則就會印出 Regular cards
combine conditions
條件之間可以用"且"(and)跟"或"(or)來連結
且的符號是 &&
或的符號是 ||
ternary operator 三元運算子
"?" 的前面是條件,?的後面是要執行的程式
要執行的程式又分兩部分
":" 前面是條件成立的情況
":" 後面是條件不成立的情況
```swift
let firstCard = 11
let secondCard = 10
print(firstCard == secondCard ? "Cards are the same" : "Cards are different")
```
上面這個print的意思就是:
如果firstCard 等於 secondCard 印出 Cards are the same
否則印出 Cards are different
### Switch宣告
有很多種狀況的時候除了用if, else if, else之外
還有switch可以用
看起來會比較清楚
```swift
switch weather {
case "rain":
print("Bring an umbrella")
case "snow":
print("Wrap up warm")
case "sunny":
print("Wear sunscreen")
default:
print("Enjoy your day!")
}
```
記得 default 的條件一定要設定
switch當中只有一個case會被執行
但是關鍵字fallthrough
能夠讓某個case執行完後再執行他的下一個case
### range operator
swift 中表示範圍可以用
```swift
0..<50 //0~49
1...60 //1~60
```
兩種方式表示,差別在於有沒有包含最後一個數
## 第四天 loops 迴圈
for 迴圈
```swift
for number in 1...10{
print("Number is \(number)")
}
let albums = ["Red", "1989", "Reputation"]
for album in albums {
print("\(album) is on Apple Music")
}
```
while 迴圈
```swift
var number = 1
while number <= 20 {
print(number)
number += 1
}
print("Ready or not, here I come!")
```
repeat迴圈
跟while很像,但是把停止條件的檢查移到程式後面
相當於其他語言的do while
```swift
var number = 1
repeat {
print(number)
number += 1
} while number <= 20
print("Ready or not, here I come!")
```
### break the loop
break 關鍵字可以跳出當下的迴圈
如果用到巢狀迴圈的時候可以定義outerLoop
並且用break outerLoop 一次跳離巢狀回圈
```swift
outerLoop: for i in 1...10 {
for j in 1...10 {
let product = i * j
print ("\(i) * \(j) is \(product)")
if product == 50 {
print("It's a bullseye!")
break outerLoop
}
}
}
```
continue關鍵字可以跳過回圈的某一次執行,直接執行下一次動作
```swift
for i in 1...10 {
if i % 2 == 1 {
continue
}
print(i)
}
```
輸出:
2
4
6
8
10
### 無限迴圈 infinite loop
while迴圈的條件放入 true
就會讓迴圈一直跑下去
所以要記得設停止條件
```swift
var counter = 0
while true{
print(" ")
counter += 1
if counter == 273{
break
}
}
```
## 第五天 functions 函式
### function的定義與使用
```swift
func printHelp() {
let message = """
Welcome to MyApp!
Run this app inside a directory of images and
MyApp will resize them all into thumbnails
"""
print(message)
}
// 呼叫函式
printHelp()
```
定義函式時,可以設定參數,在呼叫時也要給予對應的參數
```swift
func square(number: Int) {
print(number * number)
square(number: 8)
```
要注意的是呼叫時要連參數的名稱一起寫出來
對於有回傳值的函式,不只要宣告參數的資料型態
也要宣告回傳值的資料型態
方法如下
```swift
//定義
func square(number: Int) -> Int {
return number * number
}
//呼叫
let result = square(number: 8)
print(result)
```
### 參數標籤 parameter label
參數可以有兩個名字
如以下例子
```swift
func sayHello(to name: String){
print("hello, \(name)!")
}
sayHello(to: "Taylor")
```
sayHello 的參數有兩個名字 to 跟 name
to 是外部名稱, name是內部名稱
在fucntion定義中使用參數時用name
在外部呼叫function時同一個參數變成to
這樣的好處是增加可讀性
在內部時通常名稱是名詞
在外部時是動詞或介詞
也可以用底線取代外部名稱,這樣在呼叫時就可以不用填參數名稱了
```swift
func sayHello(_ name: String){
print("hello, \(name)!")
}
sayHello("Taylor")
```
但是為了閱讀方便,所以一般建議
還是要有外部名稱比較好
### 預設參數 default parameter
在定義函式時可以直接給一個預設值
這樣在呼叫時如果沒有給那個參數
參數就會直接帶入預設值
像以下的例子 nicely 有預設值 true
所以呼叫時如果沒有給nicely參數,就會直接帶入true
```swift
func greet(_ person: String, nicely: Bool = true) {
if nicely == true {
print("Hello, \(person)!")
} else {
print("Oh no, it's \(person) again...")
}
}
greet("Taylor") // Hello, Taylor
greet("Taylor", nicely: false) // Oh no, it's Taylor again...
```
### 可變參數函數 variadic function
當我們不確定函式輸入的參數會有幾個時
可以定義可變參數函式,形式如下
```swift
func square(numbers: Int...) {
for number in numbers {
print("\(number) squared is \(number * number)")
}
}
```
*numbers: Int...* 表示參數是整數,數量有多少不確定
呼叫時用逗號把每個參數隔開
```swift
square(numbers: 1, 2, 3, 4, 5)
```
輸出:
```
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
```
### 錯誤處理函式 throwing functions
有時候函式會因為錯誤的輸入或內部問題而出現錯誤
swift 讓我們可以從函式丟出錯誤,在返回值之前標記throws
然後在出現錯誤時使用throw關鍵字
首先要定義一個enum來描述我們可以拋出的錯誤
拋出的錯誤必須根據swift已經存在的錯誤型態
下面我們寫一個檢查密碼是否合規的程式
如果使用者嘗試太明顯的密碼,就丟出一個錯誤
```swift
enum PasswordError: Error {
case obvious
}
```
接著來寫checkPassword()函式
在返回值之前使用 throws關鍵字
如果使用者輸入的密碼是"password"
就會丟出一個錯誤
```swift
func checkPassword(_ password: String) throws -> Bool {
if password == "password" {
throw PasswordError.obvious
}
return true
}
```
swift 不希望程式運行時發生錯誤
這意味著他不會讓你意外地運行引發錯誤的函式
相反的,你必須使用三個新的關鍵字去呼叫錯誤函式
用 do 開始一個錯誤處理的程式區塊
try放在可能出錯的程式前
catch負責處理錯誤發生時的動作
如以下範例
```swift
do {
try checkPassword("password")
print("That password is good!")
} catch {
print("You can't use that password.")
}
```
上面的程式執行時,會印出You can't use that password
因為checkPassword("password")引發了obvious錯誤
### inout parameters
所有傳入函式的參數都是常數,也就是你不能改變它
如果你想讓變數傳入函式後能夠被改變,可以用inout關鍵字
如果外部變數被當成參數傳入
inout 會讓函式內的改變直接影像變數原本的值
舉例來說我們有一個把參數變兩倍的函式
```swift
func doubleInPlace(number: inout Int){
number *= 2
}
```
如果我們將一個變數myNum傳入,那麼doubleInPlace會直接影響myNum的值
```swift
var myNum = 10
doubleInPlace(number: &myNum)
```
經過doubleInPlace作用後myNum就會變成20
注意這裡myNum前面加了一個 '&' 符號
這明確標示出 myNum被用作 inout
## 第六天 第七天 closure 閉包
swift 讓我們像其他資料型態一樣的使用函式
這表示你可以建立一個函式並指定給一個變數
並且用這個變數呼叫函式,甚至把函式作為參數傳遞給另一個函式
這種用法稱為閉包,閉包的寫法和函式有點不一樣
以下是一個簡單的飯
```swift
let driving = {
print("I'm driving in my car")
}
```
這樣我們就建立了一個函式driving
並且可以用driving() 使用這個函式
這個函式會印出 "I'm driving in my car"
### 閉包的參數
如果要讓閉包能接受參數也有一個不同的寫法
參數的位子會轉移到大括號的內部,並加上 in關鍵字
比較一下兩者不同
```swift
// closure的寫法
let driving = {(place: String) in
print("I'm going to \(place) in my car")
}
// function的寫法
func driving(place: String){
print("I'm going to \(place) in my car")
}
//呼叫的方法都一樣
driving("London")
```
### 從 closure返回值
closure跟 function 一樣可以返回值,也一樣要宣告返回值的型態
寫法也很像,把返回值的型態宣告在參數後面
以下比較一下兩種寫法的不同
```swift
// closure
let drivingWithReturn = { (place: String) -> String in
return "I'm going to \(place) in my car"
}
// function
func drivingWithReturn(place: String) -> String{
return "I'm going to \(place) in my car"
}
```
### closure作為參數
closure可以像字串,整數一樣的被使用
所以也能夠作為參數傳入函式忠
以下是我們的driving
```swift
let driving = {
print("I'm driving in my car")
}
```
接下來我們定義一個 travel 函式
參數的位置用 () -> Void 表達不接受參數,也沒有返回值
```swift
func travel(action: () -> Void) {
print("I'm getting ready to go.")
action()
print("I arrived!")
}
```
呼叫時就可以把driving填入action中
```swift
travel(action: driving)
```
### trailing closure syntax
當function的最後一個參數是closure時 swift 讓你可以用特殊語法
trailing closure
以下是travel函式的定義
```swift
func travel(action: () -> Void) {
print("I'm getting ready to go.")
action()
print("I arrived!")
}
```
因為他的最後一個參數是closure,我們可以用 trailing closure
的語法,例如:
```swift
travel(){
print(I' mdriving in my car)
}
```
因為travel沒有其他參數了,所以travel後面的小括號也是可以省略的
```swift
travel {
print("I'm driving in my car")
}
```
帶參數的closure 作為function參數
```swift
func travel(action: (String) -> Void) {
print("I'm getting ready to go.")
action("London")
print("I arrived!")
}
travel { (place: String) in
print("I'm going to \(place) in my car")
}
```
帶參數且有回傳值的closure作為function參數
```swift
func travel(action: (String) -> String) {
print("I'm getting ready to go.")
let description = action("London")
print(description)
print("I arrived!")
}
travel { (place: String) -> String in
return "I'm going to \(place) in my car"
}
```
closure參數簡寫
```swift
// 本來長這樣
travel { (place: String) -> String in
return "I'm going to \(place) in my car"
}
// swift 知道參數跟回傳都是String所以可省略
travel { place in
return "I'm going to \(place) in my car"
}
// 因為只有一行,所以 return 對象一定就是他,return也可以省
travel { place in
"I'm going to \(place) in my car"
}
// 最後連參數名字都可以不取,讓swift自己幫你認
// 用 "$0" 代替 place
travel {
"I'm going to \($0) in my car"
}
```
很多參數的closure
```swift
func travel(action: (String, Int) -> String) {
print("I'm getting ready to go.")
let description = action("London", 60)
print(description)
print("I arrived!")
}
travel {
"I'm going to \($0) at \($1) miles per hour."
}
```
讓function 返回 closure
底下的例子用了兩個 '->'
第一個是funciton的返回值
第二個是closure的返回值
```swift
func travel() -> (String) -> Void {
return {
print("I'm going to \($0)")
}
}
// 用法
let result = travel()
result("London")
// 這也是可以的,但不建議這麼寫
let result2 = travel()("London")
```
captureing values
如果你在閉包的內部使用任何外部值
swift會將他們存在閉包旁
因此即使那們不再存在也可以對他們進行修改
我們在travel裡面加入一個變數counter
```swift
func travel() -> (String) -> Void {
var counter = 1
return {
print("\(counter). I'm going to \($0)")
counter += 1
}
}
let result = travel()
result("London")
result("London")
result("London")
```
我們呼叫三次result
每次執行時都返回一個新的閉包
但是因為都是來自 travel 函式
switch 會儲存counter的值
所以counter會隨著每次呼叫一直累加上去
因此輸出是這樣子:
```
1. I'm going to London
2. I'm going to London
3. I'm going to London
```
## 第八天,第九天 structs
### 建立 struct
swift 提供我們兩種方法建立自訂的資料型態
第一種是struct
struct 可以擁有自己的變數或常數屬性,並且擁有自己的函式
下面是一個簡單的struct範例,含有一個 name屬性,儲存字串型態的資料
```swift
struct Sport{
var name: String
}
```
這定義了一個Sport的資料型態
有了定義後就可以建立Sport 型態的實體
```swift
var tennis = Sport(name: "Tennis")
print(tennis.name)
```
因為 Sport 中的 name屬性 是變數,所以能像一般變數一樣改值
```swift
tennis.name = "Lawn tennis"
```
### computed properties
前面例子中的 name 在建立實體時直接給值,稱為 stored property
swift 裡面有另一種屬性稱為 computed property
computed property的值不直接指定,而是用程式來判斷
以下的例子 name和 isOlympicSport是 stored property
olympicStatus 是 computed property
```swift
struct Sport {
var name: String
var isOlympicSport: Bool
var olympicStatus: String {
if isOlympicSport {
return "\(name) is an Olympic sport"
} else {
return "\(name) is not an Olympic sport"
}
}
}
```
可以看出 OlympicStatus 的值受到 isOlympicStatus 影響
### property observers 屬性監視器
property observers 讓你可以在任何屬性更改之前或之後運行程式
下面的例子編寫一個Progress結構來監視任務和完成百分比
didSet 這個 property observer 可以在每次amount改變時
將amount印出來
```swift
struct Progress {
var task: String
var amount: Int {
didSet {
print("\(task) is now \(amount)% complete")
}
}
}
var progress = Progress(task: "Loading data", amount: 0)
progress.amount = 30 // Loading data is now 30% complete
progress.amount = 80 // Loading data is now 80% complete
progress.amount = 100 // Loading data is now 100% complete
```
你也可以用 willSet 在 property改變之前執行一些程式
但通常不會這麼用
### methods 方法
struct 可以有自己的函式,這邊稱作方法
定義方法的方式和一般的函式一樣,只是在struct 裡面
以下是一個City範例
```swift
struct City {
var population: Int
func collectTaxes() -> Int {
return population * 1000
}
}
```
因為方法隸屬於 struct ,我們呼叫時要用struct.method()的形式
如下例子:
```swift
let london = City(population: 9_000_000)
london.collectTaxes()
```
### mutating
如果一個 struct 擁有變數的屬性但實體被建立時宣告為常數
那麼屬性將無法被改變 -- 因為struct是常數所以所有隸屬於他的值都不可變動
當建立 struct時 swift 不確定你會將它作為常數或變數
所以 struct當中的方法預設是不能更改內部屬性的
如果想要讓 struct 的方法能改變內部的屬性
必須用 mutating 關鍵字來標記他
以下是一個例子
```swift
struct Person {
var name: String
mutating func makeAnonymous() {
name = "Anonymous"
}
}
var person = Person(name: "Ed")
person.makeAnonymous()
```
### 字串的方法和屬性
其實 字串就是一個 struct
所以我們可以用一些swift提供給我們的屬性和方法
來進行搜尋和操作
我們先建立一個測試字串
```swift
let string = "Do or do not, there is no try."
// 可以用count 屬性查詢字串有幾個字
print(string.count) // 30(空白也算一個字)
// 全部轉成大寫
print(string.uppercased()) // DO OR DO NOT, THERE IS NO TRY.
// 排序
print(string.sorted()) // [" ", " ", " ", " ", " ", " ", " ", ",", ".", "D", "d", "e", "e", "h", "i", "n", "n", "o", "o", "o", "o", "o", "r", "r", "r", "s", "t", "t", "t", "y"]
// 檢查是否以某個字開頭
print(string.hasPrefix("Do")) // true
```
### 陣列的屬性與方法
array 一樣是一個struct
所以也提供了很多屬性和方法給我們使用
```swift
var toys = ["Woody"]
// 印出 array長度
print(toys.count) // 1
// 增加一項
toys.append("Buzz") // ["Woody", "Buzz"]
// 返回符合條件的第一項的 index
toys.firstIndex(of: "Buzz") // optional(1)
// 按字母順序排序
print(toys.sorted()) ["Buzz", "Woody"]
// 刪除第一項
toys.remove(at: 0)
```
更多的屬性和方法可以參考文件,或是在xcode中打toys.
這系統會跳出可用方法和屬性的提示
### initializers 初始化器
初始化器是 struct 的特殊方法,在建構實體時會有預設的初始化器被呼叫
例如我們有個名為User 的struct
```swift
struct User {
var username: String
}
var user = User(username: "twostraws")
```
初始化器會強制每個屬性在建立實體時都有被指定值
我們可以寫出自己的初始化器來取代預設的
以下是一個例子:
```swift
struct User {
var username: String
init() {
username = "Anonymous"
print("Creating a new user!")
}
}
```
init 的前面不需要 func 關鍵字,
但你需要確認所有屬性在init()結束前都有了值
在這裡每個 User 實體被建立時 username都被指定爲 Anonymous
### 使用目前實體的變數
在方法中有個特殊的常數 self,指向了目前正在使用的 struct實體
在定義 initializer時,self可以給我們很多幫助
當你的 initializer接受名稱和屬性一樣的參數時, self可以
標記出屬於 struct的屬性
以下是個例子:
```swift
struct Person {
var name: String
init(name: String) {
print("\(name) was born!")
self.name = name
}
}
```
init 的參數名稱也是name
在init()中, self.name表示Person的屬性
後面的 name則表示init()接收的參數 name
### Lazy properties
基於效能考量,swift 讓你只有在需要時建立一些屬性
舉例來說,考慮一個家庭樹 struct
理論上建立一個家庭樹曠日費時
```swift
struct FamilyTree {
init() {
print("Creating family tree!")
}
}
```
我們可以把 FamilyTree作為屬性放入Person
```swift
struct Person {
var name: String
var familyTree = FamilyTree()
init(name: String) {
self.name = name
}
}
var ed = Person(name: "Ed")
```
每次建立一個Person實體,都會連帶建立一個 FamilyTree實體
但如果我們不需要針對每個人都建立家庭樹呢?
這時候我們可以加上 lazy關鍵字
```swift
lazy var familyTree = FamilyTree()
```
這樣 swift只有在這個屬性第一次被取值的時候才會建立出 FamilyTree實體
### static 屬性和方法
每個struct的實體都有自己的屬性,彼此獨立,互不影響
但如果想要建立能在實體之間分享的資料呢?
這時候就需要 static關鍵字了
被 static標注的屬性或方法可以在同個struct的不同實體中共享
在下面的例子中我們加入一個 static 屬性 classSize
```swift
struct Student {
static var classSize = 0
var name: String
init(name: String) {
self.name = name
Student.classSize += 1
}
}
```
因為classSize現在是屬於struct而不是單屬於某個實體了
所以在取值的時候要用struct的名稱標注他
```swift
print(Student.classSize)
```
接下來每次建立實體classSize就會增加
```swift
let ed = Student(name: "Ed") // classSize = 1
let taylor = Student(name: "Taylor") // classSize = 2
let justin = Student(name: "Justin") // classSize = 3
```
### 取用控制
private 關鍵字可以讓被標注的屬性或方法無法被 struct的外部讀取
只能透過內部的方法取值或改值
```swift
struct Person {
private var id: String
init(id: String) {
self.id = id
}
func identify() -> String {
return "My social security number is \(id)"
}
}
```
上面的例子 id 只能被 identify()取用
如果從 struct外對 Person.id取值就會引發錯誤
## 第十天 class and inheritance 類別與繼承
class 和 struct很像,都允許你建立自定義的資料型態
但仍然有五點不同,第一點是class沒有預設的初始化器
所以每次當你定義一個 class時都應該定義一個屬於它的init()
舉例來說
```swift
class Dog {
var name: String
var breed: String
init(name: String, breed: String) {
self.name = name
self.breed = breed
}
}
```
建立類別實體(又稱為物件)的方式和struct也是一樣的
```swift
let poppy = Dog(name: "Poppy", breed: "Poodle")
```
### 類別的繼承
class 和 struct 第二個不同點就是類別能夠繼承既有的類別
假設我們有個Dog 類別
```swift
class Dog {
var name: String
var breed: String
init(name: String, breed: String) {
self.name = name
self.breed = breed
}
}
```
接著定義一個類別 Poodle,繼承 Dog
Poodle是 Dog的子類別
Dog 是 Poodle 的父類別
```swift
class Poodle: Dog {
init(name: String) {
super.init(name: name, breed: "Poodle")
}
}
```
Dog 所擁有的屬性和方法,Poodle也都會有,包括init()方法
但Poodle也能覆寫 Dog的方法,產生專屬於自己的版本
像上面的例子就把 Dog的 init()改寫了
為了安全起見,子類別的 init() 必須呼叫父類別的init() (super.init)
以免父類別有些重要的設定沒有被繼承到
### overriding methods 方法覆蓋
子類別的方法可以覆蓋父類別的同名方法
```swift
class Dog {
func makeNoise() {
print("Woof!")
}
}
class Poodle: Dog {
override func makeNoise() {
print("Yip!")
}
}
let poppy = Poodle()
poppy.makeNoise() // Yip!
```
### Final classes
swift提供了一個關鍵字 final,讓你可以阻止別人繼承你的類別
被標注 final 的類別不能被繼承,也就不能被覆寫
如果別人要用這個類別就必須依照你最初定義的方式使用
```swift
final class Dog {
var name: String
var breed: String
init(name: String, breed: String) {
self.name = name
self.breed = breed
}
}
```
### copying objects 複製物件
class 與 struct的第三個不同是他們如何被複製
當你複製一個struct的實體,原來的和複製的兩個struct是不同的東西
也就是說改變其中一個的值不會影響另一個
然而當你複製一個class的實體,複製的和原來的指向同一個參考
所以改變其中一個的值,另一個會跟著改變
```swift
// class
class Singer {
var name = "Taylor Swift"
}
var singer = Singer()
print(singer.name) // Taylor Swift
var singerCopy = singer
singerCopy.name = "Justin Bieber"
print(singer.name) // Justin Bieber
// struct
struct Singer_struct{
var name = "Taylor Swift"
}
var singer = Singer_struct()
print(singer.name) // Taylor Swift
var singerCopy = singer
singerCopy.name = "Justin Bieber"
print(singer.name) // Taylor Swift
```
### Deinitializer 反初始化器
第四個 class 與 struct 的不同點是類別可以被反初始化
定義deinit 方法,這個方法內的程式會在物件被消滅時執行
```swift
class Person {
var name = "John Doe"
init() {
print("\(name) is alive!")
}
func printGreeting() {
print("Hello, I'm \(name)")
}
deinit {
print("\(name) is no more!")
}
}
for _ in 1...3 {
let person = Person()
person.printGreeting()
}
```
每次回圈執行時會建立一個Person物件
然後在回圈執行完後被消滅
輸出:
```
John Doe is alive!
Hello, I'm John Doe
John Doe is no more!
John Doe is alive!
Hello, I'm John Doe
John Doe is no more!
John Doe is alive!
Hello, I'm John Doe
John Doe is no more!
```
### mutability
class 和 struct的最後一個不同是他們與常數的互動方式
如果你宣告一個常數的struct 實體,而struct裡面有變數的屬性
這個屬性仍然不能被改變,因為struct是常數
然而如果換成擁有變數屬性的常數 class 實體,那麼這個屬性就可以被改變了
所以class 的方法不需要 mutating這個關鍵字來改變屬性
在以下的例子中 taylor是常數,但是其屬性name仍然可以被改變
```swift
class Singer {
var name = "Taylor Swift"
}
let taylor = Singer()
taylor.name = "Ed Sheeran"
print(taylor.name)
```
想讓 class 的屬性不能被改變的話
就必須把屬性宣告為常數
```swift
class Singer {
let name = "Taylor Swift"
}
```
## 第11天 protocols
# protocol 協議
協議用來描述有哪些屬性與方法是必須的
在定義 struct 與 class 時再指定他們適用哪些協議
舉例而言我們可以寫一個函式接受具有id屬性的內容,
但我們不在乎使用哪種類型的資料
我們首先建立 Identifiable 協議
該協議要求所有符合條件的類型都具有可讀取(get)或寫入(set)的id 字串
```swift
protocol Identifiable {
var id: String { get set }
}
```
我們沒辦法建立一個協議的實體,他只是一個敘述
但我們可以建立一個 struct 來遵守這個協議
```swift
struct User: Identifiable {
var id: String
}
```
最後我們寫一個displayID()函式來接受任何 Identifiable的物件
```swift
func displayID(thing: Identifiable) {
print("My ID is \(thing.id)")
}
```
### protocol inheritance
協議可以被另一個協議繼承
不同於類別,你可以一次繼承好幾個協議
以下先建立三個協議,然後讓Employee繼承三個協議
```swift
protocol Payable {
func calculateWages() -> Int
}
protocol NeedsTraining {
func study()
}
protocol HasVacation {
func takeVacation(days: Int)
}
protocol Employee: Payable, NeedsTraining, HasVacation
```
### Extensions 擴展
extensions允許你加入方法到已經存在的資料型態中
來擴展他們本來沒有的功能
舉例來說我們可以加入一個擴展到 Int 型態中讓他有 squared() 方法
返回自己的平方
```swift
extension Int {
func squared() -> Int {
return self * self
}
}
```
swift不讓你加入 stored property到extension中,
所以所有的屬性都要用 computed property
### protocol extension 協議擴展
協議(protocols)讓你描述方法該做什麼,但沒有描述具體怎麼做
擴展(extensions)讓你提供方法中的程式,但只作用於一種型態
protocol extension 同時解決了兩個問題,他可以提供擴展
並且作用於多種資料型態
```swift
let pythons = ["Eric", "Graham", "John", "Michael", "Terry", "Terry"]
let beatles = Set(["John", "Paul", "George", "Ringo"])
```
swift 的 陣列和集合都遵守collection協議
所以我們可以寫一個擴展到collection的協議中
增加一個summarize()方法
```swift
extension Collection {
func summarize() {
print("There are \(count) of us:")
for name in self {
print(name)
}
}
}
```
這樣陣列跟集合型態都能夠使用summarize()這個方法了
### protocol-oriented programming 協議導向程式設計
協議擴展(protocol extentions)可以為我們自己的協議方法提供預設的實作
這讓各種型態都很容易遵守協議
並衍生出一種稱為協議導向的程式設計方法
也就是圍繞協議和協議擴展來設計程式。
首先這裡有一個稱為Identifiable的協議,
要求所有遵守協議的型態擁有id屬性和identify()方法
```swift
protocol Identifiable {
var id: String { get set }
func identify()
}
```
我們可以對每種遵守協議的型別寫一個他自己的identify()
但是使用協議擴展讓我們能建立預設的方法
```swift
extension Identifiable {
func identify() {
print("My ID is \(id).")
}
}
```
現在每當我們建立一個遵守Identifiable的類別時
他便自動得到identify()方法了
```swift
struct User: Identifiable {
var id: String
}
let twostraws = User(id: "twostraws")
twostraws.identify()
```
## 第十二天 Optionals
### 處理遺失的資料
該如何描述不知道的值?
舉例來說,今天你要記錄一個人的年紀
未知值該設為多少?
你可能會想用 -1或 1000這種一看就知道不可能的數字來代表未知
但每種情況能用的數字不一樣,你無法保證每種數字你都能記得
所以 swift 提出一個解決辦法,就是 optionals
任何型別的資料都能設為 optionals,一個optional的整數可能有值
也可能根本沒有值--也就是 nil
要讓一個型別成為 optional,在他的後面加上問號"?"就行了
舉例來說
```swift
var age: Int? = nil
```
age 本身不代表任何值,所以他擁有 nil值
但如果我們稍後知道了他的值
可以把值指定進去age變數中
```swift
age = 38
```
### 展開optional
optional的字串值可能含有類似"hello"這樣的字串
或是nil -- 什麼都沒有
考慮以下的 optional字串
```swift
var name: String? = nil
```
如果我們用 name.count會發生什麼事?
真正的字串含有count屬性,這個屬性儲存字串的字母數
但現在 name是 nil,不是一個值,所以它沒有count屬性
因此,試圖讀取 name.count是不安全的,swift 不會允許
相反的,我們必須查看optional內容的內部並查看內部內容
這個過程稱為展開
一個展開 optional的常見方法是 if let 語法,
也就是也就是以條件判斷展開,
如果optional 包含有值,則可以使用它,如果沒有,則條件不成立
例如:
```swift
if let unwrapped = name {
print("\(unwrapped.count) letters")
} else {
print("Missing name.")
}
```
如果name有字串值,他會把值指定給unwurapped
如果name是空值, else裡的程式就會被執行
### 用 guard展開
有另一個取代if let 的語法是guard let
guard let 會展開一個 optional,但如果在其中發現nil
則希望你退出使用它的函式,循環或條件
然而,if let 和 guard let 最大的不同是展開後的 optional
在 guard let 程式區塊之外依然可用
讓我們試試一個greet()函式,接受一個 optional字串作為唯一參數
並試圖展開他,但如果裡面沒有值,便會印出一個訊息並跳出
因為 guard let展開的值在完成後仍然可用
我們在函式的最後可以把 unrapped字串印出來
```swift
func greet(_ name: String?){
guard let unwrapped = name else{
print("You didn't provide a name")
return
}
print("Hello, \(unwrapped)")
}
```
使用 guard let 讓你在函式的一開始處理問題,然後立即退出
也就是說函式的後續部分是在一切正確時才會執行。
### 強制展開
optional 代表了可能存在也可能不存在的資料,
但有時候你非常確定那個值不是 nil,在這個情況下
swift 允許你強制展開一個 optiona:
把一個optional型別轉換成一個非optionalu型別
舉例來說,如果你有個包含整數的字串,你可以將它轉換為整數
```swift
let str = "5"
let num = Int(str)
```
這讓 num成為一個optional的 Int,因為你的字串可能是"Fish"而不是"5"
即使Swift不確定轉換是否可以正常進行,你可以看到程式碼是安全的
因此,你可以透過在Int(str)後面加上"!"符號強制展開結果
```swift
let num = Int(str)!
```
Swift會立即展開 optional並且將num視為普通 Int,而不是Int?
但如果你錯了 -- 如果 str是無法轉換為整數的字串,你的程式就會崩潰
結論是,你只有在很確定他是安全的情況下才用強制展開 -- 它被
叫做崩潰運算子是有原因的
### 隱性展開optioanls implicitly unwrapped optional
隱性展開和一般和一般展開差不多,但你不需要去展開他
你可以像完全不是 optional那樣的使用它們
我們在資料型態後面加上 "!"符號就可以了
```swift
let age: Int! = nil
```
因為他們的行為就像是已經被展開一樣,你不需要 if let 或是 guard let
來使用隱性展開的optional
然而,如果你試圖使用它們但他們不含有值 -- 你的程式會崩潰
隱性展開的存在理由是有些變數會以 nil開頭,但在使用之前始終有值
因為你知道在你需要時他一定有值,所以讓他不用展開就很有幫助
雖然如此,使用一般展開的 optional通常還是個好主義
### nil合併
nil合併運算子展開一個optional,如果有值就返回內部的值,
如果沒有值(nil),則使用預設值,無論哪種方式,
結果都不是optional: 他將選擇optional內部值或備份的預設值
這裡有個接受整數作為參數的函式,並回傳一個 optional字串
```swift
func username(for id: Int) -> String? {
if id == 1{
return "Taylor Swift"
} else{
return nil
}
}
```
如果我們呼叫這個函式,id為15,會得到回傳值 nil
但有 nil合併運算子,我們可以設定一個預設值"Anonymous"
```swift
let user = username(for: 15) ?? "Anonymous"
```
這樣如果 username()返回值是 nil,便會被改成 Anonymous
就不會出現 optional的狀況了
### optional連鎖
Swift提供了一個使用 optional的捷徑:如果你想要對a.b.c取值
而其中 b是 optional,那只要在b後面加個問號,變成 a.b?.c
當這段程式碼執行時,swift會確認 b 是否有值,如果是 nil,
Swift會直接忽略後面的程式,回傳 nil ,如果有值,會被展開後繼續執行
我們來試試看,這裡有一個 names陣列
```swift
let names = ["John", "Paul", "George", "Ringo"]
```
接著把第一項轉變成大寫,first屬性回傳陣列的第一項,
uppercased()方法回傳全部大寫的字串
```swift
let beatle = names.first?.uppercased()
```
如果 first 的回傳值是 nil 那 beatle就會是 nil,
uppercased()也不會被執行
### optional try
回到 throwing function的例子
```swift
enum PasswordError: Error {
case obvious
}
func checkPassword(_ password: String) throws -> Bool {
if password == "password" {
throw PasswordError.obvious
}
return true
}
do {
try checkPassword("password")
print("That password is good!")
} catch {
print("You can't use that password.")
}
```
這例子用了 do, try, catch來處理錯誤
另外還有兩種替代try的方法,在了解optionals和 強制展開後兩種都更合理
```swift
if let result = try? checkPassword("password") {
print("Result was \(result)")
} else {
print("D'oh.")
}
```
另一個替代方案是 try!
如果你確定這個函式一定不會失敗
但如果函式丟出錯誤,那麼程式就會崩潰
我們可以把程式重寫成這樣:
```swift
try! checkPassword("sekrit")
print("OK!")
```
### failable initializer
在強制展開的範例中
```swift
let str = "5"
let num = Int(str)
```
這將字串強制轉換成整數
因為你可能輸入任何字串到Int()中
所以你實際得到的是一個 optional整數
這叫做failable initializer:
這個初始化器可能成功也可能失敗
你可以在 class或 struct中寫 init?() 來取代 init()
這樣當某個環節出錯時,會返回 nil
因此返回的會是一個 optional的型別
讓你可以依自己所想進行展開
作為範例,我們建立一個名為Person 的 struct
限制必須用九個字母的 ID字串
如果輸入的ID不是九個字母,就返回 nil
否則就正常執行
```swift
struct Person {
var id: String
init?(id: String) {
if id.count == 9 {
self.id = id
} else {
return nil
}
}
}
```
### Typecasting 類型轉換
理論上 swift總是知道你每個變數的型別
但有時候你還是知道比 swift多一點資訊
舉例來說,這裡有三個類別
```swift
class Animal { }
class Fish: Animal { }
class Dog: Animal {
func makeNoise() {
print("Woof!")
}
}
```
我們可以建立一些Dog和一些Fish然後把他們放在一個陣列中
```swift
let pets = [Fish(), Dog(), Fish(), Dog()]
```
Swift 可以知道 Dog 和 Fish都是繼承自 Animal
所以 pets是一個裝著Animal型態的陣列
如果我們想要遍歷 pets陣列並且讓每一隻狗叫
我們需要執行類型轉換,Swift 會逐一檢查每個物件
如果是 Dog類的物件就可以呼叫makeNoise()方法
這需要一個 "as?" 關鍵字的配合,回傳一個optional
(如果轉換失敗就回傳 nil)
所以這個迴圈可以這樣寫:
```swift
for pet in pets {
if let dog = pet as? Dog {
dog.makeNoise()
}
}
```