iOS App 程式開發

利用 SwiftUI 控件 讓你更彈性地設計專屬你的按鈕!

按鈕是一個非常基本的 UI 控制元件,你在所有 App 中都可以看到它的蹤影。按鈕可以處理使用者的觸控動作,以觸發一些動作。此文詳細介紹 SwiftUI 控件,彈性又輕鬆地設計專屬你的按鈕,讓你感愛 SwiftUI 的威力之處。
利用 SwiftUI 控件 讓你更彈性地設計專屬你的按鈕!
利用 SwiftUI 控件 讓你更彈性地設計專屬你的按鈕!
In: iOS App 程式開發, Swift 程式語言, SwiftUI 框架, UI

按鈕可以啟動 App 的特定動作,可以客製化背景,也可以加入標題或圖示。這個系統針對大部分使用狀況,提供了一些預先設計好的按鈕樣式。你也可以完全客製自己的按鈕。

Apple 官方文件

我相信我不需要再解釋按鈕的用途,這是一個非常基本的 UI 控制元件,你在所有的 App 中都會看到按鈕的蹤影。按鈕可以處理使用者的觸控動作,以觸發一些動作。倘若你之前有學過 iOS 程式設計的話,SwiftUI 的 Button 與 UIKit 的 UIButton非常相似,不過前者更加彈性及可客製化,讀下去你就會了解我的意思了。我將在這篇文章中詳細介紹 SwiftUI 控件,而你會學到以下的技巧:

  1. 如何建立一個簡單的按鈕,並處理使用者的選擇
  2. 如何客製按鈕的背景、間距 (padding)、和字型
  3. 如何為按鈕添加邊框 (border)
  4. 如何建立一個有圖片和文字的按鈕
  5. 如何建立一個有漸層背景 (gradient background) 與陰影的按鈕
  6. 如何建立一個全寬度 (full-width) 的按鈕
  7. 如何建立一個可重複使用的按鈕樣式
  8. 如何加入一個好看的點擊動畫 (tap animation)

建立一個啟用 SwiftUI 的新專案

好的,讓我們從基礎開始,使用 SwiftUI 來建立一個簡單的按鈕,首先,開啟 Xcode,並使用 Single View Application 模板來建立一個新專案。輸入專案名稱,我將它設定為 SwiftUIButton ,不過你也可以改用其他名稱。最重要的是,請確認你有選取 Use SwiftUI 選項。

swiftui-new-project

儲存專案後,Xcode 即會載入 ContentView.swift 檔,並於設計畫布中顯示預覽畫面。如果預覽畫面無法顯示,你可以點擊畫布中的 Resume 按鈕。

swiftui-preview

有了 SwiftUI,要建立按鈕非常簡單。基本上,你可以使用以下這段程式碼來建立按鈕:

Button(action: {
    // 所需執行的內容
}) {
    // 按鈕介面外觀設置
}

建立一個按鈕時,你需要提供這兩段程式碼:

  1. 所需執行的內容:使用者按下或選取按鈕後所執行的程式碼。
  2. 按鈕外觀設置:呈現按鈕介面外觀所需的程式碼。

舉例來說,如果你只想要將 Hello World 標籤 (label) 轉成一個按鈕,你可以將程式碼更新如下:

struct ContentView: View {
    var body: some View {
        Button(action: {
            print("Hello World tapped!")
        }) {
            Text("Hello World")
        }
    }
}

現在如畫布所示,Hello World 文字就變成了可按的按鈕。

swiftui-button

在設計畫布上的按鈕現在還無法按,如果要測試的話,你可以點選 Play 按鈕來執行這個 App。不過為了檢視按下 Hello World 後的訊息,你必須在 Play 按鈕上按右鍵,然後點選 Debug Preview。按下按鈕後,你就會在主控台 (console) 中看到訊息。

客製按鈕的字型與背景

你已經學會了如何建立一個簡單的按鈕,現在讓我們來看看,如何以內建修飾器 (modifier) 來客製按鈕的介面外觀。舉例來說,如果想改變背景與文字顏色,你可以像這樣使用 background 與 foregroundColor 修飾器:

Text("Hello World")
    .background(Color.purple)
    .foregroundColor(.white)

如果你要變更字型,你可以如此進一步使用 font 修飾器,並指定字型型式 (例如 .title):

Text("Hello World")
    .background(Color.purple)
    .foregroundColor(.white)
    .font(.title)

變更完成之後,你的按鈕應該看起來像這樣:

customize-color

如你所見,這個按鈕不怎麼好看,如果能夠在文字周圍加上些間距是不是更好呢?你可以這樣使用 padding 修飾器達到目的:

Text("Hello World")
    .padding()
    .background(Color.purple)
    .foregroundColor(.white)
    .font(.title)

變更完成後,畫布也會跟著更新按鈕。現在按鈕就好看多了。

add-padding

修飾器順序的重要性

這邊我想要強調一件事,就是 padding 修飾器要置於 background 修飾器之前。如果你將程式碼改寫如下,結果將會完全不同。

swiftui-setting

如果你將 padding 修飾器置於 background 修飾器之後,你依然可以為按鈕加入間距,但是間距卻沒有背景顏色。如果你想知道原因,你可以像這樣來理解這些修飾器。

Text("Hello World")
    .background(Color.purple) // 1. 背景顏色變為紫色
    .foregroundColor(.white)  // 2. 設定前景/字型顏色為白色
    .font(.title)             // 3. 變更字型樣式
    .padding()                // 4. 以主顏色來加入間距(也就是白色)

反之,倘若 padding 修飾器置於 background 修飾器之前,按鈕就會這樣運作:

Text("Hello World")
    .padding()                // 1. 加入間距
    .background(Color.purple) // 2. 變更背景顏色為紫色,包括間距
    .foregroundColor(.white)  // 3. 設定前景/字型顏色為白色
    .font(.title)             // 4. 變更字型樣式

為按鈕添加外框

padding 修飾器並非一定要放置在最前面,排序都只是依照你的按鈕設計而定。譬如說,你想建立一個有邊框的按鈕:

add-border

你可以如此變更 Text 控件的程式碼:

Text("Hello World")
    .foregroundColor(.purple)
    .font(.title)
    .padding()
    .border(Color.purple, width: 5)

這裡我們設定前景顏色為紫色,並在文字周圍加入一些空的間距。border 修飾器是用來定義外框寬度與顏色的,你可以自行改變 width 參數,來看看效果為何。

再舉一個例子,譬如說,設計師提供了以下的按鈕設計,以目前所學的內容,你該如何實作它呢?在閱讀下一個段落之前,請自行花個幾分鐘來思考看看。

purple-background-and-border

那麼,你所需要的程式碼就是這樣:

Text("Hello World")
    .fontWeight(.bold)
    .font(.title)
    .padding()
    .background(Color.purple)
    .foregroundColor(.white)
    .padding(10)
    .border(Color.purple, width: 5)

我們使用兩個 padding 修飾器來設計按鈕。第一個 padding 與background 修飾器搭配在一起,是用來建立以紫色作為背景搭配間距的按鈕。padding(10) 修飾器則為按鈕另外在周圍加上間距,而這個 border 修飾器則指定為將外框塗成紫色。

讓我們來看一些更複雜的範例。如果你想要設計如下具有圓角外框的按鈕,該如何做呢?

rounded-corner

SwiftUI 內建名為 cornerRadius 修飾器,可以讓你很輕易地建立圓角。要以圓角來渲染 (render) 按鈕的背景,你只要使用這個修飾器,並如下指定邊角的直徑:

.cornerRadius(40)

要加上外框的圓角,需要多花一點功夫,因為這個 border 修飾器,無法讓你建立圓角。所以,我們需要畫出這個外框,並將其疊到按鈕上才能辦到。以下為最終的程式碼:

Text("Hello World")
    .fontWeight(.bold)
    .font(.title)
    .padding()
    .background(Color.purple)
    .cornerRadius(40)
    .foregroundColor(.white)
    .padding(10)
    .overlay(
        RoundedRectangle(cornerRadius: 40)
            .stroke(Color.purple, lineWidth: 5)
    )

這個 overlay 修飾器可以讓你疊加另外一個視圖在目前視圖的上方。在程式碼中,我們使用 RoundedRectangle物件的 stroke 修飾器,來畫出一個圓角外框。這個 stroke 修飾器可以讓你設定框線的顏色與線寬。

建立圖片與文字型按鈕

到目前為止,我們只有設計文字按鈕。在真實的專案中,你或你的設計師可能想用圖片來設計按鈕。要建立圖片按鈕,除了使用 Image 控件來代替 Text 控件外,其程式碼語法是一樣的,如下所示:

Button(action: {
    print("Delete button tapped!")
}) {
    Image(systemName: "trash")
        .font(.largeTitle)
        .foregroundColor(.red)
}

為了方便起見,我們使用內建的圖示庫,也就是 SF Symbols(這裡我們使用垃圾桶圖示)來建立圖片按鈕。我們在 font 修飾器中指定使用 .largeTitle,來讓圖片大一點。你的按鈕看起來應該像這樣:

image-icon

同樣地,如果你想要建立一個圓形圖片按鈕,搭配純色的背景,你可以應用我們之前討論過的修飾器,如下圖所示。

swiftui-circular-image-button

你可以同時使用文字與圖片來建立按鈕,例如你想要將 “Delete” 放在圖示旁邊,可以將程式碼替代如下:

Button(action: {
    print("Delete tapped!")
}) {
    HStack {
        Image(systemName: "trash")
            .font(.title)
        Text("Delete")
            .fontWeight(.semibold)
            .font(.title)
    }
    .padding()
    .foregroundColor(.white)
    .background(Color.red)
    .cornerRadius(40)
}

這裏我們將圖片與文字控件以水平堆疊方式嵌入,這會將垃圾桶圖示與 Delete 文字並排。修飾器在 HStack 中設定了背景顏色、間距、與按鈕圓角,下圖就是呈現的結果。

button-with-image-and-text

按鈕加上漸層背景與陰影

有了 SwiftUI,要幫按鈕加上漸層背景非常容易。你不但可以在 background 修飾器指定顏色,更可以輕易地將漸層效果加到任何按鈕。你只要將這行程式碼:

.background(Color.red)

替代為:

.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))

SwiftUI 框架有幾個內建的漸層特效,上面的程式碼就應用了從左 (.leading) 至右 (.trailing) 的線性漸層。所以漸層的左側部分是紅色,慢慢地以藍色做結尾。

button-with-gradient-background

如果你想要讓漸層由上而下,你可以將程式碼更改如下:

.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .top, endPoint: .bottom))

你可以採用自己喜好的顏色來渲染漸層效果。譬如說,你的設計師告訴你要採用這樣的漸層:

sample-gradient

要將顏色從 hex (十六進位色碼) 轉換為 Swift 可以相容的格式,有好幾個方式,這裏我準備示範其中一種方式。在專案導覽器中,選取 asset 目錄 (也就是 Assets.xcassets)。在空白區域 (AppIcon 下方) 按下右鍵, 並選取 New Color Set

swiftui-assets

接下來,選取好顏色,並點擊 Show inspector 按鈕。然後點選屬性檢閱器 ( Attributes inspector) 圖示來打開顏色組的屬性。在名稱欄位,設定名稱為 DarkGreen。在 Color 區塊,變更輸入方法為 8-bit Hexadecimal

change-input-method

現在你可以在 Hex 欄位設定顏色。以這個範例來說,輸入 #11998e 來定義顏色。重複這個步驟來定義另一個 #38ef7d 顏色組。你可以將這組顏色名稱命名為 LightGreen 。

define-colors

現在你已經定義了兩個顏色組,讓我們回到 ContentView.swift 並更新程式碼。要使用這個顏色組,你只要將顏色名稱設定如下:

Color("DarkGreen")
Color("LightGreen")

因此,要以 DarkGreen 與 LightGreen 顏色來渲染漸層,你只需要更新 background 修飾器如下:

.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))

如果你有修改正確的話,你的按鈕應該會有下圖的漸層背景:

create-gradient-background

本節還有一個要介紹的是修飾器:shadow 修飾器,它可以讓你在按鈕 (或任何視圖) 周圍畫出陰影。只要在 cornerRadius 修飾器之後加上這行程式碼,即可觀看結果:

.shadow(radius: 5.0)

此外,你可以控制陰影的顏色,半徑,與位置。以下為範例程式碼:

.shadow(color: .gray, radius: 20.0, x: 20, y: 10)

建立一個全寬度按鈕

大一點的按鈕通常可以吸引使用者的注意。有時候,你需要建立一個全寬度的按鈕,可以佔滿整個畫面的寬度。這個 frame 修飾器是設計來讓你控制視圖的尺寸。

不論你是想要建立一個固定大小的按鈕,或者是可變化寬度的按鈕,你都可以使用這個修飾器。要建立全寬度的按鈕,你可以變更 Button 程式碼如下:

Button(action: {
    print("Delete tapped!")
}) {
    HStack {
        Image(systemName: "trash")
            .font(.title)
        Text("Delete")
            .fontWeight(.semibold)
            .font(.title)
    }
    .frame(minWidth: 0, maxWidth: .infinity)
    .padding()
    .foregroundColor(.white)
    .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
    .cornerRadius(40)
}

這裏的程式碼除了在 padding 之前加入了 frame 修飾器之外,跟我們剛所寫的非常相似,這裏我們定義一個按鈕的彈性寬度,我們設定 maxWidth 參數為 .infinity

這表示按鈕會填滿容器視圖 (container view) 的寬度。在畫布中,它現在應該可以顯示一個全寬度的按鈕了。

full-width-button-1

透過將 maxWidth 定義為 .infinity ,按鈕的寬度就會依照裝置的螢幕寬度來自動調整。如果你想要為這個按鈕加上一些水平方向的空間的話,可以在 .cornerRadius(40) 後插入一個 padding 修飾器:

.padding(.horizontal, 20)

以 ButtonStyle 來修飾按鈕

在真實的 App 中,同樣設計的按鈕會有好幾個功能。譬如說,你建立了三個按鈕 DeleteEdit 與 Share,而它們都有同樣的按鈕樣式如下:

full-width-button-2

你大致需要將程式碼撰寫如下 :

struct ContentView: View {
    
    var body: some View {

        VStack {
            
            Button(action: {
                print("Share tapped!")
            }) {
                HStack {
                    Image(systemName: "square.and.arrow.up")
                        .font(.title)
                    Text("Share")
                        .fontWeight(.semibold)
                        .font(.title)
                }
                .frame(minWidth: 0, maxWidth: .infinity)
                .padding()
                .foregroundColor(.white)
                .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
                .cornerRadius(40)
                .padding(.horizontal, 20)
            }
            
            Button(action: {
                print("Edit tapped!")
            }) {
                HStack {
                    Image(systemName: "square.and.pencil")
                        .font(.title)
                    Text("Edit")
                        .fontWeight(.semibold)
                        .font(.title)
                }
                .frame(minWidth: 0, maxWidth: .infinity)
                .padding()
                .foregroundColor(.white)
                .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
                .cornerRadius(40)
                .padding(.horizontal, 20)
            }
            
            Button(action: {
                print("Delete tapped!")
            }) {
                HStack {
                    Image(systemName: "trash")
                        .font(.title)
                    Text("Delete")
                        .fontWeight(.semibold)
                        .font(.title)
                }
                .frame(minWidth: 0, maxWidth: .infinity)
                .padding()
                .foregroundColor(.white)
                .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
                .cornerRadius(40)
                .padding(.horizontal, 20)
            }
            
        }
    }
}

從以上的程式碼你可以見到,你需要為每一個按鈕複製所有修飾器。那如果你的設計師想要修改按鈕樣式呢?你就會需要修改所有修飾器!這會是一個非常繁鎖的工作,也不是一個好的程式碼寫法。那你該如何將樣式歸納在一起,然後重複使用呢?

SwiftUI 提供了一個稱作 ButtonStyle 的協定 (protocol),可以讓你建立自己的按鈕樣式。要為按鈕建立一個可以重複使用的樣式,我們可以建立一個新的 GradientBackgroundStyle struct,讓它遵循 ButtonStyle 協定。在 #if DEBUG 上方插入以下這段程式碼:

struct GradientBackgroundStyle: ButtonStyle {
    
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .frame(minWidth: 0, maxWidth: .infinity)
            .padding()
            .foregroundColor(.white)
            .background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
            .cornerRadius(40)
            .padding(.horizontal, 20)
    }
}

這個協定需要我們提供 makeBody 函數的實作,此函數可以傳入一個 configuration 參數。這裡的 configuration 參數包含了一個 label 屬性,讓你可以應用修飾器來變更按鈕的樣式。在以上的程式碼中,我們只需要應用前面使用過的同一組修飾器即可。

所以,你要如何針對按鈕客製樣式呢?SwiftUI 提供了一個稱作 .buttonStyle 的修飾器,讓你可以這樣設定按鈕樣式:

Button(action: {
    print("Delete tapped!")
}) {
    HStack {
        Image(systemName: "trash")
            .font(.title)
        Text("Delete")
            .fontWeight(.semibold)
            .font(.title)
    }
}
.buttonStyle(GradientBackgroundStyle())

很酷吧?我們已經把程式碼簡化了,你只需要用一行程式碼,就可以輕鬆為所有按鈕設定樣式。

buttonStyle-modifier-1

你也可以透過 configuration 的 isPressed 屬性,來判斷按鈕是否有被按下,這讓你可以在使用者按下按鈕時改變樣式。舉例來說,譬如我們想要製作一個當使用者按下去時會變小一點的按鈕,你可以加入一行程式碼如下:

buttonStyle-modifier-2

這個 scaleEffect 修飾器讓你可以將按鈕 (或任何視圖) 放大或縮小。要放大按鈕,你可以傳遞一個大於 1.0 的值;要縮小按鈕,你就可以傳遞一個小於 1.0 的值。

.scaleEffect(configuration.isPressed ? 0.9 : 1.0)

這行程式碼中所做的,就是當使用者按下按鈕時,按鈕就會縮小 (也就是 0.9);而當使用者手指放開後,它會回復到原來的大小 (也就是 1.0)。如果你執行這個 App,當按鈕縮放時,你應該會見到一個不錯的動畫效果。這就是 SwiftUI 的威力之處!你不需要再寫其他的程式碼,因為它已經內建了動畫特效。

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行 App 軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。

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