iOS 17 App 程式設計實戰心法(SwiftUI)

Appendix - 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 也另外提供一個明確指定型別資訊的功能,下列的範例介紹了如何在 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")
}

陣列

Swift 中宣告陣列(Array )的語法與 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 will return 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

字典

Swift 提供三種集合型別( Collection Type ):陣列、字典與集合。我們先來討論字典( Dictionary ),每個字典中的值對應一個唯一的鍵。要在 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 )和陣列非常相似,陣列是有排序的集合,而集合則是沒有排序的集合;在陣列中的項目可以重複,但是在集合中則沒有重複值。

要宣告一個集合,你可以像這樣撰寫:

var favoriteCuisines: Set = ["Greek", "Italian", "Thai", "Japanese"]

此語法和陣列的建立一樣,不過你必須明確指定Set 型別。

如前所述,集合是不同項目、沒有經過排序的集合。當你宣告一組集合有重複的值,它便不會儲存這個值,以下列程式碼為例:

集合的操作和陣列很相似,你可以使用 for-in 迴圈來針對集合做迭代(Iterate)。不過, 當你要加入一個新項目至集合中,你不能使用 += 運算子,你必須呼叫 insert 方法:

favoriteCuisines.insert("Indian")

有了集合,你可以輕鬆判斷兩組集合中有重複的值或不相同的值,例如:你可以使用兩組集合來分別代表兩個人最愛的料理種類:

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"}

類別

在 Objective-C 中,你針對一個類別(Class )分別建立了介面(.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

方法

和其他物件導向語言一樣,Swift 允許你在類別中定義函數,即所謂的「方法」(Method )。你可以使用 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")
}

// as the speed falls within the range of 41 and 70, it'll print normal to console

switch case 可以讓你透過二個運算子 ... 與 ..< 來檢查一個範圍內的值。這兩個運算子是作為表示一個範圍值的縮寫。

例如:41...70 的範圍,... 運算子定義了從 41 到 70 的執行範圍,有包含 41 與 70。如果我們使用 ..< 取代範例中的...,則是定義執行範圍為 41 至69,換句話說,70 不在範圍之內。.

元組

Swift 導入了一個在 Objective-C 所沒有的先進型別稱為「元組」(Tuple ),元組可以允許開發者建立一個群組值並且傳送。假設你正在開發一個可以回傳多個值的方法,你便可以使用元組作為回傳值取代一個自訂物件的回傳。

元組把多個值視為單一複合值,以下列的範例來說明:

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  // compile-time error
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!  // runtime error

這種情況不會有編譯期錯誤,當強制解開啟用後,編譯器假定可選型別包含了一個值,不過當你執行 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 物件而不是字串。

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》研究有關可選鏈的資訊。

可失敗初始化器

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

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

如果字型檔案不存在或無法讀取,UIFont 物件的初始化便會失敗,初始化失敗會使用可失敗初始化器來回報,回傳的物件是一個可選型別,此不是物件本身就是 nil,因此我們需要使用 if let 來處理可選型別:

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

   // 下列為要處理的程序

}

泛型

「泛型」(Generic )不是新的觀念,在其他程式語言如 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 來代替。

你可以用相同的方式呼叫這個 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
}

你可以在型別參數名稱後面寫上一個協定的型別約束,以冒號來做區隔,這裡的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

泛型型別

在函數中,使用泛型是沒有限制的,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 )並沒有實際儲存一個值,相對的,它提供了自己的 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 即可。在過去,你可能會建立一個可以執行計算並回傳總價的方法,有了 Swift,你可以使用計算屬性來代替,在這個範例中,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 )是我最喜歡的 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,如你所見,屬性觀察者對於值變更的通知特別有用。

可失敗轉型

as!(或者 as? )即所謂的可失敗轉型( failable cast )運算子。你若不是使用 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,則會顯示一個錯誤訊息。當你在 Playground 執行這個程式碼,它會顯示文章的標題。通常 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 子句來匹配任何錯誤,這可以確保所有可能的錯誤都有處理到。

可行性檢查

若是所有的使用者被強制更新到最新版的 iOS 版本,這可讓開發者更輕鬆些,Apple 已經盡力推廣使用者升級它們的iOS 裝置,不過還是有一些使用者不願升級,因此為了能夠推廣給更多的使用者使用,我們的 App 必須應付不同 iOS 的版本(例如:iOS 13、iOS 14 與 iOS 15)。

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

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

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

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

if #available(iOS 15.0, *) {
    List {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
    .refreshable {
        // update item
    }
} else {
    // Fallback on earlier versions
    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: 你不能在 Playground 做可行性檢查,若你想要嘗試的話,可建立一個新的 Xcode 專案來測試這個功能。.