iOS App 程式開發

SwiftUI 初體驗: 建構一個簡單 App 讓你了解 SwiftUI 有多強大!

SwiftUI 初體驗: 建構一個簡單 App 讓你了解 SwiftUI 有多強大!
SwiftUI 初體驗: 建構一個簡單 App 讓你了解 SwiftUI 有多強大!
In: iOS App 程式開發, Swift 程式語言, SwiftUI 框架, Xcode

在今年 WWDC 2019 大會之中,最令人振奮的主題演講莫過於開發者工具的改善,而其中最大而最棒的消息就是 SwiftUI 的發佈。SwiftUI 是一個全新的框架,讓你用更少程式碼、並以宣告的方式,來設計及開發使用者介面。

SwiftUIUIKit 不同,UIKit 通常要配合 storyboards 一起使用,而 SwiftUI 則完全建構在程式碼之上。不過,SwiftUI 的語法相當容易理解,而且可以透過 Automatic Preview 來快速預覽我們設計的介面。

因為 SwiftUI 是使用 Swift 所建構而成,讓你可以使用更少的程式碼來完成複雜的 App。更重要的是,使用 SwiftUI 能夠讓你的 App 自動支援一些進階的功能,像是動態型別 (Dynamic Type)、黑暗模式 (Dark Mode)、本地化 (Localization)、以及輔助使用 (Accessibility)。此外,SwiftUI 支援所有的平台,包括 macOS、iOS、iPadOS、watchOS 以及 tvOS。所以現在,你的 UI 程式碼能夠在所有平台上同步,這樣一來你就可以專注於平台的特定程式碼。

關於本次教學

對於開發人員來說,盡早熟悉如何使用 SwiftUI 是非常重要的,因為 Apple 會漸漸將重心轉移到這個框架上。在本次教學中,我們將會介紹 SwiftUI 的基礎知識,並建構一個簡單的導師列表 App,來顯示我們教學團隊的所有成員,從中探索如何創建導航視圖、圖像、文字、和清單。當你點擊某個成員,App 就會進入詳細資料視圖,展示成員的照片及簡介。好,讓我們開始吧!

本次教學需要在 Xcode 11 上執行。在撰寫本篇教學時,Xcode 11 還在 beta 版本,所以有些功能可能運作得未如預期。我們將會在本次教學中使用 Swift 5,雖然不會用到 Swift 的進階知識,但是還是建議你需要有 Swift 的基礎知識。

編者註解:為了從 Xcode 的視窗中預覽視圖、並與視圖互動,請確認你的 Mac OS 版本為 macOS 10.15 beta。

使用 SwiftUI 設定專案

讓我們從零開始吧,這樣你就能夠瞭解如何立即執行一個 SwiftUI App。首先,打開 Xcode,並點擊 Create new Xcode project。在 iOS 之下選擇 Single View App,並為專案命名。然後在下方勾選 Use SwiftUI 的選項,如果沒有勾選該選項的話,Xcode 會自動產生 storyboard 檔案。

use-swiftUI

現在 Xcode 會自動幫你創建一個名為 ContentView.swift 的檔案,當中最神奇的地方,是它會在程式碼的右邊呈現一個即時的預覽視窗,如同下圖所示:

instant preview

假如你沒有看到預覽畫面,請點擊預覽視窗中的 Resume 按鈕。建構專案是需要一點時間的,請稍等一下。

現在讓我們開始來看看,如何修改這個檔案來創造我們的 App 吧。

建構列表視圖 (List View)

要建構列表視圖有三個部分,第一部分就是要在列表中創建行 (row),你會發現這種設計與 UITableView 類似。為了創建行,我們要創建一個 ContactRow。第二部分是將我們需要的數據連接到列表中。我已經編寫了數據相關的程式碼,只需要做一些修改,就可以將列表連接到數據。最後一部分,我們只需要添加一個導航列,並將列表嵌入到導航視圖中就可以了。是不是很簡單呢?讓我們看看如何在 SwiftUI 中實現這些步驟吧!

創建導師成員列表

首先,讓我們創建一個列表視圖,以呈現所有教學團隊成員的個人照片及簡介。讓我們看看如何做到這一點。

如你在產生出來的程式碼中所見,我們已經有了包含著“Hello World”值的 Text 元件。在程式碼編輯器中,將程式碼的值改為“Simon Ng”。

struct ContentView: View {
    var body: some View {
        Text("Simon Ng")
    }
}

如果都順利的話,你應該會看到右邊的預覽視窗自動更新,這就是所有人都期待已久的即時預覽功能!

swiftUI-preview-1

讓我們加入另一個 Text 視圖到 App 之中,這會是我們成員的簡介。要加入新的 UI 元件到 App 之中,按下右上方角落的 + 按鈕,一個新視窗就會出現,列出不同的視圖,把標題為 Text 的視圖拖出,並將它放入到我們初始 Text 視圖的下方,如下圖所示:

swiftUI-preview-2

同時注意左方的程式碼:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Simon Ng")
            Text("Placeholder")
        }
    }
}

你可以看到,一個新的 Text 視圖已經加入到 Simon Ng 文字視圖的下方。不同的是,新的視圖被一個叫做 VStack 的東西包裹著。VStack 其實是垂直堆疊 (Vertical Stack) 的縮寫,是在 SwiftUI 中用來取代 Auto Layout 的功能。假如你有開發過 watchOS,你會知道它沒有任何約束條件,所有的東西都會被加入到群組之中。透過垂直堆疊,我們所有的視圖都可以以垂直的方式排列。

現在,將“Placeholder”的文字改寫為“Founder of AppCoda”。

接著,讓我們加入一張圖片到這個文字的左邊。因為我們想將視圖以水平方式排列到現有的視圖旁邊,所以我們需要以 HStack 包裹著 VStack。為了達成這個目的,使用 CMD+Click 點擊程式碼中的 VStack,並選擇 Embed in HStack 選項,如下圖所示:

swiftUI-preview-3

你的程式碼看起來應該會像這樣:

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                Text("Simon Ng")
                Text("Founder of AppCoda")
            }
        }
    }
}

自動預覽視窗看起來似乎沒有特別的變化,現在讓我們來加入一張圖片。將程式碼改為下面這樣:

struct ContentView: View {
    var body: some View {
        HStack {
            Image(systemName: "photo")
            VStack {
                Text("Simon Ng")
                Text("Founder of AppCoda")
            }
        }
    }
}

從 iOS 13 開始,Apple 介紹了一個名為 SFSymbols 的新功能。SF Symbols 這功能由 Apple 所設計,當中集合了 1500 多個可以在 App 之中使用的符號。由於它們能夠與 San Francisco 系統字體完美整合,無論是任何粗細及大小的文字,符號都可以確保它們垂直對齊。因為我們目前沒有導師的照片,所以在此先使用佔位符圖片來代替。

swiftUI-preview-4

現在,讓我們專注於一些次要的設計議題。因為我們想要效仿 UITableRow 的外觀及感覺,讓我們將文字對齊到左邊 (也就是 leading)。在 VStack 上使用 CMD+Click 並點擊 Inspect,選擇靠左對齊的圖示:

aline

你將會看到程式碼變成下面這樣。而且,你也會看到即時預覽反映出新的改變。

VStack(alignment: .leading) {
    ...
}

由於第二個文字視圖是標題,現在讓我們更改字體來反映這一點。如同之前一樣,在即時預覽中的“Founder of AppCoda”文字視圖中,使用 CMD + Click 並選擇 Inspect。 將字體更改為“Subheadline”,並觀察即時預覽和程式碼的變動。

font-change

讓我們把顏色也改成“Gray”吧,你的程式碼看起來應該像這樣:

struct ContentView: View {
    var body: some View {
        HStack {
            Image(systemName: "photo")
            VStack(alignment: .leading) {
                Text("Simon Ng")
                Text("Founder of AppCoda")
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}

目前我們已經完成了樣本行的設計,現在神奇的部份來了:要創建一個列表原來這麼簡單!在 HStack 上使用 CMD+Click 並點擊 Embed in List。就是這樣了!看看程式碼如何自動改變,以及預覽視窗將會反映出五個新的行,每個行都顯示了 Simon Ng 為團隊成員。

swiftUI-preview-5

另外,請注意程式碼中 List 是如何被創建的。移除 HStack 並使用重複的 List 來取代它,就已經能夠創建表格視圖。現在來思考一下,你不需要使用 UITableViewDataSourceUITableViewDelegate、Auto Layout 及黑暗模式的實現等,節省了多少的時間及程式碼,這展現了 SwiftUI 的強大!當然,我們離完成還有一段距離。讓我們來加入一些真實數據到新的列表之中吧!

將數據連結到列表中

我們所需要的資料,是教學團隊成員列表、他們的簡介、及一個包含成員照片的資料夾。你可以在這裡下載所需的檔案,當中有 Tutor.swiftTutor.xcassets 兩個檔案。

下載完成後,我們要將 Swift 檔及 asset 資料夾匯入到 Xcode 專案中。要達到這一點,只需要直接將它們拖曳到專案導覽器之中即可。

Tutor.swift 裡面,我們宣告一個名為 Tutorstruct,並使它遵循 Identifiable 協定,待會你就會知道這個步驟有多重要。我們也定義它的變數為 idnameheadlinebio、和 imageName。最後,我們包含一些將會在 App 內用到的測試數據,在 Tutor.xcassets 之中,我們有所有成員的圖片。

回到 ContentView.swift 之中,並如此修改程式碼:

struct ContentView: View {
    //1
    var tutors: [Tutor] = []

    var body: some View {
        List(0..<5) { item in
            Image(systemName: "photo")
            VStack(alignment: .leading) {
                Text("Simon Ng")
                Text("Founder of AppCoda")
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        //2
        ContentView(tutors: testData)
    }
}
#endif

我們在這裡做的事情相當簡單:

  1. 我們定義了新的變數 tutors,它是用來存放 Tutor 結構的空陣列。
  2. 由於我們為 ContentView 結構定義了一個新變數,所以需要改變 ContentView_Previews 來反映這個變動。我們將 tutors 參數設定為 testData

在即時預覽之中,你不會看到任何變化,因為我們還沒使用到測試資料。為了展示測試資料,如此更改程式碼:

struct ContentView: View {
    var tutors: [Tutor] = []

    var body: some View {
        List(tutors) { tutor in
            Image(tutor.imageName)
            VStack(alignment: .leading) {
                Text(tutor.name)
                Text(tutor.headline)
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}

在這裡,我們需要確保 ContentView 會使用 tutors 列表,來在螢幕展示數據。

登愣!看看即時預覽視窗的變化吧!

swiftUI-preview-6

目前圖片都是以正方形顯示,我們希望圖片以圓形顯示。讓我們先看看如何為圖片做圓角的效果吧!在右上角點選 + 按鈕,並點擊第二個頁籤,這應該會顯示出佈局修改器的清單,讓你能夠選擇並加入到視圖。

layout-editor

搜尋“Corner Radius”,將它拖曳到即時預覽視窗之中,並在圖片上放開。你應該會看到程式碼和即時預覽視窗產生這樣的變動:

rounded-corner

然而,圓角半徑 3 有點太小了,讓我們將它改為 40。這樣一來,你就能獲得一張完美的圓形照片了!

rounded-image

給自己一點掌聲吧,現在單元格和列表的部分都完成了!下一步我們需要做的,就是在使用者點擊單元格時呈現細節視圖。讓我們開始建構導航吧。

建構導航

一個導航視圖會將我們現有的視圖,包裹在導航列與導航控制器中。假設你之前使用過 Storyboard,你應該知道在導航界面中嵌入視圖非常容易,你只需點擊幾下即可完成所有操作。

在 SwiftUI,將 List 視圖包裹在 NavigationView 視圖中也是相當簡單,只要將程式碼改成這樣:

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            ...
        }
    }
}
...

你只需要將 List 的程式碼包裹在 NavigationView 之中。在預設的情況下,導航列並不會有標題。你的預覽應該會將列表往下移動,導致中間有一段很大間隔,這是因為我們還沒設定導航列的標題。為了修正這個問題,我們可以加上一行程式碼(也就是 .navigationBarTitle)來設定標題:

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            ...
        }
        .navigationBarTitle(Text("Tutors"))
    }
}
...

你的螢幕看起來應該像這樣:

swiftUI-preview-7

接著,讓我們來設定導航按鈕。一個 NavigationButton 代表了一個 destination,也就是一個呈現在導航堆疊上的視圖。就如同我們將 List 包裹到 NavigationView 一樣,我們需要透過 NavigationButton 來包裹 List 的內容,像是這樣:

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            NavigationButton(destination: Text(tutor.name)) {
                Image(tutor.imageName)
                VStack(alignment: .leading) {
                    Text(tutor.name)
                    Text(tutor.headline)
                        .font(.subheadline)
                        .color(.gray)
                }
            }
        }
        .navigationBarTitle(Text("Tutors"))
    }
}
...

我們簡單地在細節視圖中呈現了團隊成員的名字,現在該是時候來測試一下。

在現在的預覽模式當中,你無法與視圖互動。在正常情況下,當你點擊自動預覽時,它只是強調對應的程式碼部分。為了測試並與 UI 互動,你需要點擊右下角的播放按鈕。

img

這時候視圖將會變暗,你需要等待幾秒,當整個模擬器準備好之後,你就能夠與視圖互動。

swiftui-demo-app

當它完成載入,你應該能夠點擊單元格,然後它會導航到一個推疊中的新視圖,當中顯示了所選單元格的名字。

img

在開始實作細節視圖之前,讓我向你展示一個小技巧,讓你的程式碼更清晰。在NavigationButton 使用 CMD+Click 並選擇“Extract Subview”。

出現了!你可以看到 NavigationButton 中的所有程式碼都被創建為一個全新的 struct,這讓程式碼更加清晰。將 ExtractedView 重新命名為 TutorCell

現在,你可能會在 TutorCell 中收到錯誤訊息。這是因為在新的結構中,我們沒有 tutor 參數來傳遞。要修正這個錯誤非常簡單,只需要在 TutorCell 結構中添加一個新的常數:

struct TutorCell: View {
    let tutor: Tutor
    var body: some View {
        ...
    }
}

並且在 ContentView 中,改變這行來加入缺少的參數:

...
List(tutors) { tutor in 
    TutorCell(tutor: tutor)
}.navigationBarTitle(Text("Tutors"))
...

就是這樣!我們的列表和單元格都設計與佈局好了!接下來,我們將構建一個細節視圖,用來顯示導師的所有資訊。

img

建構細節視圖

讓我們到 File > New > File 創建一個新檔案。在 iOS 的頁籤中,選擇 SwiftUI View,並將檔案命名為 TutorDetail

img

在自動預覽中,你可以看到已經創建了的基礎視圖,讓我們來做些改動。首先,點選 + 按鈕,並拖曳出一個圖片,放到內建的 Text 視圖上方。將圖片命名為“Simon Ng”,Simon 的照片應該會出現。現在將程式碼修改成這樣:

struct TutorDetail: View {
    var body: some View {
        //1
        VStack {
            //2
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            //3
            Text("Simon Ng")
                .font(.title)
        }
    }
}

你應該能夠瞭解程式碼所做的事,讓我簡單解釋一下:

  1. 首先,我們將所有視圖包裹到垂直堆疊裡,這對我們待會做的佈局設計非常重要。
  2. 接著,我們對 Simon 的照片做了一些調整。我們先將圖片的剪輯設定成圓形,而不是設定它的 cornerRadius,這樣的做法會更有效率,因為它可以適應不同的圖片尺寸。然後,我們加上了一個帶有白色邊框的圓形遮罩,它提供了一個漂亮的橙色邊框。最後,我們添加了一個光影來呈現深度。
  3. 最後一行程式碼將導師名字的字型設定為 title
img

我們還需要加入兩個文字視圖:headlinebio。拖曳出兩個文字視圖到導師名字的文字視圖下方,然後編輯:

struct TutorDetail: View {
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            Text("Founder of AppCoda")
            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
        }
    }
}
img

好消息是我們已經呈現了文字視圖;但壞消息是它不太好看,而且也沒有顯示出兩個文字視圖之間的差異。加上,個人簡介的文字視圖也沒有呈現全部的文字。讓我們一步一步來修正這些問題。

如此更新程式碼:

struct TutorDetail: View {    
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            //1
            Text("Founder of AppCoda")
                .font(.subheadline)
            //2
            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
                .font(.headline)
                .multilineTextAlignment(.center)

        }
    }
}
  1. 首先我們將“Founder of AppCoda”字體粗細設定為 subheadline
  2. 同樣地,將個人簡介的字體粗細設定為 headline,並以 .multilineTextAlignment(.center) 將文字置中。
swiftui-demo

讓我們繼續修正下一個問題。我們需要展示個人介文字視圖中的所有文字,這非常簡單,加上一行程式碼即可:

...
Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
        .font(.headline)
        .multilineTextAlignment(.center)
        .lineLimit(50)
...
img

現在一切看起來都很好,讓我們來做最後一次設計上的改變。因為標題和個人簡介文字視圖之間過於接近,我想在這兩個 Text 視圖之間留一些空間。此外,我想為所有視圖添加一些填充,使它們不會太貼近裝置的邊緣。請如此更改程式碼:

struct TutorDetail: View {
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            Text("Founder of AppCoda")
                .font(.subheadline)
            //1
            Divider()

            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
                .font(.headline)
                .multilineTextAlignment(.center)
                .lineLimit(50)
        //2
        }.padding()
    }
}

我們在這裡做了兩個改變:

  1. 簡單地呼叫 Divider() 來加上分隔線。
  2. 為了在整個垂直堆疊加上填充,我們只需要在 VStack 宣告的最後呼叫 .padding() 即可。
img

就是這樣,我們終於完成細節視圖了!剩下的步驟,就是將我們的列表連接到細節視圖,這非常簡單。

傳遞數據

為了傳遞數據,我們需要在 TutorDetail 結構之中宣告一些參數,在宣告變數 body 之前,加入下列變數:

var name: String
var headline: String
var bio: String
var body: some View {
    ...
}

這些是我們將會從 ContentView 傳遞出去的參數,用這些參數來取代文字:

...
var body: some View {
    VStack {
        // 1
        Image(name)
            .clipShape(Circle())
               .overlay(
                   Circle().stroke(Color.orange, lineWidth: 4)
            )
               .shadow(radius: 10)
        //2
        Text(name)
            .font(.title)
        //3
        Text(headline)
            .font(.subheadline)
        Divider()
        //4
        Text(bio)
            .font(.headline)
            .multilineTextAlignment(.center)
            .lineLimit(50)
        //5
    }.padding().navigationBarTitle(Text(name), displayMode: .inline)
}
...
  1. 我們用變數 name 來取代圖片的名字。
  2. 我們也用變數 name 來取代導師的名字。
  3. 我們使用變數 headline 來取代標題文字。
  4. 最後,我們使用變數 bio 來取代長篇的段落文字。
  5. 我們也加了一行新的程式碼,來把導航列的標題設定為導師的名字。

最後,我們需要加入缺少的參數到 TutorDetail_Previews 結構之中。

#if DEBUG
struct TutorDetail_Previews : PreviewProvider {
    static var previews: some View {
        TutorDetail(name: "Simon Ng", headline: "Founder of AppCoda", bio: "Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
    }
}    
#endif

在上面的程式碼之中,我們加入了缺少的參數,並使用之前有的內容填入訊息。

你可能會問: #if DEBUG/#endif 敘述是甚麼意思呢?這些程式碼代表著無論哪些程式碼包含在這些命令之中,它們都只會在預覽中顯示,以便進行除錯;而不會在最終 App 之中執行。

接著繼續執行自動預覽,看起來沒有什麼改變,因為我們依然保持同樣的資訊。但是在繼續下一步之前,請確保你能夠預覽 TutorDetail

img

最後一步,我們要將這個視圖連結到列表中。切換到 ContentView.swift 檔案,你唯一需要做的,就是改變 TutorCell 結構之中的一行程式碼,依照下列指示改變 NavigationButton 程式碼:

...
var body: some View {
    return NavigationButton(destination: TutorDetail(name: tutor.name, headline: tutor.headline, bio: tutor.bio)) {
        ...
    }
}
...

我們不使用教師姓名呈現 Text 視圖,而是將目的地更改為 TutorDetail,同時填寫適當的詳細資訊,你的程式碼現在應該像這樣:

img

點擊即時預覽的播放按鈕,並與視圖互動,如果沒有問題,App 應該可以順利運作。

簡單地選擇其中一個成員的紀錄:

img

你應該會在細節視圖中,看到關於成員的更多資訊。

img

總結

這是一篇寫給初次接觸 SwiftUI 的教學文章,提供了 SwiftUI 的基礎知識。你現在可以輕鬆地構建簡單的 App,像是待辦事項列表等。我建議你看一下下面的資源,比如關於這個框架的 Apple 說明文件和 WWDC 2019 的議程。

這個框架將會是 Apple 開發的未來,所以越早熟悉它就能領先別人一步。請記得,假如你對程式碼有所疑問,可以試著與自動預覽視窗互動,並直接變更 UI,來觀察所對應的程式碼是如何建構的。 如果你有任何問題,歡迎在下方留言。

如果想要參考這次教學文章的程式碼,你可以在這裡下載完整的專案。

譯者簡介:HengJay,iOS 初學者,閒暇之餘習慣透過線上 MOOC 資源學習新的技術,喜歡 Swift 平易近人的語法也喜歡狗狗,目前參與生醫領域相關應用的 App 開發,希望分享文章的同時也能持續精進自己的基礎。

LinkedIn: https://www.linkedin.com/in/hengjiewang/
Facebook: https://www.facebook.com/hengjie.wang

原文SwiftUI First Look: Building a Simple Table View App
作者
Sai Kambampati
Sai Kambampati 是程式開發員,生活於美國加州薩克拉門托,於2017獲得Apple's WWDC獎學金。精於 Swift及Python語言,渴望自家開發人工智能產品。閒時喜歡觀看Netflix、做健身或是遛漣圖書館中。請到推特追蹤 @Sai_K1065 。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。