附錄 - 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 中, 開發者在宣告變數時,必須明確地指定型別的資訊,如 intdouble 或者 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 中,為了指定字串是否可變更,你必須在 NSStringNSMutableString 類別間做選擇。而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 中,你可以將任何物件放進 NSArrayNSMutableArray ,而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]?
}

在上列的程式碼中, nameingredients 屬性自動被指定一個 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 類別,並採用 UITableViewDelegateUITableView 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 中,無法在 NSStringswitch 。你必須用數個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 值檢查後解開 stockCodeOptional,我們知道 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 協定的型別,都可以支援相等(==)運算子。所有標準型別如 StringIntDouble 都支援 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 類別有儲存二個屬性:roomPriceroomCount 。要計算旅館的總價,我們只要將 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
    }
}

例如:你設定 roomPrice2000 ,這裡的 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 所示。

圖 A.1. throwing 方法會以throws 關鍵字來標示
圖 A.1. throwing 方法會以throws 關鍵字來標示

現在你應該了解如何呼叫一個 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 所示。

圖 A.2. 如果你使用一個非 iOS 目標版本所支援的類別,Xcode 會顯示出錯誤
圖 A.2. 如果你使用一個非 iOS 目標版本所支援的類別,Xcode 會顯示出錯誤
Note: 你不能在 Playgrounds 做可行性檢查。若你想要試試看的話,建立一個新的 Xcode 專案來測試這個功能。

本文摘自《iOS 17 App程式設計實戰心法》(Swift+UIKit)》一書。如果你想更深入學習Swift程式設計和下載完整程式碼,你可以從 AppCoda網站 購買完整電子版

results matching ""

    No results matching ""