# 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() } } ```