附錄 - 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 字典總共包含5 個項目。"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 類別, 並採用 UITableViewDelegateUITableViewDataSource 協定。你可以像這樣做類別宣告:

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 參數作為輸入。-> 運算子是作為方法回傳值的指示器(Indicator ),從上面的程式碼來看,你將代辦項目總回傳數的回傳型態指定為 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)")

Optionals 的介紹

什麼是 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

為何需要 Optionals?

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 與Goolge 的股票代號。其他的輸入則回傳 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 能夠改善程式碼的品質。

解除 Optionals

那麼我們該如何讓程式可以運作?很顯然的,我們需要測試 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!  // runtime error

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

Optional Binding

除了強迫解除之外,Optional Binding 是一個較簡單且比較推薦用來解除 Optional 的方式。你可以使用 Optional Binding 來檢查 Optional 是否有含值。如果它有含值,則解除它,並把它放進一個暫時的常數或變數。

沒有比使用一個實際範例來解釋 Optional Binding 的最佳方式了。我們將前面範例的範例程式轉換成 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)是 Optional Binding 的兩個關鍵字。以白話來說,這個程式碼是說:「如果 stockCode 有包含一個值,則解開它,將其值設定到 tempStockCode,然後執行後面的條件敘述,否則的話彈出這段程式」。因為 tempStockCode 是一個新的常數,你不需要使用! 字尾來存取其值。

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

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

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

Optional Chaining

在解釋 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 Binding 來檢查實際上是否有含值。很顯然的,Stock 類別的 price 屬性是 Optional,我們再一次使用 if let 敘述來測試 stock. price 是否有包含一個非空值。

上列的程式碼運作沒有問題。你可以使用 Optional Chaining 來取代巢狀式 if let 的撰寫以簡化程式。這功能可以讓我們將多個 Optional 以 ?. 連結起來。以下是程式碼的簡化版本:

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

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

可失敗化初始器(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) {
     // 執行某些動作
}

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

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 協定的型態,都可以支援相等(==)運算子。所有標準型態如 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 項目陣列的簡單類別。它提供兩個方法:

The IntStore class is a simple class to store an array of Int items. It provides two methods for:

  • 新增項目到 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 即可。在過去,你可能會建立一個可以執行計算並回傳總價的方法。有了 Swift,你可以使用計算屬性來代替。在這個範例中,totalPrice 是一個計算屬性。這裡不使用儲存固定的值的方式,它定義了一個自訂的getter 來執行實際的計算,然後回傳房間的總價。就和值存在屬性一樣,你也可以使用點語法來存取屬性:

let hotel = Hotel(roomCount: 30, roomPrice: 100)
print("Total price: \(hotel.totalPrice)")
// 總價: 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,你的 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 版本(也就是 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 關鍵字來標示。

現在你應該了解要如何呼叫一個 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 11、iOS 12 與 iOS 13)。

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

譬如說, UIViewsafeAreaLayoutGuide 屬性只能在 iOS 11 使用。如果你在更早的 iOS 版本使用這個屬性,你便會得到一個錯誤,也因此可能會造成 App 的閃退。

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

if #available(iOS 11.0, *) {
    // iOS 11 或以上版本
    let view = UIView()
    let layoutGuide = view.safeAreaLayoutGuide
} else {
    // 早期的 iOS 版本
}

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

相同的,你可以使用 guard 代替 if 來檢查 API 可行性,如下列這個範例:

guard #available(iOS 11.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 會告訴你下列的錯誤:

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

results matching ""

    No results matching ""