附錄 A - 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 loops

如果你想要迭代一定範圍的值,你可使用「 ... 」或者「..< 」運算子。這些都是在 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 )?當你在 Swift 中宣告變數,它們預設是設定為非可選型別。換句話說,你必須指定一個非 nil 的值給這個變數。如果你試著設定一個 nil 值給非可選型別,編譯器會告訴你:「Nil 值不能指定為String 型別 !」。

var message: String = "Swift is awesome!" // OK
message = nil // 編譯期間錯誤

在類別中,宣告屬性時也會應用到,屬性預設設定為非可選型別。

class Messenger {
    var message1: String = "Swift is awesome!" // OK
    var message2: String // 編譯期間錯誤
}

這個 message2 會得到一個編譯期錯誤(Compile-time Error )的訊息,因為它沒有指定一個初始值,這對那些有 Objective-C 經驗的開發者而言會有些驚訝,在 Objective-C 或其他程式語言(例如:JavaScript),指定一個nil 值給變數,或宣告一個沒有初始值的屬性, 不會有編譯期錯誤的訊息。

不過,這並不表示你不能在 Swift 中宣告一個沒有指定初始值的屬性,Swift 導入了可選型別來指出缺值,它是在型別宣告後面加入一個? 運算子來定義,以下列範例來說明:

class Messenger {
    var message1: String = "Swift is awesome!" // OK
    var message2: String? // OK
}

當變數定義為可選型別時,你仍然可以指定值給它,但若是這個變數沒有指定任何值給它,它會自動定義為 nil。

為何需要可選型別?

Swift 是為了安全性考量而設計的。Apple 曾經提過,可選型別是 Swift 作為型別安全語言的一個印證。從上列的範例來看,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)

這個函數接收一個公司名稱,並回傳對應的股票代號。從程式碼中可以看出它只能支援 Apple 與 Google 這兩家公司,回傳值可以是 AAPL、GOOG 或 nil 值。

假設Swift 沒有可選型別(Optional )的功能,那麼當我們執行上面的程式碼時會發生什麼事呢?由於這個方法對Facebook 回傳 nil 值,因此執行 App 時會丟擲出執行期例外(Runtime Exception ),最糟的情況是 App 可能會當機。

有了 Swift 的可選型別,它會在編譯期找出錯誤,而不是在執行期才發現錯誤。由於 stockCode 被定義一個可選型別,Xcode 會立即偵測到一個潛在的錯誤:「可選型別String? 的值還未解開」(value of optional type String? is not unwrapped),並且告訴你要修正它。

從範例中可以知道 Swift 的可選型別強化了 nil 值的檢查,並提供編譯期錯誤的提示給開發者,因此使用可選型別有助於提升程式碼的品質。

解開可選型別

那麼我們該如何讓程式可以運作?顯然的,我們需要測試 stockCode 是否有包含一個 nil值,我們修改程式碼如下:

var stockCode: String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
if stockCode != nil {
    let message = text + stockCode!
    print(message)
}

我們使用 if 來執行 nil 檢查,一旦我們知道可選型別必須包含一個值,我們在可選型別名稱的後面加上一個驚嘆號(!)來解開它。在 Swift 中,這就是所謂的「強制解開」(Forced Unwrapping),你可以使用 ! 運算子來解開可選型別的包裹以及揭示其內在的值。

參考上列的範例程式碼,我們只在 nil 值檢查後解開 stockCode 可選型別,我們知道可選型別在使用! 運算子解開它之前,必須包含一個非 nil 的值。這裡要強調的是,建議在解開它之前,驗證可選型別必須包含值。

但如果我們像下列的範例這樣忘記驗證呢?

var stockCode:String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
let message = text + stockCode!  // 執行期間錯誤

這種情況不會有編譯期錯誤,當強制解開啟用後,編譯器假定可選型別包含了一個值,不過當你執行App 時,就會在主控台產生一個執行期錯誤的訊息。

可選綁定

除了強制解開之外,「可選綁定」(Optional Binding )是一個較簡單且推薦用來解開可選型別包裹的方式,你可以使用可選綁定來驗證可選型別是否有值,如果它有值則解開它,並把它放進一個暫時的常數或變數。

沒有比使用實際範例的更好方式來解釋可選綁定了。我們將前面範例中的範例程式碼轉換成可選綁定:

var stockCode: String? = findStockCode(company: "Facebook")
let text = "Stock Code - "
if let tempStockCode = stockCode {
    let message = text + tempStockCode
    print(message)
}

if let(或 if var )是可選綁定的兩個關鍵字,以白話來說,這個程式碼是說:「如果 stockCode 包含一個值則解開它,將其值設定到 tempStockCode,然後執行後面的條件敘述,否則的話彈出這段程式」。因為tempStockCode 是一個新的常數,你不需要使用「!」字尾來存取其值。

你也可以透過在 if 敘述中做函數的判斷,進一步簡化程式碼:

let text = "Stock Code - "
if var stockCode = findStockCode(company: "Apple") {
    let message = text + stockCode
    print(message)
}

這裡的 stockCode 不是可選型別,所以不需要使用「!」字尾在程式碼區塊中存取其值。如果從函數回傳 nil 值,程式碼區塊便不會執行。

可選鏈

在解釋「可選鏈」(Optional Chaining )之前,我們調整一下原來的範例。我們建立了一個名為「Stock」的新類別,這個類別有 code 以及 price 屬性,且都是可選型別。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() 的回傳值是可選型別,我們使用可選綁定來驗證實際上是否有值。顯然的,Stock 類別的 price 屬性是可選型別,我們再次使用 if let 敘述來驗證 stock.price 是否包含一個非空值。。

上列的程式碼運作沒有問題。你可以使用可選鏈來取代巢狀式 if let 的撰寫,以簡化程式碼。這個功能允許我們將多個可選型別以 ?. 運算子連結起來,下列是程式碼的簡化版本:

if let sharePrice = findStockCode(company: "Apple")?.price {
    let totalCost = sharePrice * 100
    print(totalCost)
}

可選鏈提供另一種存取 price 值的方式,現在程式碼看起來更簡潔了,此處只是介紹了可選鏈的基礎概念,你可以進一步至《Apple's Swift Guide》研究有關可選鏈的資訊。

可失敗化初始器(Failable Initializers)

Swift 有一個功能稱作「可失敗化初始器」( Failable Initializers )。初始化( Initialization )是一個類別中所儲存每一個屬性設定初始值的程序。在某些情況下,實例( instance )的初始化可能會失敗。現在像這樣的失敗可以使用可失敗化初始器。可失敗化初始器的結果包含一個物件或是 nil 。你需要使用 if let 來檢查初始化是否成功。舉例而言:

let myFont = UIFont(name : "AvenirNextCondensed-DemiBold", size: 22.0)

如果字型檔案不存在或無法讀取,UIFont 物件的初始化便會失敗。初始化失敗會使用可失敗化初始器來回報。

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) {
     // 執行某些動作
}

這二個函數看起來非常相似。假設函數本身是相同,差異性在於「輸入的型別」。有了泛型,你可以將它們簡化成可以處理多種輸入型別的泛型函數:

func process<T>(a: T, b: T) {
     // 執行某些動作
}

現在它是以佔位符型別( 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? 的話,它會回傳一個 可選型別的值,假設轉型失敗的話,這個值會是 nil

repeat-while

Apple 導入了一個新的流程控制運算子,稱作 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 函數來顯示一篇文章的標題。不過,我們只是要輸出一篇超過上千文字的文章資訊,由於變數是可選型別,我們使用 if let 來確認是否 可選型別 有包含一個值。如果這個可選型別 是 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,你的App 必須處理網路無法連線或者雲端伺服器故障而無法連接的情況。

在之前的 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 版本處理錯誤的做法。

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 13 、iOS 14 與 iOS 15)。

當你只在你的 App 使用最新版本的 API,則在其他較舊版本的 iOS 會造成錯誤。當使用只能在最新的 iOS 版本才能使用的 API,你必須要在使用這個類別或呼叫這個方法之前做一些驗證。

例如: 用於 Listrefreshable 修飾器只能在 iOS 15(或之後的版本)使用。如果你在更早的 iOS 版本使用這個修飾器,你便會得到一個錯誤。

Say, the refreshable modifier for List is only available on iOS 15 (or later). If you use the modifier on older versions of iOS, you’ll end up with an error.

圖 A.2. refreshable 修飾器只適用 iOS 15及之後的版本
圖 A.2. refreshable 修飾器只適用 iOS 15及之後的版本

Swift 內建了 API 可行性的檢查。你可以輕易地定義一個可行性條件,因此這段程式碼將只會在某些 iOS 版本執行。如下列的範例:

if #available(iOS 15.0, *) {
    List {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
    .refreshable {
        // 
    }
} else {
    // 退回至早期的版本
    List {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
}

你在一個 if 敘述中使用 #available 關鍵字。在這個可行性條件中,你指定了要確認的 OS 版本(例如:iOS 15)。星號( * )是必要的,並指示了 if 子句所執行的最低部署目標以及其他 OS 的版本。以上列的範例來說, if 的主體將會在 iOS 15 或以上版本執行,以及其他平台如 watchOS。

那麼當你想要開發一個類別或方法,可以讓某些 OS 的版本使用呢? Swift 讓你在類別/方法/函數中應用@available 屬性,來指定你的目標平台與 OS 版本。舉例而言,你正在開發一個名為 SuperFancy 的類別,而它只能適用於 iOS 15 或之後的版本,你可以像這樣應用 @available

@available(iOS 15.0, *)
class SuperFancy {
    // 實作內容
}

當你試著在 Xcode 專案使用這個類別來支援多種 iOS 版本,Xcode 會顯示錯誤

Note: 你不能在 Playgrounds 做可行性檢查。若你想要試試看的話,建立一個新的 Xcode 專案來測試這個功能。

results matching ""

    No results matching ""