Xcode 12 和 Swift 5.3 新功能詳解 讓你寫出更強大的程式碼!


WWDC 2020 上個月首次在前所未有的條件下完成,全球所有開發者都有機會坐在前排,了解 Apple 今年即將發佈的所有新功能和改進。與往常一樣,Apple 介紹了許多新東西和大進步,相信我們每個人都非常興奮,希望作很多新嘗試。

這篇文章旨在介紹 Xcode 和 Swift 的新版本。 Xcode 12 的第一個 beta 版本在第一天就提供給開發者,讓每個人都有足夠的時間下載並試用。它帶著很多舊版本中缺少的新功能和工具,而這些都將對整個開發過程有很大幫助。

Xcode 12 也包含了 Swift 5.3 版本的 Bundle。Swift 語言發展得越強大,我們獲得的功能就越多,從而編寫出更好、更安全、更清晰、更強大的程式碼。 Swift 再次帶來了改進,這對所有開發者都十分有用。

所以現在,我們準備重點介紹 Xcode 12 和 Swift 5.3 的一些新功能。這篇文章只是所有新東西的一部分,我盡量挑選比較普遍的主題,讓大家都適合閱讀。我建議大家可以閱讀 Xcode 的 Release Note,查看所有的新功能,並閱讀 Swift 的 changelog,以詳細了解本篇文章未有提及的部分。

Xcode 12 的新功能

Xcode 12 的改變,在 Xcode 啟動時就已經顯而易見。運行 Xcode 12 後,你會發現,“Welcome” 窗口中缺少了創建 Playground 的選項:

Xcode-12-welcome

除了創建新專案和從 Git 儲存庫複製 (clone) 的選項之外,第三個選項是打開現有專案。要創建一個新的 Playground,現在你可以打開 File> New> Playground…選單。

SwiftUI App 的生命週期 (Life Cycle)

在創建新的 SwiftUI 專案視窗,在初始配置有一個改變,就是多了 Life Cycle 選單。

Xcode-12-app-lifecycle

選擇了 SwiftUI App 之後,專案就再也沒有 AppDelegateSceneDelegate,來處理 App 的生命週期及其各種狀態,例如 App 即將啟動、進入前景或背景等。沒有了 AppDelegate.swiftSceneDelegate.swift 檔案,代替的是一個與專案名字相同的檔案,加上 “App” 後綴。例如,MyProjectApp.swift 檔案:

現在,App 的 entry point 是採用 App 框架的結構 (structure),這是一種全新的型別,根據 Apple 官方文件,「它代表了 App 的結構和行為」。在構建單純基於 SwiftUI 的專案時,這個結構實際上是其它 UI 的基礎。

看看在上面的截圖中,MyProjectApp 結構被加了 @main 屬性 (attribute) 標記,以表示這是 App 的 entry point,而且不可能會有多於一個結構被這樣標記。此外,結構的 body 型別為 Scene,這是另一個協定,根據官方文件,它是一個要顯示給使用者的視圖層次結構 (view hierarchy) 的容器。在 WindowGroup scene 內,應該有一個 App 啟動後立即顯示的視圖實例 (instance),就是 ContentView 視圖。

我們簡單地介紹了 SwiftUI App 的生命週期、以及 AppScene 協定,讓你可以更深入地了解,如何使用可在不同平台共享的 codebase 建立專案。

Xcode Appearance And Tabs

在 Xcode 12 打開了一個專案後,我們馬上就看到其新外觀,與新的 macOS Big Sur 完美融合。幾個地方都有新的圖標 (icon),像是 Navigators、Assistants、Preferences 視窗、更新了的 Tab Bar,以及很多大大小小的改變,都提升了使用者體驗和工作效率。

尤其是在 Navigators 中,顯示的字體大小會因應我們在 Preferences > General > Sidebar icon size 的設置而自動調整:

Xcode-12-preference-sidebar-icon-size

也就是說,如果你為 App Sidebar 選擇了小、中、或大尺寸,Xcode Navigators就會相應更新;而這一個設定會被選擇的特定字體大小所覆寫。打開 Xcode Preferences,切換到 General 頁面,就會看到 Navigator Size 選單。你可以將預設的系統設置更改為需要的大小。

Xcode-12-navigator-size

除此之外,另一個有趣的部分就是 Tab。Tab 很明顯不是 Xcode 的新東西,只是現在更好用了。首先,在 Project navigator 中雙擊任何檔案都會在新的 Tab 中打開,不論是源程式碼檔案、圖像、plist 檔案、或其他檔案。Tab 的寬度也比較小,可以顯示每個檔案的圖標和名稱,而且不再是擴展到整個 Xcode 視窗的寬度,並且可以根據需要重新排列。

Xcode-12-tab

在預設情況下,當打開第二個檔案時,Tab Bar 就會變為可見。但是,你也可以在 View > Always Show Tab Bar 中,把它設定為總是可見。

自動完成 (Code completion)

Xcode 12 也改善了自動完成功能,最明顯的改進是:

  1. 新的 UI 佔用的空間更少,看起來更精簡,信息量更大。
  2. 自動完成建議只會包含與輸入的程式碼相關的項目,而不會包含其他不相關的項目。
Xcode-12-code-completion

然而,你要是在 Xcode 12 中開始使用自動完成功能,就會發現它變得更快了,根據 Apple 的說法:

現在 Swift 函式的自動完成功能,比 Xcode 11.5 快了 12 倍。

相信到我們在 Xcode 12 全速工作時,就會感受到這個改變了!到目前為止,自動完成功能整體來說感覺還不錯,體驗也令人滿意。

SwiftUI 預覽

SwiftUI 預覽功能是一個很好的工具,不僅讓我們無需在設備或模擬器上運行,都可以實時預覽 App 的 UI,而且還可以快速構建出色的 UI。在 Xcode 12 中,預覽功能有了新的外觀。現在,在每個畫布預覽上方都有一個帶有按鈕的欄位,提供了我們需要的所有功能:

讓我們從左到右看看每個按鈕的功能吧:

  1. 點擊第一個按鈕,就會在 Xcode 運行當前 SwiftUI 視圖的實時預覽。長按這個按鈕的話,就會顯示一個選單,讓我們選擇運行實時預覽還是除錯 (debug) 預覽。
  2. 點擊第二個按鈕,就會在設備上運行預覽。
  3. 而第三個是 Inspect Preview 按鈕。點擊按鈕就可以為任何選定的視圖添加或編輯修飾符 (modifier)。請注意,對於新添加的修飾符,自動完成功能都會創建可編譯的程式碼,讓我們無需提供一個確實的數值,都可以進行預覽,並馬上看到修改的結果。另外,你也可以在 Attributes inspector 找到與 Inspect Preview 相同的功能。
  4. 最後一個按鈕可用於複制預覽。如果您想嘗試傳遞不同的引數 (argument) 來測試視圖,或使用不同的配色來看看其外觀,就可以使用這個功能。
Xcode-12-swiftui-preview

Xcode 12 預覽功能最強大的地方,是我們可以直接在預覽中添加控件和媒體,而相應的程式碼會實時地更新,還可以透過 Attributes inspector 就這樣為添加的視圖編輯修改器。這樣一來,我們連一行程式碼都無需編寫,就可以構建出整個視圖。 Xcode 會處理缺少了的程式碼,或在你直接在畫布上配置 UI 時更新程式碼。要了解更多,我強烈建議你去觀看 WWDC 的 Visually edit SwiftUI views 短片。

除此之外,Xcode 12 預覽功能支援預覽 Widget 和 App Clips。而且,根據 Apple 的 Xcode Release Note,現在我們可以在 iOS 14 和 iPadOS 14 使用預覽 App, 來直接在設備上試用 Swift UI 預覽功能,以獲得更真實的體驗。

添加客製化 SwiftUI 視圖和修飾符到 Xcode 儲存庫

在 Xcode 11.5,我們可以將客製化程式碼片段添加到 Xcode 的儲存庫中,並可在每個新專案中重用。現在,Xcode 12 進一步擴展了此功能,讓我們可以將整個 SwiftUI 視圖和修飾符添加到儲存庫中,以便可以重用或共享。

為了把客製化視圖添加到 Xcode 儲存庫,我們需要做一點標準的步驟和操作。

首先,假設我們已經實作了一個非常簡單的視圖,當中有一個文本,設定了 padding、特定的文本顏色和字體大小: 

struct CustomizedText: View {
    var message: String?
    var body: some View {
        Text(message ?? "Your text here").padding()
            .accentColor(.red)
            .font(.title)
    }
}

你可以在現有的 SwiftUI 源程式碼檔案中完成以下步驟,但是為了表達得更清楚,我們將創建一個新檔案,並將內容分開。

現在,從 File > New > File… 選單添加一個新的 Swift 檔案到專案中(而不是SwiftUI 檔案中),並命名為 LibraryContent.swift(你也可以改成其他名稱)。

首先,在這個新檔案中,匯入 SwiftUIDeveloperToolsSupport 框架。

import DeveloperToolsSupport

然後,我們需要實作一個採用 LibraryContentProvider 協定的新結構。為了將 SwiftUI 視圖添加到 Xcode 儲存庫中,我們必須從 LibraryContentProvider 中實作一個名為 views 的計算屬性 (computed property)。

在這個屬性中,我們必須回傳 LibraryItem 物件的集合。我們必須使用要添加到 Xcode 儲存庫的 SwiftUI 視圖實例,來初始化每個這樣的物件。

最簡單來說,我們可以這樣實作:

struct LibraryContent: LibraryContentProvider {
    @LibraryContentBuilder var views: [LibraryItem] {
        LibraryItem(CustomizedText())
    }
}

無需構建專案,Xcode 就會解析上面的程式碼,並將 CustomizedText 視圖添加到儲存庫中。要查看它的運行情況,你可以切換到任何 SwiftUI 視圖的實作檔案中,單擊右側 Xcode 工具欄上的加號 (+) 按鈕以顯示儲存庫,然後向下滾動去找 Customized Text 物件的條目: 

像其他程式碼段一樣,你可以直接將它拖放到源程式碼中使用。

這真是一個很酷的功能!我們還可以添加參數 (parameter) 到 LibraryContent.swift 檔案內 LibraryItem 物件的初始化中。讓我們把這一段程式碼:

LibraryItem(CustomizedText())

以這一段取代:

LibraryItem(CustomizedText(),
    title: "My Customized SwiftUI Text",
    category: .control
)
  • 上面的第一個引數是行內初始化的 CustomizedText 視圖實例。
  • title 是將在視圖儲存庫中顯示的標題。
  • category 是視圖所屬的類別。一共有四種可用的類別:controllayouteffect、和 other。當你不確定要用哪一種類別的時候,就可以使用 .other。如果你沒有設定 category,也會預設使用 .other。

現在再打開 Xcode 儲存庫,你將會客製化視圖的條目已經相應更新:

你會看到 title 用作描述視圖,而 categorycontrol

這看起來不錯,因為它顯示了在專案創建可重用的 SwiftUI 視圖的路徑。但是,如果你現在創建一個新專案並打開 Xcode 儲存庫,就會發現無法在可用視圖列表中找到那個客製化視圖!為什麼會這樣呢?

上面的幾行程式碼,是實作一個符合 LibraryContentProvider 協定的 LibraryContent struct。Xcode 所做的是在源程式碼檔案中找尋採用 LibraryContentProvider 的結構,找到後就實時向儲存庫添加客製化 SwiftUI 視圖(和修飾符)。因此,Xcode 儲存庫的客製化視圖在同一專案中完全可用,但在新專案中卻找不到。

那麼如果客製化視圖不可重用,我們做的步驟又有什麼意義呢?

其實只要把客製化 SwiftUI 視圖和修飾符包裝在 Swift package 中,並把 Swift package 作為 dependency 添加到新專案中,它就可以重用了。 Xcode 將在其中找 LibraryContentProvider 的實作(以及實作視圖和修飾符的源檔案),並將所有新項目添加到儲存庫中。這也是與其他人共享 SwiftUI 視圖的方法!

要在 Xcode 儲存庫添加修飾符,方法都十分相似。這一點我留給你自己探索,在此之前,請先閱讀這篇文章,以及有關 DeveloperToolsSupport 框架的資料。

Interface Builder Minimap

對於使用 Interface Builder 和 storyboards 或 XIB 檔案的開發者(包括我自己),Xcode 12 的這個功能應該十分有用,就是提供畫布概覽 (overview) 的 Minimap。雖然這聽起來沒什麼大不了的,但是當畫布有許多視圖或視圖控制器時,就會很難導航。有了 Minimap,我們只需在上面拖動就可以輕鬆導航,並且可以快速瀏覽界面上所有的 UI 項目。

要開啟和關閉 Minimap,我們可以到 Editor > Canvas > Minimap 選單,或單擊 Adjust Editor Options 按鈕以顯示選單,兩個方法都可以讓你找到 Minimap 的選項。

除了 Minimap 之外,Interface Builder 還有其他新功能,當中很多都與 macOS 有關。例如,有多個控件已經更新了,它們的外觀與 iOS 和 iPadOS 上的控件相似(像是 slider 和 progress bar)。另外,iOS 中的 Safe Area Layout Guides 現在也可見於 NSViews。但最重要的是,它引入了一個全新的控件 ── 一個帶有 sidebar 物件的視窗控制器 (window controller),讓我們可以製作出可以利用 macOS Big Sur 全部功能的 App。我們將會在另一篇文章討論這些 macOS 的改變。

Asset Catalog

最令我興奮的 Xcode 12 功能,是它現在支援 SVG (Scalable Vector Graphics) image asset!簡而言之,只要一個 SVG 圖像就可以適用於任何顯示尺寸!

之前,如果我們想在 Xcode 添加 Vector 圖像,就必須將原本的 SVG 圖像轉換為PDF,然後在 Asset Catalog 中使用 PDF 形式。或者,如果沒有 Vector 圖形,就要把 image asset 點陣化 (rasterize),並提供三個比例 (@1x, @2x, @3x) 的 PNG檔。

在 Xcode 12 中使用 SVG 非常簡單。首先,將 SVG 文件添加到 Asset Catalog:

然後,像用其他圖像一樣使用就可以了:

另一個改進是關於添加到 Asset Catalog 的 color sets。現在在預設情況下,視窗會展示 Dark Appearance 的顏色。如果 Light Mode 和 Dark Mode 你都想指定顏色的話,就可以在 Appearances 彈出選單中的 Attributes inspector 進行更改。

假設以下就是我們將在文本控件中使用的文本顏色。為了作示範,我為 Light Appearance 指定了藍色,為 Dark Mode 模式指定了黃色。

如此一來,文本控件就會根據指定的 Appearance 使用相應的顏色: 

此外,在右鍵選單 (context menu) 中,可以在 Asset Catalog 中添加新 Asset 的功能也更改了。現在,選單更加緊湊,井井有條地把不同平台分成不同的子選單,讓我們找 Asset 的時候更方便。

最後一個關於 Asset Catalog 的更新,就是現在當添加 App 圖標時,它會為所有可用的變數體顯示所需像素大小 (pixel size):

模擬器 (Simulator) 的進步

在 Xcode 12,iOS 模擬器也有些新功能和改進。第一個也是最明顯的新功能,就是現在模擬器可以切換到全屏模式。這是一種全新的體驗,尤其是如果你有一個大屏幕來觀看效果的話。

Xcode-12-simulator-ios

此外,現在即使模擬器不在 focus 狀態,它還是可以保留在其他窗口之上,這在錄影模擬器屏幕時非常有用,因為你會想模擬器一直在其他窗口之上,或是在切換模擬器與其他 App 時,還是需要模擬器在可見狀態。你可以在模擬器的 Window > Stay On Top 選單開啟和關閉這功能。

最後,有一個我認為非常方便的功能,就是模擬器支援 Nearby Interactions。只要你有兩台或以上支援 Nearby Interactions 的模擬器,並移動屏幕,就可以感應對方的距離和方向。

Swift Package 和資源 (resources)

從 Xcode 12 開始,任何使用 Swift 5.3 版本創建的 Swift Package 都可以包含資源及其源程式碼,Asset Catalog、Storyboard 檔案、圖像、文本檔案,.plist 檔案等。

常見資源型別將由 Xcode 自動處理,例如 Interface Builder 檔案、Core Data 檔案、Asset Catalog、和 .lproj 資料夾等資源,Xcode 無需任何配置就可以處理。但是,對於其他型別的資源,我們就需要在 Package 設定檔 (manifest) 中作宣告。

以上面的屏幕截圖為例,Xcode 會自動處理 Media.xcassets catalog 和MyView.xib 檔案。但是,有一個名為 notes.txt 的檔案,我們需要在設定檔中作宣告,而宣告必須在所屬 target 的 initializer 內進行。

.target(
    name: "MyPackage",
    dependencies: [],

    // Declare the resource.
    resources: [
        .process("Resources/notes.txt")
    ]
),

如果你要宣告的檔案位於子資料夾中,就需要把子資料夾的名稱加在檔案名稱前面,就像上面的程式碼一樣。

在上面的範例中,還有另一個名為 temp.txt 的檔案。假設我們想把檔案包含在 Package 中,但不是以資源的形式。在這種情況下,我們必須明確排除 (exclude) 它。這個宣告也是必須在 target 的 initializer 內進行。請注意,我們需要在宣告資源之前先宣告要排除的項目:

.target(
    name: "MyPackage",
    dependencies: [],

    // Exclude resources.
    exclude: ["Resources/temp.txt"],

    resources: [
        .process("Resources/notes.txt")
    ]
),

除了上述內容之外,我還想要強調兩個新功能:

  1. 首先,請記住,本地化 (localized) 的內容可以包含在 Swift Package 的任何資源中。
  2. Xcode 12 的 Swift packages 也包含了 binary 框架 (XCFrameworks)!這一點非常重要,因為這個框架允許我們使用 Swift Package 發佈閉源 (closed-source) 程式碼,並讓我們把框架作為 dependency 包含在其中!我不會在這裡深入討論這個主題,因為 AppCoda 將會有另一篇教學討論這一點!

你可以在 Apple 官方文件中找到更多關於在 Swift Package 添加資源的資料。

備註:你可以閱讀這篇文章,學習如何實作 Swift Package。

Swift Package 和 Playground

說到 Swift Package,它與 Xcode 12 的 Playground 可以很好地配合使用。這一點有多重要呢?

Playground 是一種很棒的方式,將兩個不同的功能加到 Package 中: 

  1. Playground 可以提供有關 Package API 和源程式碼的文件和說明,以取代預計的描述。
  2. 更重要的是,Playground 可以包含 runnable 程式碼,以展示 Package 的功能。

假設我們有一個 Swift Package 名為 MyPackage,其中包含各種數學計算的實作。在我們的範例中,只有一種方法,用來計算一個整數數字陣列的平均值:

public struct MyPackage {

    /// Calculate the average of the given integer numbers.
    public static func calculateAverage(of numbers: [Int]) -> Double? {
        guard numbers.count > 0 else { return nil }
        let sum = numbers.reduce(0, +)
        return Double(sum / numbers.count)
    }

}

現在,讓我們把 Playground 添加到 Package 內,它會使用上面的方法,同時顯示一些文件。要將新的 Playground 添加到 Package,我們需要先在 Xcode 中點選 File > New > Playground… 來創建一個 Playground。讓我們先創建一個 Playground 並儲存,將其命名為 Demo.playground。 Xcode 準備好後,我們把它關閉!

回到 Swift Package,如你在下面的截圖可見,我建立了一個名為 Playgrounds 的資料夾。右擊資料夾並選擇 Add files to “Playgrounds”…,或是就這樣把 Demo.playground 檔案從 Finder 拖放到 Project navigator 中,並放在 Playgrounds 資料夾下。

現在,Playground 檔案已經包含在 Package 內了。

所有新的 Playground 都會包含了一些我們不需要的初始程式碼,因此打開 Demo.playground 檔案後,第一步就是將初始程式碼刪除。然後,我們可以如此添加文件和範例程式碼:

/*:
 ## MyPackage documentation
 */

/*:
 **Step 1:** Import {{EJS0}}.
 */

import MyPackage

/*:
 **Step 2:** Create an array with integer numbers.
 */
let numbers = [5, 8, 12, 19, 24, 33, 51, 65, 80, 94]

/*:
 **Step 3:**
 Use the {{EJS1}} static method from {{EJS2}} to calculate the average of any given array of integer numbers.
 */

let average = MyPackage.calculateAverage(of: numbers)

上面的範例程式碼非常簡單:首先我們匯入 MyPackage 模組 (module),然後宣告一個整數數字的陣列,最後利用 MyPackage 內的 calculateAverage(of:) static 方法,來計算數字的平均值。

而且,我們會有一些標記了的程式碼區塊 (code blocks),以指引如何使用 API。但它看起來不太漂亮對吧!

要改變這一點,我們可以到 Inspectors 打開 File inspector。在 Playground Settings 點擊 Render Documentation 的 checkbox,以啟用這個功能。如此一來,就會看到閱讀得更舒服的文件了!

更好的是,我們可以運行 Playground 程式碼,並查看計算結果:

Xcode-swift-package-documentation

以上的範例雖然是簡化了,但目標是讓你了解到我們可以簡易地在 Xcode 12 添加 Playground 到 Swift Package,並在可執行的範例程式碼中提供文件。

觀看這個 WWDC 2020 視頻,以了解更多相關信息!

其他功能

前文只涵蓋了 Xcode 12 部分的新功能和改進,新的東西很多,而我只重點說明了大部分開發者都會感興趣的功能。Xcode 在其他方面也有改進,例如是測試、除錯、Create ML、工具等,所以我建議你閱讀 Xcode 12 Release Note。Organizer 和 Devices & Simulators 視窗也有改善,比如說,Organizer 現在把 App 不同的報告和指標 (metrics) 分門別類,在視窗的 sidebar 顯示。當然,還有其他很棒的功能,例如本地 App 內購買測試,我希望在下一篇教學文章深入說明這個功能。

是時候說說下一個重點,看看 Xcode 12 內的 Swift 5.3 有甚麼新功能吧!

Swift 5.3 的新功能

2014 年 Swift 1.0 的發佈感覺仿如昨天。六年後的今天,我們已經在談論 Swift 5.3 帶來的重大改進,讓 Swift 開發更輕鬆、安全、和整潔。在下文中,我們將重點介紹 5.3 版本中的一些主要功能。讓我們開始吧!

Multi-Pattern Catch 字句

直到 Swift 5.2 版本,要在 do-catch 敘述 (statement) 中處理多個錯誤,都需要在單個 catch 字句中使用 switch 敘述或 if-else 條件,以分別處理不同的錯誤情況;又或是需要多個 catch 字句,而每個字句只能處理一個錯誤。從 Swift 5.3 開始,我們可以在一個 catch 區塊中以逗號分隔不同的錯誤情況來處理。

讓我們以以下的 do-catch 敘述為例,我們在 do 區塊中使用客製的 API,對 SQLite 數據庫執行 insert 操作:

do {
    try insert(into: "table", values: [...])
} catch { ... }

假設有一個名為 DBError 的客製錯誤型別,如果上述的操作失敗,可以因應失敗原因回傳不同的錯誤情況。讓我們試著使用多個 catch 區塊,來分別處理不同錯誤情況:

do {
    try insert(into: "table", values: [...])
}
catch DBError.tableNotExist(let tableName) {
    print("\(tableName) table does not exist")
}
catch DBError.missingAllValues, DBError.missingSomeValues {
    print("There are missing values")
}
catch DBError.databaseNotOpen {
    print("Database is not open")
}
catch {
    print("Something went wrong and record could not be inserted to database")
}

新功能就在第二個 catch 字句中!我們在一個 catch 區塊中處理了兩種不同的錯誤情況,也就是 DBError.missingAllValuesDBError.missingSomeValues 錯誤,我們用逗號分隔了這兩個錯誤。我們不需要將它們分成兩個不同的 catch 區塊,這個功能讓我們可以組合有類似處理方法的錯誤情況。

@escaping 閉包內不需要 self

直到現在,為了避免循環引用 (reference cycle),在閉包中引用當前實例時,我們必須將 self 關鍵字包含在閉包中。但是,在標有 @escaping 屬性的閉包中,而且不太可能發生循環引用的這,Swift 5.3 讓我們可以避免重複編寫 self,只要我們將其明確包含在閉包的捕獲列表 (capture list) 中即可。

讓我們舉個例來說清楚一點吧!以下是一個從遠程 URL 獲取圖像的方法。完成方法後,它將向 completion 傳遞獲取的圖像數據;或是如果由於某種原因未能獲取任何圖像,則傳遞 nil:

func fetchImage(from url: URL, completion: @escaping (_ image: Data?) -> Void) {
    // Logic implementation that fetches the image from the given URL.

    // At the end call the completion handler passing
    // a hypothetical imageData object that either contains
    // the fetched image data or it's nil.
    completion(imageData)
}

在 Swift 5.3 推出之前,我們會這樣做,以在上述內容後引用 self 屬性和方法:

fetchImage(from: Some_URL { (data) in
    guard let imageData = data else { return }

    self.didFetchImage = true

    self.updateUI(with: imageData)            
}

而我們現在不一定要這樣做了,我們可以明確地把 self 包含在閉包的捕獲列表中,而不在閉包內使用它:

fetchImage(from: Some_URL { [self] (data) in
    guard let imageData = data else { return }

    didFetchImage = true

    updateUI(with: imageData)
}

合成 Enum 型別對 Comparable 的遵從性

讓我們看看以下的 Performance Case 的 enum,Case 是按語義排序的:

enum Performance {
    case bad
    case average
    case good
    case excellent
}

假設我們想如此比較以上 enum 的 Case:

let expectedPerformance = Performance.good
let userPerformance = Performance.average

if userPerformance < expectedPerformance {
    print("Not so good. Please try again.")
}

在 Swift 5.3 之前,我們要先做幾個步驟讓上述實作成功:

  1. 遵從 Comparable 協定。
  2. 實作一個 <> 的 required 函式,以執行實際比較。
  3. Performance 提供一個 Int 的原始型別,讓我們可以比較整數原始值

程式碼如下:

enum Performance: Int, Comparable {
    case bad
    case average
    case good
    case excellent

    static func < (lhs: Performance, rhs: Performance) -> Bool {
        return lhs.rawValue < rhs.rawValue
    }
}

有了 Swift 5.3,我們就不需要這樣做了。Swift 會自動合成對 Comparable 的遵從性:

enum Performance: Comparable {
    case bad
    case average
    case good
    case excellent
}


let expectedPerformance = Performance.good
let userPerformance = Performance.average

if userPerformance < expectedPerformance {
    print("Not so good. Please try again.")
}

合成了的 Comparable 遵從性也適用於有關聯值 (associated values) 的enums,只要數值型別也遵從 Comparable 協定。看看以下例子:

enum Age: Comparable {
    case kid(age: Int)
    case young
    case middleAged
    case oldAged
}


let ages = [Age.young, Age.kid(age: 12), Age.oldAged, Age.kid(age: 5)]
let sortedAges = ages.sorted()
print(sortedAges)

// Prints:
// Age.kid(age: 5), Age.kid(age: 12), Age.young, Age.oldAged]

Enum 可用作 Protocol Witness

讓我們看看以下協定(來源:Swift ChangeLog):

protocol P {
  static var foo: Self { get }
  static func bar(value: Int) -> Self
}

這裡有一個 Self 型別的 get-only 靜態屬性 (foo)、以及一個帶有回傳 Self. 參數的靜態函式。在 Swift 5.3 推出之前,如果 enum 遵從這樣的協議,我們就必須如此實作所需的屬性和功能:

enum E: P {
    case _foo
    case _bar(value: Int)

    static var foo: Self {
        return ._foo
    }

    static func bar(value: Int) -> Self {
        return ._bar(value)
    }
}

你會看到 enum 的 Case 與協定的屬性相同,而相對的函式就需要重新命名(例如是 ._foo 而不是 foo),而且實作協定的要求是強制性的。然而,前面那些可有可無的程式碼讓 enum 變得繁複。

這些限制在 Swift 5.3 已經消除了,所以:

  • 一個 Self 型別的 get-only 靜態屬性,可以由一個沒有關聯值的 Enum Case 所 witness
  • 一個帶有回傳 Self. 參數的靜態函式,可以由一個有關聯值的 Enum Case 所 witness

也就是說,在 Swift 5.3 中,我們只需要用以下的程式碼即可:

enum E: P {
  case foo // matches 'static var foo'
  case bar(value: Int) // matches 'static func bar(value:)'
}

多重尾隨閉包 (Multiple Trailing Closures)

多重尾隨閉包是一個簡單的語法糖 (syntactic sugar),可以讓程式碼更簡潔易讀。

讓我們先回憶一下 UIKit 框架,尤其是 UIView 動畫。我們知道一個簡單的動畫可以寫成這樣:

UIView.animate(withDuration: 0.4, animations: {    
    // perform animation...    
}, completion: { (_) in
    // do something upon completion...
})

或是更加簡單:

UIView.animate(withDuration: 0.4, animations: {
    // perform animation...    
}) { (_) in
    // do something upon completion...
}

在第二段程式碼中,右括號是跟在 animations 閉包後的,而且省略了 completion 標籤 (label)。但是在 Swift 5.3 中,我們可以再簡化上面的程式碼:

UIView.animate(withDuration: 0.4) {
    // perform animation...
} completion: { (_) in
    // do something upon completion...
}

你會看到現在右括號是跟在第一個閉包後的,而不再使用 animations 標籤。但在這個情況下,我們就需要使用 completion 標籤。可以看到現在程式碼更易讀!

同樣的方法也適用於 SwiftUI 控件,最好的例子就是按鈕。以下的範例程式碼,是一個有 action 和 label 閉包的按鈕:

Button(action: {
    // do something...
}, label: {
    Text("Click Me!")
})

現在,我們可以省略 action 標籤,並可以更進一步 ── 因為這裡沒有參數值,我們甚至可以省略括號!

Button {
    // do something...
} label: {
    Text("Click Me!")
}

Float16 型別

Swift 5.3 引入了半精度浮點 (Half-precision floating-point) 數值型別 Float16。根據 Apple 的說法,這個型別專門用於圖形與 ML App 中。

let floatNumber: Float16 = 5.4

完善 didSet 語義

在 Swift 5.3 之前,無論是否需要舊數值 (oldValue),didSet observer 都會調用實作該屬性的 getter,以獲取幕後的舊數值。所以執行 didSet 的次數越多,效率就會隨之下降。

在 Swift 5.3 中,如果 oldValue 沒有明確被引用,在實作 didSet observer 的時候,就不需要調用屬性的 getter。你可以看看以下 SE-0268 的例子:

class Foo {
  var bar = 0 {
    didSet { print("didSet called") }
  }

  var baz = 0 {
    didSet { print(oldValue) }
  }
}

let foo = Foo()
// This will not call the getter to fetch the oldValue
foo.bar = 1
// This will call the getter to fetch the oldValue
foo.baz = 2

baz 屬性中,print 敘述引用了 oldValue,因此我們需要在 didSet 調用其 getter。相反,在 bar 屬性中 didSet 沒有調用 oldValue,因此也不會調用 getter。

在 lazy 屬性中的 willSet 和 didSet Observer

現在,lazy 屬性也支援 willSetdidSet 屬性 Observer。看看以下的例子:

class C {
  lazy var property: Int = 0 {
    willSet { print("willSet called!") } // Okay
    didSet { print("didSet called!") } // Okay
  }
}

(來源:Swift ChangeLog

經常使用這些 Observer 的開發者應該十分喜歡這個新功能。

總結

上文展示了 Xcode 12 和 Swift 5.3 最重要的新功能,希望大家去試一下這些功能。儘管文章涵蓋了不同的主題,但仍有更多探索空間。新引入的功能和改進太多了 ── 尤其是 Xcode 中的,實在無法全寫在同一篇文章中,因此我再次建議大家到官方網站閱讀 Xcode 和 Swift 的新功能。我們在文章提過,希望針對一些主題寫獨立教程,相信很快就會在 AppCoda 上發佈,請大家密切留意,不要錯過這些有趣的主題!

譯者簡介:Kelly Chan-AppCoda 編輯小姐。
原文:What’s New in Xcode 12 and Swift 5.3


資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。

blog comments powered by Disqus
Shares
Share This