附錄 - Swift 基礎概論
Swift 是開發 iOS、macOS、watchOS 以及 tvOS App 的新程式語言。與 Objective-C 相較,Swift 是一個簡潔的語言,可使 iOS App 開發更容易。在附錄 A 中,我會對 Swift 做簡要的介紹。這裡的內容並不是完整的程式指南,不過我們提供了初探 Swift 所需的基本概念,你可以參考官方文件( https://swift.org/documentation/ ),有更完整的內容。
變數、常數與型別推論
Swift 中是以 var
關鍵字來宣告變數(Variable ),常數(Constant )的宣告則使用 let
關鍵字。下列為其範例:
var numberOfRows = 30
let maxNumberOfRows = 100
有二個宣告常數與變數的關鍵字需要知道。你可使用 let
關鍵字來儲存不會變更的值。反之,則使用 var
關鍵字儲存可變更的值。
這是不是比 Objective-C 更容易呢?
有趣的是 Swift 允許你使用任何字元作為變數與常數名稱,甚至你也可以使用表情符號(Emoji Character )來命名。
你可能注意到 Objective-C 在變數的宣告上與 Swift 有很大的不同。在 Objective-C 中, 開發者在宣告變數時,必須明確地指定型別的資訊,如 int
、 double
或者 NSString
等。
const int count = 10;
double price = 23.55;
NSString *myMessage = @"Objective-C is not dead yet!";
你必須負責指定型別。而在 Swift,你不再需要標註變數型別的資訊,它提供了一個「型別推論」(Type Inference )的強大功能,這個功能啟動編譯器,透過你在變數中所提供的值做比對來自動推論其型別。
let count = 10
// count 被推論為 Int 型別
var price = 23.55
// price 被推論為 Double 型別
var myMessage = "Swift is the future!"
// myMessage 被推論為 String 型別
和 Objective-C 相較的話,它使得變數與常數的宣告更容易。Swift 也另外提供一個明確指定型別資訊的功能。下列的範例介紹了如何在 Swift 宣告變數時指定型別資訊。
var myMessage: String = "Swift is the future!"
沒有分號做結尾
在 Objective-C 中,你需要在你的程式碼的每一段敘述( Statement )之後,加上一個分號作為結尾。如果你忘記加上分號,在編譯時會得到一個錯誤提示。如同上列的範例, Swift 不需要你在每段敘述之後加上分號( ;
),但是若你想要這麼做的話也沒問題。
var myMessage = "No semicolon is needed"
基本字串操作
在Swift 中,字串是以 String
型別表示,全是 Unicode 編譯。你可將字串宣告為變數或常數:
let dontModifyMe = "You cannot modify this string"
var modifyMe = "You can modify this string"
在 Objective-C 中,為了指定字串是否可變更,你必須在 NSString
與 NSMutableString
類別間做選擇。而Swift 不需要這麼做,當你指定一個字串為變數時(也就是使用 var
), 這個字串就可以在程式碼中做變更。
Swift 簡化了字串的操作,並且可以讓你建立一個混合常數、變數、常值(Literal )、運算式(Expression )的新字串。字串的串連超級簡單,只要將兩個字串以 +
運算子加在一起即可:
let firstMessage = "Swift is awesome."
let secondMessage = "What do you think?"
var message = firstMessage + secondMessage
print(message)
Swift 自動將兩個訊息結合起來,你可以在主控台看見下列的訊息。注意 print 是 Swift 中一個可以將訊息列印輸出到主控台中的全域函數(Global Function )。
Swift 太棒了,你覺得呢?你可以在 Objective-C 中使用 stringWithFormat:
方法來完成。但是Swift 是不是更容易閱讀呢?
NSString *firstMessage = @"Swift is awesome. ";
NSString *secondMessage = @"What do you think?";
NSString *message = [NSString stringWithFormat:@"%@%@", firstMessage, secondMessage];
NSLog(@"%@", message);
字串的比較也更簡單了。你可以像這樣直接使用 ==
運算子來做字串的比較:
var string1 = "Hello"
var string2 = "Hello"
if string1 == string2 {
print("Both are the same")
}
陣列(Arrays)
Swift 中宣告陣列的語法與 Objective-C 相似。舉例如下:
Objective-C:
NSArray *recipes = @[@"Egg Benedict", @"Mushroom Risotto", @"Full Breakfast", @"Hamburger", @"Ham and Egg Sandwich"];
Swift:
var recipes = ["Egg Benedict", "Mushroom Risotto", "Full Breakfast", "Hamburger", "Ham and Egg Sandwich"]
在 Objective-C 中,你可以將任何物件放進 NSArray
或 NSMutableArray
,而Swift 中的陣列只能儲存相同型別的項目。以上列的範例來說,你只能儲存字串至字串陣列。有了型別推論,Swift 自動偵測陣列型別。或者你也可以用下列的形式來指定型別:
var recipes : String[] = ["Egg Benedict", "Mushroom Risotto", "Full Breakfast", "Hamburger", "Ham and Egg Sandwich"]
Swift 提供各種讓你查詢與操作陣列的方法。只要使用 count 方法就可以找出陣列中的項目數:
var numberOfItems = recipes.count
// recipes.count 會回傳 5
Swift 讓陣列操作更為簡單,你可以使用 +=
運算子來增加一個項目:
recipes += ["Thai Shrimp Cake"]
這樣的做法可以讓你加入多個項目:
recipes += ["Creme Brelee", "White Chocolate Donut", "Ham and Cheese Panini"]
要在陣列存取或變更一個特定的項目,和 Objective-C 以及其他程式語言一樣使用下標語法( Subscript Syntax )傳遞項目的索引值( Index )。
var recipeItem = recipes[0]
recipes[1] = "Cupcake"
Swift 中一個有趣的功能是你可以使用「 ...
」來變更值的範圍。舉例如下:
recipes[1...3] = ["Cheese Cake", "Greek Salad", "Braised Beef Cheeks"]
這將 recipes 陣列的項目 2 至 4 變更為「Cheese Cake」、「Greek Salad」、「Braised Beef Cheeks」(要記得陣列第一個項目是索引值 0,這便是為何索引 1 對應項目 2 )。
當你輸出陣列至主控台,結果如下所示:
- Egg Benedict
- Cheese Cake
- Greek Salad
- Braised Beef Cheeks
- Ham and Egg Sandwich
字典(Dictionaries)
Swift 提供三種集合型別(Collection Type ):陣列、字典與 Set。我們先來討論字典, 每一個字典中的值,對應一個唯一的鍵。要在 Swift 宣告一個字典,程式碼寫法如下所示:
var companies = ["AAPL" : "Apple Inc", "GOOG" : "Google Inc", "AMZN" : "Amazon.com, Inc", "FB" : "Facebook Inc"]
鍵值配對( key-value pair )中的鍵與值用冒號分開,然後用方括號包起來,每一對用逗號來分開。
就像陣列或其他變數一樣,Swift 自動偵測鍵與值的型別。不過,你也可以用下列的語法來指定型別資訊:
var companies: [String: String] = ["AAPL" : "Apple Inc", "GOOG" : "Google Inc", "AMZN" : "Amazon.com, Inc", "FB" : "Facebook Inc"]
要對字典做逐一查詢,可以使用 for-in
迴圈。
for (stockCode, name) in companies {
print("\(stockCode) = \(name)")
}
// 你可以使用 keys 與 values 屬性來取得字典的鍵值
for stockCode in companies.keys {
print("Stock code = \(stockCode)")
}
for name in companies.values {
print("Company name = \(name)")
}
要取得特定鍵的值,使用下標語法指定鍵,當你要加入一個新的鍵值配對到字典中, 只要使用鍵作為下標,並指定一個值,就像這樣:
companies["TWTR"] = "Twitter Inc"
現在 companies
字典總共包含五個項目。"TWTR":"Twitter Inc" 配對自動地加入 companies
字典。
Set
Set 和陣列非常相似,陣列是有排序的集合,而 Set 則是沒有排序的集合。在陣列中的項目可以重複,但是在 Set 中則沒有重複值。
要宣告一個 Set,你可以像這樣寫:
var favoriteCuisines: Set = ["Greek", "Italian", "Thai", "Japanese"]
此語法和陣列的建立一樣,不過你必須明確指定 Set
型別。
如前所述,Set 是不同項目、沒有經過排序的集合。當你宣告一組 Set 有重複的值,它便不會儲存這個值,以下列程式碼為例:
Set 的操作和陣列很相似,你可以使用 for-in
迴圈來針對 Set 做迭代( Iterate )。不過, 當你要加入一個新項目至 Set 中,你不能使用 +=
運算子。你必須呼叫 insert
方法:
favoriteCuisines.insert("Indian")
有了 Set,你可以輕易地判斷兩組 Set 中有重複的值或不相同的值。舉例而言,你可以使用兩組 Set 來分別代表兩個人最愛的料理種類。
var tomsFavoriteCuisines: Set = ["Greek", "Italian", "Thai", "Japanese"]
var petersFavoriteCuisines: Set = ["Greek", "Indian", "French", "Japanese"]
當你想要找出他們之間共同喜愛的料理種類,你可以像這樣呼叫 intersection
方法:
tomsFavoriteCuisines.intersection(petersFavoriteCuisines)
結果會回傳:
{"Greek", "Japanese"}
.
或者,若你想找出哪些料理是他們不共同喜愛的,則可以使用 symmetricDifference
方法:
tomsFavoriteCuisines.symmetricDifference(petersFavoriteCuisines)
// Result: {"French", "Italian", "Thai", "Indian"}
類別(Classes)
在 Objective-C 中,你針對一個類別分別建立了介面( .h )與實作( .m )檔。而Swift 不再需要開發者這麼做了。你可以在單一個檔案( .swift )中定義類別,不需要額外分開介面與實作。
要定義一個類別,須使用 class 關鍵字。下列是 Swift 中的範例類別:
class Recipe {
var name: String = ""
var duration: Int = 10
var ingredients: [String] = ["egg"]
}
在上述的範例中,我們定義一個 Recipe
類別加上三個屬性,包含 name、duration 與 ingredients。Swift 需要你提供屬性的預設值。如果缺少初始值,你將得到編譯錯誤的結果。
若是你不想指定一個預設值呢? Swift 允許你在值的型別之後寫一個問號( ?
),將它的值定義為可選擇性的值( Optional )。
class Recipe {
var name: String?
var duration: Int = 10
var ingredients: [String]?
}
在上列的程式碼中, name
與 ingredients
屬性自動被指定一個 nil
的預設值。想建立一個類別的實例( instance ),只要使用下列的語法:
var recipeItem = Recipe()
// 你可以使用點語法來存取或變更一個實例的屬性
recipeItem.name = "Mushroom Risotto"
recipeItem.duration = 30
recipeItem.ingredients = ["1 tbsp dried porcini mushrooms", "2 tbsp olive oil", "1 onion, chopped", "2 garlic cloves", "350g/12oz arborio rice", "1.2 litres/2 pints hot vegetable stock", "salt and pepper", "25g/1oz butter"]
Swift 允許你繼承以及採用協定。舉例而言,如果你有一個從 UIViewController
類別延伸而來的SimpleTableViewController
類別,並採用 UITableViewDelegate
與 UITableView DataSource
協定。你可以像這樣做類別宣告:
class SimpleTableViewController : UIViewController, UITableViewDelegate, UITableViewDataSource
方法( Methods )
和其他物件導向語言一樣,Swift 允許你在類別中定義函數,也就是所謂的「方法」。你可以使用 func
關鍵字來宣告一個方法。下列為沒有帶回傳值與參數的方法範例:
class TodoManager {
func printWelcomeMessage() {
print("Welcome to My ToDo List")
}
}
在 Swift 中,你可以使用點語法( Dot Syntax )呼叫一個方法:
todoManager.printWelcomeMessage()
當你需要宣告一個帶著參數與回傳值的方法,方法看起來如下:
class TodoManager {
func printWelcomeMessage(name:String) -> Int {
print("Welcome to \(name)'s ToDo List")
return 10
}
}
這個語法看起來較為難懂,特別是 ->
運算子。上述的方法取一個字串型別的name 參數作為輸入。->
運算子是作為方法回傳值的指示器。從上列的程式碼來看,你將代辦項目總回傳數的回傳型別指定為 Int
。下列為呼叫此方法的範例:
var todoManager = TodoManager()
let numberOfTodoItem = todoManager.printWelcomeMessage(name: "Simon")
print(numberOfTodoItem)
控制流程(Control Flow)
控制流程與迴圈利用了和 C 語言非常相似的語法。如同前面小節所見,Swift 提供了 for-in 迴圈來迭代陣列與字典。
for 迴圈
如果你想要迭代一定範圍的值,你可使用「 ...
」或者「..<
」運算子。這些都是在 Swift 中新導入的運算子,表示一定範圍的值。例如:
for i in 0..<5 {
print("index = \(i)")
}
這會在主控台輸出下列的結果:
index = 0
index = 1
index = 2
index = 3
index = 4
那麼「..<
」與「 ...
」有什麼不同?如果我們將上面範例中的「..<
」以「 ...
」取代,這定義了執行 0 到 5 的範圍,而 5 也包括在範圍內。下列是主控台的結果:
index = 0
index = 1
index = 2
index = 3
index = 4
index = 5
if-else 敘述
和 Objective-C 一樣,你可以使用 if
敘述依照某個條件來執行程式碼。這個 if-else
敘述的語法與 Objective-C 很相似。Swift 只是讓語法更簡單,讓你不再需要用一對圓括號來將條件包覆起來。
var bookPrice = 1000;
if bookPrice >= 999 {
print("Hey, the book is expensive")
} else {
print("Okay, I can affort it")
}
switch 敘述
我要特別強調 Swift 的 switch 敘述,相對於 Objective-C 而言是一個很大的改變。請看下列的範例,你有注意到什麼地方比較特別嗎?
switch recipeName {
case "Egg Benedict":
print("Let's cook!")
case "Mushroom Risotto":
print("Hmm... let me think about it")
case "Hamburger":
print("Love it!")
default:
print("Anything else")
}
首先,switch
敘述可以處理字串。在 Objective-C 中,無法在 NSString
做 switch
。你必須用數個if 敘述來實作上面的程式碼。而 Swift 可使用 switch 敘述,這個特點最受青睞。
另一個你可能會注意到的有趣特點是,它沒有 break。記得在 Objective-C 中,你需要在每個 switch case 後面加上 break。否則的話,它會進到下一個 case。在 Swift 中,你不需要明確的加上一個 break 敘述。Swift 中的switch 敘述不會落到每一個 case 的底部,然後進到下一個。相反的,當第一個 case 完成配對後,全部的 switch 敘述便完成任務的執行。
除此之外,switch 敘述也支援範圍配對( range matching ),以下列程式碼來說明:
var speed = 50
switch speed {
case 0:
print("stop")
case 0...40:
print("slow")
case 41...70:
print("normal")
case 71..<101:
print("fast")
default:
print("not classified yet")
}
// 當速度落在 41 與 70 的範圍,它會在主控台上輸出 normal
switch case 可以讓你透過二個新的運算子「 ...
」與「..<
」,來檢查一個範圍內的值。這兩個運算子是作為表示一個範圍值的縮寫。
例如:「41...70」的範圍,「...
」運算子定義了從 41 到 70 的執行範圍,有含括 41 與 70。如果我們使用「..<
」取代範例中的「 ...
」,則是定義執行範圍為 41 至 69。換句話說, 70 不在範圍之內。
元組(Tuples)
Swift 導入了一個在 Objective-C 所沒有的先進型別稱作「元組」( Tuples )。元組可以允許開發者建立一個群組值並且傳遞。假設你正在開發一個可以回傳多個值的方法,你便可以使用元組作為回傳值取代一個自訂物件的回傳。
元組把多個值視為一個單一複合值。以下列的範例來說明:
let company = ("AAPL", "Apple Inc", 93.5)
上面這行程式碼建立了一個包含股票代號、公司名稱以及股價的元組。你可能會注意到,元組內可以放入不同型別的值。你可以像這樣來解開元組的值:
let (stockCode, companyName, stockPrice) = company
print("stock code = \(stockCode)")
print("company name = \(companyName)")
print("stock price = \(stockPrice)")
一個使用元組的較佳方式是在元組中賦予每個元素一個名稱,而你可以使用點語法來存取元素值,如下列的範例所示:
let product = (id: "AP234", name: "iPhone X", price: 599)
print("id = \(product.id)")
print("name = \(product.name)")
print("price = USD\(product.price)")
常見使用元組的方式就是作為值的回傳,在某些情況下,你想要在方法中不使用自訂類別來回傳多個值。你可以使用元組作為回傳值,如下列的範例所示:
class Store {
func getProduct(number: Int) -> (id: String, name: String, price: Int) {
var id = "IP435", name = "iMac", price = 1399
switch number {
case 1:
id = "AP234"
name = "iPhone X"
price = 999
case 2:
id = "PE645"
name = "iPad Pro"
price = 599
default:
break
}
return (id, name, price)
}
}
在上列的程式碼中,我們建立了一個名為 getProduct
、帶著數字參數的呼叫方法,並且回傳一個元組型別的產品值。你可像這樣呼叫這個方法並儲存值:
let store = Store()
let product = store.getProduct(number: 2)
print("id = \(product.id)")
print("name = \(product.name)")
print("price = USD\(product.price)")
Optional 的介紹
何謂 Optional?當你在Swift 中宣告變數,它們預設是設計為非 Optional。換句話說, 你必須指定一個非nil 的值給這個變數。如果你試著設定一個 nil
值給非 Optional
,編譯器會告訴你:「 Nil 值不能指定為 String 型別 !」。
var message: String = "Swift is awesome!" // OK
message = nil // 編譯期間錯誤
在類別中,宣告屬性時也會應用到。屬性預設是被設計為非 Optional。
class Messenger {
var message1: String = "Swift is awesome!" // OK
var message2: String // 編譯期間錯誤
}
這個 message2
會得到一個編譯期間錯誤( Compile-time Error )的訊息,因為它沒有指定一個初始值。這對那些有 Objective-C 經驗的開發者而言會有些驚訝。在 Objective-C 或另外的程式語言(例如:JavaScript ),指定一個 nil
值給變數或宣告一個沒有初始值的屬性,不會有編譯期間錯誤的訊息。
NSString *message = @"Objective-C will never die!";
message = nil;
class Messenger {
NSString *message1 = @"Objective will never die!";
NSString *message2;
}
不過,這並不表示你不能在 Swift 中宣告一個沒有指定初始值的屬性。Swift 導入了 Optional 型別來指出缺值。它是在型別宣告後面加入一個問號( ?
)運算子來定義。以下列範例來說明:
class Messenger {
var message1: String = "Swift is awesome!" // OK
var message2: String? // OK
}
當變數定義為 Optional 時,你仍然可以指定值給它。但若是這個變數沒有指定任何值給它,它會自動定義為 nil
。
為何需要 Optional?
Swift 是為了安全性考量而設計的。Apple 曾經提過,Optional 是 Swift 作為型別安全語言的一項映證。從上列的範例來看,Swift 的 Optional 提供編譯時檢查,避免執行期間一些常見的程式錯誤。我們來看下列的範例,你將會更了解 Optional 的功能。
參考這個在 Objective-C 的方法:
- (NSString *)findStockCode:(NSString *)company {
if ([company isEqualToString:@"Apple"]) {
return @"AAPL";
} else if ([company isEqualToString:@"Google"]) {
return @"GOOG";
}
return nil;
}
你可以使用 findStockCode
方法來取得清單中某家公司的股票代號。為了示範起見,這個方法只回傳 Apple 與Google 的股票代號。其他的輸入則回傳 nil
。
假設這個方法定義在同一個類別,我們可以這樣使用它:
NSString *stockCode = [self findStockCode:@"Facebook"]; // 回傳 nil
NSString *text = @"Stock Code - ";
NSString *message = [text stringByAppendingString:stockCode]; // 執行期間錯誤
NSLog(@"%@", message);
這段程式碼會正確編譯,但因為是 Facebook,所以會回傳 nil
,在執行 App 時會出現執行期間錯誤的情況。有了 Swift 的 Optional,錯誤不會在執行期間才被發現,而是在編譯期間就會先出現了。假使我們以 Swift 重寫上述的範例,它看起來會像這樣:
func findStockCode(company: String) -> String? {
if (company == "Apple") {
return "AAPL"
} else if (company == "Google") {
return "GOOG"
}
return nil
}
var stockCode: String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
let message = text + stockCode // 編譯期間錯誤
print(message)
stockCode
是以 Optional 來定義,意思是說它可以包含一個字串或是 nil
。你不能執行上列的程式碼,因為編譯器偵測到潛在的錯誤:「Optional 型別 String? 的值還未解開包裝」( value of optional type String? is not unwrapped ),並且告訴你要修正它。
從上述的範例中可以知道,Swift 的 Optional 加強了 nil 的檢查,並且提供編譯期間錯誤的線索給開發者。很顯然的,使用 Optional 能夠改善程式碼的品質。
解開 Optional
那麼我們該如何讓程式可以運作?很顯然的,我們需要測試 stockCode
是否有包含一個 nil
值。我們修改程式碼如下:
var stockCode: String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
if stockCode != nil {
let message = text + stockCode!
print(message)
}
和 Objective-C 相同的部分是,我們使用 if
來執行 nil
檢查。一旦我們知道 Optional 必須包含一個值,我們在Optional 名稱的後面加上一個驚嘆號( !
)來解開它。在 Swift 中,這就是所謂的強制解開( Forced Unwrapping )。你可以使用 !
運算子來解開 Optional 的包裝以及揭示其內在的值。
參照上列的範例程式碼,我們只是在 nil 值檢查後解開 stockCode
Optional,我們知道 Optional 在使用 !
運算子解開它之前,必須包含一個非 nil 的值。這裡要強調的是,建議在解開它之前,確保 Optional 必須包含一個值。
但如果我們像下列的範例這樣忘記確認呢?
var stockCode:String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
let message = text + stockCode! // 執行期間錯誤
這種情況不會有編譯期間錯誤,當強制解開啟用後,編譯器假定 Optional 包含了一個值。不過當你執行 App 時,就會在主控台產生一個執行期間錯誤的訊息。
Optional 綁定
除了強制解開之外,Optional 綁定( Optional Binding )是一個較簡單且比較推薦用來解開 Optional 包裝的方式。你可以使用 Optional 綁定來檢查 Optional 是否有含值。如果它有含值則解開它,並把它放進一個暫時的常數或變數。
沒有比使用一個實際範例來解釋 Optional 綁定的最佳方式了。我們將前面範例的範例程式轉換成 Optional 綁定:
var stockCode: String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
if let tempStockCode = stockCode {
let message = text + tempStockCode
print(message)
}
if let
(或 if var
)是 Optional 綁定的兩個關鍵字。以白話來說,這個程式碼是說:「如果 stockCode
包含一個值則解開它,將其值設定到 tempStockCode
,然後執行後面的條件敘述,否則的話彈出這段程式」。因為tempStockCode
是一個新的常數,你不需要使用「!
」字尾來存取其值。
你也可以透過在 if
敘述中做函數的判斷,進一步簡化程式碼:
let text = "Stock Code - "
if var stockCode = findStockCode(company: "Apple") {
let message = text + stockCode
print(message)
}
這裡的 stockCode
不是 Optional,所以不需要使用 !
字尾在程式碼區塊中存取其值。如果從函數回傳 nil
值,程式碼區塊便不會執行。
Optional 鏈
在解釋 Optional 鏈( Optional Chaining )之前,我們調整一下原來的範例。我們建立了一個名為 Stock 的新類別, 這個類別有 code 以及 price 屬性, 且都是 Optional。findStockCode
函數修改成以 Stock
物件取代String 來回傳。
class Stock {
var code: String?
var price: Double?
}
func findStockCode(company: String) -> Stock? {
if (company == "Apple") {
let aapl: Stock = Stock()
aapl.code = "AAPL"
aapl.price = 90.32
return aapl
} else if (company == "Google") {
let goog: Stock = Stock()
goog.code = "GOOG"
goog.price = 556.36
return goog
}
return nil
}
我們重寫原來的範例如下所示,並先呼叫 findStockCode
函數來找出股票代號,然後計算買 100 張股票的總成本是多少。
if let stock = findStockCode(company: "Apple") {
if let sharePrice = stock.price {
let totalCost = sharePrice * 100
print(totalCost)
}
}
由於 findStockCode()
的回傳值是 Optional,我們使用Optional 綁定來檢查實際上是否有含值。顯然地,Stock 類別的 price 屬性是 Optional,我們再次使用 if let
敘述來測試 stock.price
是否有包含一個非空值。
上列的程式碼運作沒有問題。你可以使用 Optional 鏈指定程式碼,來取代巢狀式 if let
的撰寫,以簡化程式。這功能允許我們將多個 Optional 以 ?.
運算子連結起來。下列是程式碼的簡化版本:
if let sharePrice = findStockCode(company: "Apple")?.price {
let totalCost = sharePrice * 100
print(totalCost)
}
Optional 鏈提供另一種存取 price 值的方式。現在程式碼看起來更簡潔了。此處只是介紹了 Optional 鏈的基礎部分。你可以進一步至《Apple's Swift Guide》研究有關 Optional 鏈的資訊。
可失敗化初始器(Failable Initializers)
Swift 導入新的功能稱作「可失敗化初始器」( Failable Initializers )。初始化( Initialization )是一個類別中所儲存每一個屬性設定初始值的程序。在某些情況下,實例( instance )的初始化可能會失敗。現在像這樣的失敗可以使用可失敗化初始器。可失敗化初始器的結果包含一個物件或是 nil
。你需要使用 if let
來檢查初始化是否成功。舉例而言:
let myFont = UIFont(name : "AvenirNextCondensed-DemiBold", size: 22.0)
如果字型檔案不存在或無法讀取,UIFont
物件的初始化便會失敗。初始化失敗會使用可失敗化初始器來回報。回傳的物件是一個 Optional,此不是物件本身就是 nil。因此我們需要使用 if let
來處理 Optional:
if let myFont = UIFont(name : "AvenirNextCondensed-DemiBold", size: 22.0) {
// 下列為要處理的程序
}
泛型(Generics)
泛型不是新的觀念,在其他程式語言如 Java,已經運用很久了。但是對於 iOS 開發者而言,你可能會對泛型感到陌生。
泛型函數(Generic Functions )
泛型是 Swift 強大的功能之一,可以讓你撰寫彈性的函數。那麼,何謂泛型呢?好的, 我們來看一下這個範例。假設你正在開發一個 process
函數:
func process(a: Int, b: Int) {
// 執行某些動作
}
這個函數接受二個整數值來做進一步的處理。那麼,當你想要帶進另外一個型別的值,如 Double
呢?你可能會另外撰寫函數如下:
func process(a: Double, b: Double) {
// do something
}
這二個函數看起來非常相似。假設函數本身是相同,差異性在於「輸入的型別」。有了泛型,你可以將它們簡化成可以處理多種輸入型別的泛型函數:
func process<T>(a: T, b: T) {
// do something
}
現在它是以佔位符型別( Placeholder Type )取代實際的型別名稱,函數名稱後的 <T>
,表示這是一個泛型函數。對於函數參數,實際的型別名稱則以泛型型別 T
來代替。
你可以用相同的方式呼叫這個 process 函數。實際用來取代 T
的型別,會在函數每次被呼叫時來決定。
process(a: 689, b: 167)
約束型別的泛型函數
我們來看另一個範例,假設你撰寫另一個比較二個整數值是否相等的函數。
func isEqual(a: Int, b: Int) -> Bool {
return a == b
}
當你需要和另一個型別值如字串來做比較,你需要另外寫一個像下列的函數:
func isEqual(a: String, b: String) -> Bool {
return a == b
}
有了泛型的幫助,你可以將二個函數合而為一:
func isEqual<T>(a: T, b: T) -> Bool {
return a == b
}
同樣的,我們使用 T 作為型別值的佔位符。如果你在 Xcode 測試上列的程式碼,這個函數無法編譯。問題在於 a==b
的檢查。雖然這個函數接受任何型別的值,但不是所有的型別皆可以支援這個相等( ==
)的運算子,因此Xcode 才會指出錯誤。在這個範例中, 你需要使用約束型別的泛型函數。
func isEqual<T: Equatable>(a: T, b: T) -> Bool {
return a == b
}
你可以在型別參數名稱後面寫上一個約束協定( protocol constraint )的約束型別,以冒號來做區隔。這裡的 Equatable
就是約束協定。換句話說,這個函數只會接受支援約束協定的值。
在 Swift 中,它內建一個標準的協定稱作Equatable
,所有遵循這個 Equatable
協定的型別,都可以支援相等(==)運算子。所有標準型別如 String
、Int
與 Double
都支援 Equatable
協定。
所以你可以像這樣使用 isEqual
函數:
isEqual(a: 3, b: 3) // true
isEqual(a: "test", b: "test") // true
isEqual(a: 20.3, b: 20.5) // false
泛型型別(Generic Types)
在函數中,使用泛型是沒有限制的。Swift 可以讓你定義自己的泛型型別。這可以是自訂類別或結構。內建的陣列與字典就是泛型型別的範例。
我們來看下列的範例:
class IntStore {
var items = [Int]()
func addItem(item: Int) {
items.append(item)
}
func findItemAtIndex(index: Int) -> Int {
return items[index]
}
}
IntStore
是一個儲存 Int
項目陣列的簡單類別。它提供兩個方法:
- 新增項目到 Store中。
- 從 Store中回傳一個特定的項目。
顯然地,在 IntStore
類別支援 Int
型別項目。那麼如果你能夠定義一個處理任何型別值的泛型 ValueStore
類別會不會更好呢?下列是此類別的泛型版本:
class ValueStore<T> {
var items = [T]()
func addItem(item: T) {
items.append(item)
}
func findItemAtIndex(index: Int) -> T {
return items[index]
}
}
和你在泛型函數一節所學到的一樣,使用佔位符型別參數( T
) 來表示一個泛型型別。在類別名稱後的型別參數() 指出這個類別為泛型型別。
要實例化類別,則在角括號內寫上要儲存在 ValueStore
的型別。
var store = ValueStore<String>()
store.addItem(item: "This")
store.addItem(item: "is")
store.addItem(item: "generic")
store.addItem(item: "type")
let value = store.findItemAtIndex(index: 1)
你可以像之前一樣呼叫這個方法。
計算屬性(Computed Properties)
計算屬性( Computed Properties )並沒有實際儲存一個值。相對地,它提供了自己的 getter 與 setter 來計算值。以下列的範例說明:
class Hotel {
var roomCount: Int
var roomPrice: Int
var totalPrice: Int {
get {
return roomCount * roomPrice
}
}
init(roomCount: Int = 10, roomPrice: Int = 100) {
self.roomCount = roomCount
self.roomPrice = roomPrice
}
}
這個 Hotel
類別有儲存二個屬性:roomPrice
與 roomCount
。要計算旅館的總價,我們只要將 roomPrice
乘上 roomCount
即可。在過去,你可能會建立一個可以執行計算並回傳總價的方法。有了S wift,你可以使用計算屬性來代替。在這個範例中,totalPrice
是一個計算屬性。這裡不使用儲存固定的值的方式,它定義了一個自訂的 getter 來執行實際的計算,然後回傳房間的總價。就和值儲存在屬性一樣,你也可以使用點語法來存取屬性:
let hotel = Hotel(roomCount: 30, roomPrice: 100)
print("Total price: \(hotel.totalPrice)")
// Total price: 3000
或者,你也可以對計算屬性定義一個 setter。再次以這個相同的範例來說明:
class Hotel {
var roomCount: Int
var roomPrice: Int
var totalPrice: Int {
get {
return roomCount * roomPrice
}
set {
let newRoomPrice = Int(newValue / roomCount)
roomPrice = newRoomPrice
}
}
init(roomCount: Int = 10, roomPrice: Int = 100) {
self.roomCount = roomCount
self.roomPrice = roomPrice
}
}
這裡我們定義一個自訂的 setter,在總價的值更新之後計算新的房價。當 totalPrice
的新值設定好之後,newValue
的預設名稱可以在 setter
中使用,然後依照這個 newValue
,你便可以執行計算並更新roomPrice
。
那麼可以使用方法來代替計算屬性嗎?當然可以,這和編寫程式的風格有關。計算屬性對簡單的轉換與計算特別有用。你可看上列的範例,這樣的實作看起來更為簡潔。
屬性觀察者(Property Observers)
屬性觀察者( Property Observers )是我最喜歡的 Swift 功能之一。屬性觀察者觀察並針對屬性的值的變更做反應。這個觀察者在每次屬性的值設定後都會被呼叫。在一個屬性中可以定義二種觀察者:
willSet
會在值被儲存之前被呼叫。didSet
會在新值被儲存之後立即呼叫。
再次以 Hotel
類別為例,例如:我們想要將房價限制在 1000 元。每當呼叫者設定的房價值大於1000 時,我們會將它設定為 1000。你可以使用屬性觀察者來監看值的變更:
class Hotel {
var roomCount: Int
var roomPrice: Int {
didSet {
if roomPrice > 1000 {
roomPrice = 1000
}
}
}
var totalPrice: Int {
get {
return roomCount * roomPrice
}
set {
let newRoomPrice = Int(newValue / roomCount)
roomPrice = newRoomPrice
}
}
init(roomCount: Int = 10, roomPrice: Int = 100) {
self.roomCount = roomCount
self.roomPrice = roomPrice
}
}
例如:你設定 roomPrice
為 2000
,這裡的 didSet
觀察者會被呼叫並執行驗證。由於值是大於 1000,所以房價會設定為 1000。如你所見,屬性觀察者對於值變更的通知特別有用。
可失敗轉型(Failable Casts)
as!
(或者 as?
)也就是所謂的可失敗轉型運算子。你若不是使用 as!
,就是使用 as?
,來將物件轉型為子類別型態。若是你十分確認轉型會成功,則可以使用 as!
來強制轉型。以下列範例來說明:
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! RestaurantTableViewCell
如果你不太清楚轉型是否能夠成功,只要使用 as?
運算子即可。使用 as?
的話,它會回傳一個 Optional 值,假設轉型失敗的話,這個值會是 nil
。
repeat-while
Swift 2 導入了一個新的流程控制運算子,稱作 repeat-while
,主要用來取代 do-while
迴圈。舉例如下:
var i = 0
repeat {
i += 1
print(i)
} while i < 10
repeat-while
在每一個迴圈後做判斷。若是條件為 true
,它就會重複程式碼區塊。如果得到的結果是 false
時,則會離開迴圈。
for-in where 子句
你不只可以使用 for-in
迴圈來迭代陣列中所有的項目,你也可以使用 where
子句來定義一個過濾項目的條件。例如:當你對陣列執行迴圈,只有那些符合規則的項目才能繼續。
let numbers = [20, 18, 39, 49, 68, 230, 499, 238, 239, 723, 332]
for number in numbers where number > 100 {
print(number)
}
在上列的範例中,它只會列印大於 100 的數字。
Guard
在 Swift 2 時導入了 guard
關鍵字。在 Apple 的文件中,guard 的描述如下:
一個 guard 敘述就像 if 敘述一樣,依照一個表達式的布林值來執行敘述。為了讓 guard 敘述後的程式碼被執行,你使用一個 guard 敘述來取得必須為真的條件。
在我繼續解釋 guard 敘述之前,我們直接來看這個範例:
struct Article {
var title: String?
var description: String?
var author: String?
var totalWords: Int?
}
func printInfo(article: Article) {
if let totalWords = article.totalWords, totalWords > 1000 {
if let title = article.title {
print("Title: \(title)")
} else {
print("Error: Couldn't print the title of the article!")
}
} else {
print("Error: It only works for article with more than 1000 words.")
}
}
let sampleArticle = Article(title: "Swift Guide", description: "A beginner's guide to Swift 2", author: "Simon Ng", totalWords: 1500)
printInfo(article: sampleArticle)
在上列的程式碼中,我們建立一個 printInfo
函數來顯示一篇文章的標題。不過,我們只是要輸出一篇超過上千文字的文章資訊,由於變數是 Optional,我們使用 if let
來確認是否 Optional 有包含一個值。如果這個Optional 是 nil
,則會顯示一個錯誤訊息。當你在 Playgrounds 執行這個程式碼,它會顯示文章的標題。
通常 if-else
敘述會依照這個模式:
if some conditions are met {
// 執行一些動作
if some conditions are met {
// 執行一些動作
} else {
// 顯示錯誤或執行其他操作
}
} else {
// 顯示錯誤或執行其他操作
}
你也許注意到,當你必須測試更多條件,它會嵌入更多條件。編寫程式上,這樣的程式碼沒有什麼錯,但是就可讀性而言,你的程式碼看起來很凌亂,因為有很多嵌套條件。
因此 guard
敘述因應而生。guard
的語法如下所示:
guard else {
// 執行假如條件沒有匹配要做的動作
}
// 繼續執行一般的動作
如果定義在 guard
敘述內的條件不匹配,else
後的程式碼便會執行。反之,如果條件符合,它會略過 else
子句並且繼續執行程式碼。
當你使用 guard
重寫上列的範例程式碼,會更簡潔:
func printInfo(article: Article) {
guard let totalWords = article.totalWords, totalWords > 1000 else {
print("Error: It only works for article with more than 1000 words.")
return
}
guard let title = article.title else {
print("Error: Couldn't print the title of the article!")
return
}
print("Title: \(title)")
}
有了 guard
,你就可將重點放在處理不想要的條件。甚至,它會強制你一次處理一個狀況,避免有嵌套條件。如此一來,程式碼便會變得更簡潔易讀。
錯誤處理
在開發一個 App 或者任何程式,不論好壞,你需要處理每一種可能發生的狀況。顯然地,事情可能會有所出入。例如:當你開發一個連線到雲端的 App,你的A pp 必須處理網路無法連線或者雲端伺服器故障而無法連接的情況。
在之前的 Swift 版本,它缺少了適當的處理模式。舉例而言,處理錯誤條件的處理如下:
let request = NSURLRequest(URL: NSURL(string: "http://www.apple.com")!)
var response: NSURLResponse?
var error: NSError?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
if error == nil {
print(response)
// 解析資料
} else {
// 處理錯誤
}
當呼叫一個方法時,可能會造成失敗,通常是傳遞一個 NSError
物件(像是一個指標) 給它。如果有錯誤,這個物件會設定對應的錯誤,然後你就可以檢查是否錯誤物件為 nil,並且給予相對的回應。
這是在早期 Swift 版本(也就是 Swift 1.2)處理錯誤的做法。
Note: NSURLConnection.sendSynchronousRequest() 在 iOS 9 已經不推薦使用,但因為大部分的讀者比較熟悉這個用法,所以在這個範例中才使用它。
try / throw / catch
從 Swift 2 開始,內建了使用 try-throw-catch
關鍵字,如例外( exception )的模式。相同的程式碼會變成這樣:
let request = URLRequest(url: URL(string: "https://www.apple.com")!)
var response:URLResponse?
do {
let data = try NSURLConnection.sendSynchronousRequest(request, returning: &response)
print(response)
// 解析資料
} catch {
// 處理錯誤
print(error)
}
現在你可以使用 do-catch
敘述來捕捉( catch )錯誤並處理它。你也許注意到,我們放了一個 try
關鍵字在呼叫方法前面,有了錯誤處理模式的導入,一些方法會丟出錯誤來表示失敗。當我們呼叫一個 throwing 方法,你需要放一個 try
關鍵字在前面。
你要如何知道一個方法是否會丟出錯誤呢?當你在內建編輯器輸入一個方法時,這個 throwing 方法會以 throws
關鍵字來標示,如圖 A.1 所示。
現在你應該了解如何呼叫一個 throwing 方法並捕捉錯誤,那要如何指示一個可以丟出錯誤的方法或函數呢?
想像你正在規劃一個輕量型的購物車,客戶可以使用這個購物車來短暫儲存並針對購買的貨物做結帳,但是購物車在下列的條件下會丟出錯誤:
- 購物車只能儲存最多5個商品,否則的話會丟出一個
cartIsFull
的錯誤。 - 結帳時在購物車中至少要有一項購買商品,否則會丟出
cartIsEmpty
的錯誤。
在 Swift 中,錯誤是由遵循 Error
協定的型別值來呈現。
通常是使用一個列舉(enumeration )來規劃錯誤條件。在此範例中,你可以建立一個採用 Error
的列舉,如下列購物車發生錯誤的情況:
enum ShoppingCartError: Error {
case cartIsFull
case emptyCart
}
對於購物車,我們建立一個 LiteShoppingCart
類別來規劃它的函數。參考下列程式碼:
struct Item {
var price:Double
var name:String
}
class LiteShoppingCart {
var items:[Item] = []
func addItem(item: Item) throws {
guard items.count < 5 else {
throw ShoppingCartError.cartIsFull
}
items.append(item)
}
func checkout() throws {
guard items.count > 0 else {
throw ShoppingCartError.emptyCart
}
// 繼續結帳
}
}
若是你更進一步看一下這個 addItem
方法,你可能會注意到這個 throws
關鍵字。我們加入 throws
關鍵字在方法宣告處來表示這個方法可以丟出錯誤。在實作中,我們使用 guard
來確保全部商品數是少於 5 個。否則,我們會丟出 ShoppingCartError.cartIsFull
錯誤。
要丟出一個錯誤,你只要撰寫 throw
關鍵字,接著是實際錯誤。針對 checkout 方法。我們有相同的實作。如果購物車沒有包含任何商品,我們會丟出 ShoppingCartError.emptyCart
錯誤。
現在,我們來看結帳時購物車是空的會發生什麼事情?我建議你啟動 Xcode,並使用 Playgrounds 來測試程式碼。
let shoppingCart = LiteShoppingCart()
do {
try shoppingCart.checkout()
print("Successfully checked out the items!")
} catch ShoppingCartError.cartIsFull {
print("Couldn't add new items because the cart is full")
} catch ShoppingCartError.emptyCart {
print("The shopping cart is empty!")
} catch {
print(error)
}
由於 checkout
方法會丟出一個錯誤,我們使用 do-catch
敘述來捕捉錯誤,當你在 Playgrounds 執行上列的程式碼,它會捕捉 ShoppingCartError.emptyCart
錯誤,並輸出相對的錯誤訊息,因為我們沒有加入任何項目。
現在至呼叫 checkout
方法的前面,在 do 子句插入下列的程式碼:
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #1"))
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #2"))
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #3"))
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #4"))
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #5"))
try shoppingCart.addItem(item: Item(price: 100.0, name: "Product #6"))
在這裡,我們加入全部 6 個商品至 shoppingCart
物件。同樣的,它會丟出錯誤,因為購物車不能存放超過 5 個商品。
當捕捉到錯誤時,你可以指示一個正確的錯誤(例如:ShoppingCartError.cartIsFull
)來匹配,因此你就可以提供一個非常具體的錯誤處理。
另外,如果你沒有在 catch
子句指定一個模式( pattern ),Swift 會匹配任何錯誤,並自動地綁定錯誤至 error 常數。最好的做法還是應該要試著去捕捉由 throw 方法所丟出的特定錯誤。同時,你可以寫一個 catch 子句來匹配任何錯誤,這可以確保所有可能的錯誤都有處理到。
可行性檢查(Availability Checking)
若是所有的使用者被強制更新到最新版的 iOS 版本,這可讓開發者更輕鬆些,Apple 已經盡力推廣讓使用者升級它們的 iOS 裝置,不過還是有一些使用者不願升級。因此,為了能夠推廣給更多的使用者用,我們的 App 必須應付不同 iOS 的版本(例如:iOS 12、iOS 13 與iOS 14)。
當你只在你的 App 使用最新版本的 API,則在其他較舊版本的 iOS 會造成錯誤。當使用只能在最新的 iOS 版本才能使用的 API,你必須要在使用這個類別或呼叫這個方法之前做一些驗證。
例如:UIView 的 safeAreaLayoutGuide
屬性只能在 iOS 12(或之後的版本)使用。如果你在更早的 iOS 版本使用這個屬性,你便會得到一個錯誤,也因此可能會造成 App 的閃退。
Swift 內建了API 可行性的檢查。你可以輕易地定義一個可行性條件,因此這段程式碼將只會在某些 iOS 版本執行。如下列的範例:
if #available(iOS 12.0, *) {
// iOS 12 或以上版本
let view = UIView()
let layoutGuide = view.safeAreaLayoutGuide
} else {
// 早期的 iOS 版本
}
你在一個 if
敘述中使用 #available
關鍵字。在這個可行性條件中,你指定了要確認的 OS 版本(例如:iOS 14 )。星號( *
)是必要的,並指示了 if
子句所執行的最低部署目標以及其他 OS 的版本。以上列的範例來說, if 的主體將會在 iOS 12 或以上版本執行,以及其他平台如 watchOS。
相同的,你可以使用 guard
代替 if
來檢查 API 可行性,如下列這個範例:
guard #available(iOS 12.0, *) else {
// 如果沒有達到最低 OS 版本需求所需要執行的動作
return
}
let view = UIView()
let layoutGuide = view.safeAreaLayoutGuide
那麼當你想要開發一個類別或方法,可以讓某些 OS 的版本使用呢? Swift 讓你在類別/方法/函數中應用@available
屬性,來指定你的目標平台與 OS 版本。舉例而言,你正在開發一個名為 SuperFancy
的類別,而它只能適用於 iOS 12 或之後的版本,你可以像這樣應用 @available
:
@available(iOS 12.0, *)
class SuperFancy {
// 實作內容
}
當你試著在 Xcode 專案使用這個類別來支援多種 iOS 版本,Xcode 會告訴你下列的錯誤,如圖 A.2 所示。
Note: 你不能在 Playgrounds 做可行性檢查。若你想要試試看的話,建立一個新的 Xcode 專案來測試這個功能。
本文摘自《iOS 17 App程式設計實戰心法》(Swift+UIKit)》一書。如果你想更深入學習Swift程式設計和下載完整程式碼,你可以從 AppCoda網站 購買完整電子版