iOS App 程式開發

利用 SwiftUI 建立表單 UI 體驗更互動的開發過程!

對一些開發者來說,SwiftUI 的宣告式語法可能還有點陌生;但一旦掌握好這種語法,你就會喜歡上用它來撰寫 UI 程式碼了。結合新的預覽功能,你可以撰寫程式碼,並即時預覽視覺上的變化,讓整體開發體驗變得更快、更有互動性。
利用 SwiftUI 建立表單 UI 體驗更互動的開發過程!
利用 SwiftUI 建立表單 UI 體驗更互動的開發過程!
In: iOS App 程式開發, Swift 程式語言, SwiftUI 框架, UI, Xcode

上一篇教學中,我們簡單介紹了 SwiftUI 的基礎知識,並帶你建構了一個簡單的使用者介面。經濟一星期的探索,即使這個框架現階段還是有些 Bug,但我真的很喜歡利用這個框架來建構使用者介面!這個框架為開發者提供了一個新的方式來開發 UI,讓你以更少的程式碼建構出相同的 UI。

對一些開發者來說,宣告式語法 (Declarative Syntax) 可能還有點陌生,需要花點時間來習慣它。但這就像當初我們從 Objective-C 轉換到 Swift 一樣,一旦掌握到它,你就會喜歡上用宣告式語法來撰寫 UI 程式碼了,因為你可以更自然地描述所需的 App 佈局。結合新的預覽功能,你可以撰寫程式碼,並即時預覽視覺上的變化,讓整體開發體驗變得更快、更有互動性。

在先前的教學中,你學會了如何建立一個表格視圖的 App,並且處理導覽功能。今天,讓我來教大家使用 SwiftUI,來建立一個簡單的表單,就像下圖這樣:

img
編註:這個示範畫面是我們 FoodPin App 的其中一個畫面,你可以在我們的新手教學裡學習如何建構這個 App。

使用 SwiftUI 和設計畫布的先決條件

為了接下來的教學,你的 Mac 需要先安裝 Xcode 11。而且因為設計畫布 (Design Canvas),也就是即時預覽功能,只能在 macOS Catalina 上運作,所以你必須升級 macOS 到 macOS Catalina。在撰寫本篇教學時,Xcode 11 及 macOS Catalina (aka macOS 10.15) 都只有 beta 版本。

你也需要有 Swift 的基本知識。如果沒有的話,可以參考本篇免費入門指南

建立新專案

我們將從零開始建立上面那個畫面。首先,在 Xcode 11 裡選擇 Single View Application 模版來建立一個新專案,並且命名為 FormDemo(你也可以改別的名字)。請記得勾選 Use SwiftUI

new-project

設計文字欄位 (Text field)

我們將從的實作文字欄位、及放置在文字欄位正上方標籤開始。為了建立一個文字標籤,使用 Text 元件,並這樣撰寫程式碼:

Text("NAME").font(.headline)

我們把文字標籤的數值設為 NAME,並將它的字型設為 headline。就像 UIKit 一樣,SwiftUI 框架也有內建的文字欄位元件。為了建立一個有佔位符 (Placeholder) 的文字欄位,你這樣撰寫程式碼:

TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))

為了放置文字標籤到文字欄位置上,你可以使用 VStack 來排列兩個元件。你最後的程式碼應該會像這樣:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
        }
    }
}

在這個設計畫布中,它應該會依據你在 Xcode 所選擇的機型,顯示模擬器中的變化。

design-canvas-preview

比較一下現時文字欄位設計、和前文的最終畫面,你就會知道我們需要做些變更:

  • 更改背景顏色為 light gray
  • 為文字欄位製作圓角
  • 加入一些內邊距 (Padding)

所以,我們如此更新程式碼:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
        }
    }
}

在程式碼中,我們為 TextField 四邊加入內邊距 (.padding(.all)),並更換了它的背景顏色。我們也設定圓角半徑為 5.0 來產生圓角效果。現在你的文字欄位看起來應該像這樣:

text-field-1

文字欄位與文字標籤都過於靠近左右邊界。為了修正這一點,我們可以在垂直堆疊 (VStack) 中,使用 padding 屬性來加入一些水平空間。所以,更新後的程式碼會像這樣:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            .
            .
            .
        }
        .padding(.horizontal, 15)
    }
}

變更完成後,文字欄位看起來應該會更好:

text-field-2

抽取文字欄位來複用

就如你在最後的設計中所見,所有文字欄位都共享相同的佈局與設計。與其複製我們剛寫好的程式碼到其他文字欄位,不如抽取程式碼來建立單獨的視圖以供複用更好。

我們來把結構命名為 LabelTextField,並且搬移 VStack 程式碼區塊到這個結構,就像這樣:

struct LabelTextField : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
        }
        .padding(.horizontal, 15)
    }
}

為了複用,我們將進一步調整結構來接收 labelplaceholder 兩個變數。LabelTextField 現在看起來應該像這樣:

struct LabelTextField : View {
    var label: String
    var placeHolder: String

    var body: some View {

        VStack(alignment: .leading) {
            Text(label)
                .font(.headline)
            TextField(.constant(""), placeholder: Text(placeHolder))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
            }
            .padding(.horizontal, 15)
    }
}

現在回到 ContentView。你可以像下面這樣創建一個有特定標籤名稱與佔位符的 LabelTextField,來建立一個文字欄位:

struct ContentView : View {
    var body: some View {
        LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
    }
}

預覽畫面應該仍顯示相同的標籤設計。但在內部,我們現在可以輕鬆地建立具有不同文字標籤與佔位符數值的文字欄位。

使用 List 來建立多個文字欄位

最終的表單會有多個文字欄位讓使用者填入。為了在垂直排列中呈現多個文字欄位,你可以使用 VStack 來佈局文字欄位。然而,因為我們無法在一個畫面中顯示所有資訊,所以我們將堆疊嵌入 List,讓表單可以捲動。SwiftUI 提供了一個叫做 List 的容器,讓開發者可以快速建立一個表格,或是在單行中呈現多列資料。

現在,如此更新 ContentView

struct ContentView : View {
    var body: some View {
        List {
            VStack(alignment: .leading) {
                LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
                LabelTextField(label: "TYPE", placeHolder: "Fill in the restaurant type")
                LabelTextField(label: "ADDRESS", placeHolder: "Fill in the restaurant address")
                LabelTextField(label: "PHONE", placeHolder: "Fill in the restaurant phone")
                LabelTextField(label: "DESCRIPTION", placeHolder: "Fill in the restaurant description")
                }

        }

    }
}

變更後,你就會看到預覽畫面更新成這樣:

multiple-text-fields

如果你想要調整文字欄位左右邊界與螢幕外框的間距,你可以在 VStack 後插入一行程式碼:

List {
   VStack(alignment: .leading) {
                ...
   }
   .listRowInsets(EdgeInsets())
}

藉由使用 listRowInsets,你可以延伸文字欄位讓其更靠近螢幕外框。

multiple-text-fields-2

加入特色照片

你可以先在這裡下載範例圖片,或使用任何你喜歡的照片。在使用照片之前,先將照片匯入 Asset 目錄裡。

asset-catalog

SwiftUI 提供一個名為 Image 的元件,讓你這樣呈現圖像:

Image("chicken")

如果你在垂直堆疊 (VStack) 前面放入這行程式碼,那麼照片將會佈滿整個螢幕。為了縮小它,你可以撰寫以下程式碼:

Image("chicken")
    .resizable()
    .scaledToFill()
    .frame(height: 300)
    .clipped()

這段程式碼宣告圖像是個可伸縮的視圖,並把它的 Content Mode 設定為 Scale to fill。同時,我們把框架高度設為一個固定值。最後,我們將視圖裁切至框架邊界。你會看到這樣的預覽:

featured-photo

為了延伸照片到螢幕的邊界,你可以呼叫 listRowInsets,並將數值設為 EdgeInsets()。讓我們這樣更新 Image

Image("chicken")
    .resizable()
    .scaledToFill()
    .frame(height: 300)
    .clipped()
    .listRowInsets(EdgeInsets())

現在,特色照片應該是完美地呈現了。最後,你可能注意到 Name 欄位非常靠近照片的底邊。我們可以在宣告 VStack 的後面插入下列程式碼,讓它有多一些空間:

.padding(.top, 20)

如果你正確地照著教學走,你的畫面看起來應該像這樣:

swiftui-form-featured-photo-2

建立圓角按鈕

現在,你已經建立一個簡單的表單了,下一步讓我們來實作 Save 按鈕吧!SwiftUI 框架提供一個名為 Button 的元件來建立按鈕。要建構一個按鈕,最簡單的方法是撰寫下列程式碼:

Button(action: {}) {
  Text("Save")
}

在這裡,我們定義了一個叫做 Save 的標準按鈕。Action 參數接受一個閉包,而閉包將會在按鈕被點擊時觸發。在這個範例中,它不會執行任何動作。

為了好好組織程式碼,我們將建立另一個名為 RoundedButton 的結構:

struct RoundedButton : View {
    var body: some View {
        Button(action: {}) {
            Text("Save")
        }
    }
}

但你如何預覽這個圓角按鈕呢?你可以在 ContentView 中建立 RoundedButton,來在現在的模擬器中預覽它的設計。Xcode 11 的預覽功能讓開發者撰寫程式碼,來在設計畫布中加入多個預覽畫面。調整 ContentView_Previews 結構來產生圓角按鈕的預覽:

struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
            RoundedButton().previewLayout(.sizeThatFits)
        }
    }
}

Group 允許你將多個預覽畫面組成一個群組。在上面的程式碼,我們仍然在你所選的模擬器上產生 ContentView 的預覽畫面。至於圓角按鈕,我們呼叫 previewLayout 方法來改變預覽方式。我們不再在另一個模擬器上渲染圓角按鈕,而是將數值設為 .sizeThatFits,這會指示 Xcode 在容器視圖中產生預覽畫面,就像這樣:

swiftui-form-preview-button

現在更新 RoundedButton 的程式碼為:

struct RoundedButton : View {
    var body: some View {
        Button(action: {}) {
            HStack {
                Spacer()
                Text("Save")
                    .font(.headline)
                    .color(Color.white)
                Spacer()
            }
        }
        .padding(.vertical, 10.0)
        .background(Color.red, cornerRadius: 4.0)
        .padding(.horizontal, 50)
    }
}

我們就這樣更改了按鈕的字型與字體顏色。就如你在最後成果中所見,我想讓按鈕看起來更像一個按鈕。所以,我們變更它的背景顏色,並加上一些內邊距,讓按鈕有個好看的背景。

swiftui-form-color-button

為了使用這個按鈕,並將它加入到表單中,你可以在 ContentView 中插入下列程式碼,並將其放在最後一個文字欄位之後:

RoundedButton().padding(.top, 20)

我們呼叫 padding 函式,好在文字欄位與 Save 按鈕之間加入一些額外的空間。現在你的設計畫布將會顯示為這樣:

swiftui-form-form-button

將視圖嵌入至一個導覽視圖中

我們快完成所有的實作了!最後一個步驟,就是要將整個表單嵌入到一個導覽視圖裡。前面的教學提過,SwiftUI 提供一個名為 NavigationView 的容器視圖來建立導覽介面。你只需要將 ContentView 裡的所有列表嵌進 NavigationView,就像這樣:

struct ContentView : View {
    var body: some View {

        NavigationView {
            List {

                Image("chicken")
                    .resizable()
                    .scaledToFill()
                    .frame(height: 300)
                    .clipped()
                    .listRowInsets(EdgeInsets())

                VStack(alignment: .leading) {
                    LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
                    LabelTextField(label: "TYPE", placeHolder: "Fill in the restaurant type")
                    LabelTextField(label: "ADDRESS", placeHolder: "Fill in the restaurant address")
                    LabelTextField(label: "PHONE", placeHolder: "Fill in the restaurant phone")
                    LabelTextField(label: "DESCRIPTION", placeHolder: "Fill in the restaurant description")

                    RoundedButton().padding(.top, 20)
                }
                .padding(.top, 20)
                .listRowInsets(EdgeInsets())
            }

            .navigationBarTitle(Text("New Restaurant"))
            .navigationBarItems(trailing:
                    Button(action: {

                    }, label: {
                        Text("Cancel")
                    })
            )
        }

    }
}

如果要設置導覽列的標題,我們可以呼叫 navigationTitle 函式,並指定文字元件;如果需要設定 bar items,我們可以呼叫 navigationBarItems 函式,來設置導覽列的前/後緣邊界顯示的視圖。在上面的程式碼中,我們在後緣邊界設置了Cancel 按鈕。

swiftui-form-navigation-bar

利用固定佈局預覽 UI

依照以上的步驟,你就可以利用 SwiftUI 來佈局出一個表單介面了。最後,讓我們看看 Xcode 預覽畫面有多強大吧!如你所見,表單的長度超過手機視窗。為了檢視整個表單,你可以點擊 Play 按鈕來執行 App,然後你就可以捲動表單。Xcode 11 也提供了另外一個方法來預覽整個表單。讓我們編輯 ContentView_Previews 結構,並且初始化 ContentView,就像這樣:

ContentView().previewLayout(.fixed(width: 375, height: 1000))

我們告訴 Xcode 在固定的矩型視窗中渲染預覽畫面,而不是在模擬器上預覽佈局。這樣一來,不用執行 App 你也能預覽整個表單畫面了。

swiftui-form-preview-layout

下一步呢?

花了一週時間探索 SwiftUI,我真的很享受使用這個全新的框架來建置 UI。SwiftUI 讓 UI 開發變得輕而易舉,並且讓你不必撰寫那麼多的程式碼。我仍在探索這個框架,所以如果你在這篇教學中找到任何錯誤,歡迎留言讓我知道。

在未來的教學中,我們會含括其他 SwiftUI 的功能,像是動畫、以及更複雜的佈局。請持續關注我們的教學,亦歡迎你留言表達意見。

編註:我們現正為我們的 Swift 書籍更新 SwiftUI 的部分。如果你現在購買書籍,等書籍正式更新時,你將會免費收到更新。
譯者簡介:楊敦凱-目前於科技公司擔任 iOS Developer,工作之餘開發自有 iOS App 同時關注網路上有趣的新玩意、話題及科技資訊。平時的興趣則是與自身專業無關的歷史、地理、棒球。來信請寄到:[email protected]

原文How to Build a Form UI with SwiftUI
作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 17 App 程式設計實戰心法》、《iOS 17 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。