第 4 章
使用堆疊視圖設計UI
To the user, the interface is the product.
- Aza Raskin
我已經簡要介紹了 SwiftUI 的觀念,並向你展示如何處理一些基本的 UI 元件,其中包括垂直堆疊視圖(即 VStack )。我們建立的第一個 App 非常簡單,隨著 App 的 UI 變得更複雜時,將會需要使用不同型態的堆疊視圖來建立使用者介面,更重要的是,你需要學習如何建立可相容各式螢幕大小的 UI。
在本章中,我將會介紹所有類型的堆疊,並建立更全面的 UI,在真實世界所運用的 App 中,你可能看過這些 UI。此外,我將介紹另一種用來顯示圖片的常見 SwiftUI 元件。你將會學習到:
- 使用圖片視圖(Image View)來顯示圖片。
- 使用內建的素材目錄(Asset Catalog)來管理圖片。
- 使用堆疊視圖來佈局使用者介面。
- 使用尺寸類別來調整堆疊視圖。
你會很驚訝使用堆疊視圖便可完成這麼多的工作。
VStack、HStack與 ZStack 介紹
SwiftUI 為開發者提供三種不同類型的堆疊,以組合不同方向的視圖。依據你要如何排列視圖,而可以使用:
- HStack - 水平排列視圖。
- VStack - 垂直排列視圖。
- ZStack - 將一個視圖重疊在其他視圖之上。
圖 4.1 展示了如何使用這些堆疊來組織視圖。

範例 App
首先,我們來看一下將建立的範例 App。我會示範如何使用堆疊視圖來佈局歡迎畫面, 如圖 4.2 所示。

在前面的章節中,你已經使用了VStack 來垂直排列UI 元件,但是要建立App UI,你需要組合各種類型的堆疊視圖,如你所見,該App UI 在所有螢幕尺寸上都運作良好。如果你有使用過UIKit 的經驗,可能就會知道使用自動佈局來建立相容所有螢幕尺寸的UI 的必要性,而對初學者而言,自動佈局可能是一個複雜的主題且不易學習,好消息是SwiftUI 不再使用自動佈局,並簡化了編寫自適應UI 的過程,你很快就會明白我的意思。
建立新專案
現在開啟 Xcode,並建立一個新的 Xcode 專案,選擇「Application( 在iOS 下)→ App」,並點選「Next」按鈕。在專案選項中,你可以填入下列資訊:
- Product Name(專案名稱): StackViewDemo – 這是你的 App名稱。
- Team:(團隊) 這裡先不做更動。
- Organization Identifier((組織識別碼)): com.appcoda – 這其實是反向域名,如果你擁 有網域,你可以使用自己的網域名稱;否則的話,你可以使用「com.appcoda」或者只填寫「edu.self」。
- Bundle Identifier(套件識別碼): com.appcoda.StackViewDemo - com.appcoda.StackViewDemo;這是你的App的唯 一識別碼,在App 送審時會用到。你不需要填入這個選項,Xcode 會自動幫你產生。
- Interface(介面): SwiftUI - 如前所述,Xcode 現在支援兩種建立 UI 的方式,這裡請 選擇「SwiftUI」,因為本書會使用 SwiftUI 來開發 UI。
- Language(語言): Swift – ;我們會使用 Swift 來開發專案。
- Include Tests(包含測試): [不用勾選] – 這個選項不要勾選,此專案不會進行任何測試。
點選「Next」按鈕,接著Xcode 會詢問你要將 StackViewDemo 專案儲存在哪裡,在你的 Mac 電腦中挑選一個資料夾,並點選「Create」按鈕來繼續。
加入圖片至 Xcode 專案中
你可能會注意到,範例 App 包含了三張圖片,問題是你該如何在Xcode 專案中綁定三張圖片呢?
在每個Xcode 專案中,都包含了一個素材目錄(即Assets ),用來管理你的 App 所使用的圖片及圖示,如圖4.3 所示。至專案導覽器並選取「Assets」資料夾,它預設的Appicon 與AccentColor 集是空的。本章並不會介紹App 圖示與強調色(Accent Color ),之後本書的其他章內容會複習這個部分。

現在下載本章所準備的圖片集 (https://www.appcoda.com/resources/swift4/stackviewdemo-images.zip) ,並將其解壓縮到Mac 中,這個壓縮檔包含了五個圖檔:
- user1.pdf
- user2.png
- [email protected]
- [email protected]
- user3.pdf
Credit: 圖片是由 usersinsights.com 所提供。.
iOS 支援兩種類型的圖檔:「點陣圖」(Raster Image )及「向量圖」(Vector Image )。PNG 與 JPEG 等常見的圖片格式都歸類為點陣圖,點陣圖是使用像素網格來形成一個完整的圖片,它有放大後品質不佳的問題,將點陣圖片放大後,通常會失真,因此Apple 建議開發者在使用 PNG 時提供三種不同解析度的圖片。在本例中,圖檔共有三個版本,其中後綴為 @3x 的圖片擁有較高的解析度,適用於 iPhone 8 Plus、iPhone 14/15 Pro 與 iPhone 14/15 Pro Max;後綴為 @2x 的圖片,適用於 iPhone SE/8/14/15;而沒有後綴 @ 的圖片, 則適用非視網膜螢幕的舊裝置(例如:iPad 2)。如果有興趣了解如何使用圖片的細節, 你可以進一步參考下列的連結: (https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/image-size-and-resolution/).
向量圖的檔案格式通常是 PDF 或 SVG,你可以使用像是 Sketch 與 Pixelmator 等工具來建立向量圖。和點陣圖不同的是,向量圖是以路徑所組成,而不是由像素所組成,其圖檔可以放大而不會失真。由於這個功能,你只需要為 Xcode 提供 PDF 格式的單一版本圖片即可。
為了示範,我故意在範例中加入這兩種圖檔,而開發一個真正的 App 時,你通常會使用其中一種圖檔。那麼,哪一種類型的圖檔比較好呢?如果可以的話,請你的設計師準備 PDF 格式的圖檔,整體檔案較小,且不會因為縮放而失真。
要將圖檔加入至素材目錄的話,你只需要將這些圖片從Finder 拖曳至套圖清單或套圖檢視器中,如圖 4.4 所示。
當你將圖片加到素材目錄中,套圖視圖會自動歸類這些圖片至不同的位置中,如圖 4.5 所示。之後,如果要使用該圖片,你只需要使用該圖片的套圖名稱(例如:user1)即可。你可省略檔案副檔名,即使你有同一圖片的多個版本(例如:user2),也不必擔心要使用哪個版本的圖片(@2x / @3x ),這些都由 iOS 相應處理。
使用堆疊視圖佈局標題標籤
現在你已經在專案中綁定必要的圖片,我們將繼續建立堆疊視圖。首先開啟 ContentView. swift,我們從這兩個標籤的佈局開始,如圖 4.6 所示。
我相信你知道如何建立這兩個標籤,因為我們之前已經使用過 VStack。堆疊視圖可以在水平與垂直方向排列多個視圖。由於標題及副標題標籤是垂直排列,因此垂直堆疊視圖是較合適的選擇。
現在更新 ContentView 結構如下:
struct ContentView: View {
var body: some View {
VStack {
Text("Instant Developer")
.fontWeight(.medium)
.font(.system(size: 40))
.foregroundStyle(.indigo)
Text("Get help from experts in 15 minutes")
}
}
}
我們使用 VStack 來嵌入兩個 Text 視圖,如圖 4.7 所示。對於「Instant Developer」標籤, 我們設定固定的字型大小(例如:40 點)來使字體變大一點以及變更字型粗細來加粗文字; 要變更字型顏色,我們使用foregroundStyle 修飾器,並設定顏色為「.indigo」。
使用留白與間距
預設上,這個堆疊視圖是顯示在螢幕的中央,不過如果你參見圖 4.2,這兩個標籤應該放置在靠近狀態列的位置,那麼我們該如何移動這兩個標籤呢?
訣竅是使用一個名為「留白」(Spacer )的特殊 SwiftUI 元件,留白視圖是一個沒有內容的視圖,它在堆疊視圖中占用儘可能多的空間,例如:當你將留白視圖放置在垂直佈局中,它會在堆疊允許的範圍內垂直擴展。
我們來看一下這個留白視圖的實際應用,如此你就會了解它如何幫助你排列 UI 元件。
要將兩個標籤推移到螢幕的上方,我們可以建立另一個 VStack(我們稱之為「根堆疊視圖」)來嵌入到目前的堆疊視圖中,然後加入一個 Spacer 視圖。
你可以按住鍵不放,然後在 VStack 上點擊,在內容選單中選擇「Embed in VStack」,Xcode 會自動將目前的VStack 包裹在另一個 VStack 中,如圖 4.8 所示。

接下來,在根堆疊視圖的右大括號之前插入 Spacer 視圖,如圖 4.9 所示。

當加入留白視圖後,它會展開以占據垂直堆疊視圖的所有可用空間,然後將標籤推到螢幕的頂部。
如果你仔細查看圖 4.2,你會發現這兩個標籤仍然沒有放置在預期的位置上,它現在離螢幕的頂部邊緣太近了,我們需要在邊緣與文字視圖之間留出一些間距才行。
在 SwiftUI 中,你可以使用padding 修飾器在視圖周圍增加間距。在此範例中,你可以加入 padding 修飾器到根VStack 視圖,如下所示:
VStack {
.
.
.
}
.padding(.top, 30)
padding 修飾器接受兩個可選型別的參數,你可以指定要填入的邊緣與間距量,這裡我們告知 SwiftUI 於頂部邊緣加入間距,並設定間距量為「30 點」,如圖 4.10 所示。

在 SwiftUI 中,間距對於排列視圖的佈局非常有用,透過將間距應用在視圖中,你可以在不同視圖之間加入一些間距。
使用圖片
接下來,我們將佈局三個使用者圖片。在 SwiftUI 中,我們使用一個名為「Image」的視圖來顯示圖片。由於我們已經將圖片匯入素材目錄中,你可以編寫程式碼如下,以在螢幕上顯示圖片:
Image("user1")
你不需要指定檔案副檔名(例如:png / jpg / pdf),你只需要告知 Image 視圖圖片名稱。要將圖片視圖放置在文字視圖下的話,你可以在 Spacer() 前面插入上列的程式碼,如圖 4.11 所示。

預設上,iOS 會以原始大小來顯示圖片。要在 SwiftUI 中調整圖片大小,則我們可以加入 resizable 修飾器,如下所示:
Image("user1")
.resizable()
iOS 會延伸圖片來填滿可用區域,圖 4.12 顯示了此修飾器的效果。

此延伸模式並沒有考量圖片本身的長寬比,它只是延伸每一邊來填滿整個視圖區域。要保持原來圖片的長寬比,則你可以應用 scaledToFit 修飾器如下:
Image("user1")
.resizable()
.scaledToFit()
或者,你可以使用 aspectRatio 修飾器,並設定內容模式為「.fit」,也可達到相同的結果。
Image("user1")
.resizable()
.aspectRatio(contentMode: .fit)
當你應用這些修飾器後,圖片將自動調整大小,並保持長寬比,如圖 4.13 所示。

使用水平堆疊視圖來排列圖片
現在你應該了解如何顯示圖片,我們來看如何將三張圖片並排在一起。之前我們使用 VStack 來垂直排列視圖,SwiftUI 框架提供另一種名為「HStack」的堆疊視圖來水平排列視圖。
使用 HStack 視圖來包裹 Image 視圖,並加入其他兩個視圖,如下所示:
HStack {
Image("user1")
.resizable()
.scaledToFit()
Image("user2")
.resizable()
.scaledToFit()
Image("user3")
.resizable()
.scaledToFit()
}
當你將這些圖片視圖嵌入水平堆疊時,它會從左至右來並排放置圖片,如圖 4.14 所示。

圖片堆疊太靠近畫面的左右邊緣了,若要加入一些間距,則我們可以加入 padding 修飾器到 HStack,如下所示:
HStack {
.
.
.
}
.padding(.horizontal, 20)
這告知 iOS 要在 HStack 視圖的左右邊緣加入 20 點的間距,如圖 4.15 所示。

我想要對水平堆疊視圖進行一些調整:
- 如果你仔細觀看這些圖片,它們並不是完全對齊的,我們想要將所有的圖片都與堆疊 視圖的底部邊緣對齊。
- 我們在這些圖片之間加入一些間距。
HStack 視圖實際上提供兩個可選型別的參數,一個是 alignment,另一個則是 spacing。透過為這些參數傳送一個適當的值,我們便可輕鬆完成上述的需求。
我們變更 HStack 的初始設定如下:
HStack(alignment: .bottom, spacing: 10) {
.
.
.
}
這告知水平堆疊視圖將所有的圖片視圖對齊底部邊緣,並在視圖之間加入10 點的間距。
圖片現在已經完美對齊,而且看起來更加好看,對吧?
在圖片下方加入標籤
我們尚未在圖片下方加入標籤,這個實作應該非常簡單。你可以在 Spacer() 視圖之前插入下列的程式碼:
Text("Need help with coding problems? Register!")
正如圖 4.16 的預覽所示,文字視圖與圖片視圖靠得太近了。與 HStack 類似,VStack 也接受一個名為「spacing」的參數,可讓你為堆疊視圖中的項目加入一些間距。

現在更新根 VStack 視圖如下,以指定間距:
VStack(spacing: 20) {
.
.
.
}
你應該注意到圖片堆疊視圖與文字視圖現在已經分開了,如圖 4.17 所示。

使用堆疊視圖佈局按鈕
我們還沒有完成,接下來繼續在螢幕底部佈局兩個按鈕,這兩個按鈕的固定寬度為「200 點」。
要建立紫色背景的「Sign Up」按鈕時,程式碼可以編寫如下:
Button {
} label: {
Text("Sign Up")
}
.frame(width: 200)
.padding()
.foregroundStyle(.white)
.background(.indigo)
.clipShape(RoundedRectangle(cornerRadius: 10))
你應該很熟悉這些程式碼了,因為它和建立「Hello World」按鈕的程式碼非常相似。對你來說,比較陌生的地方是 frame 修飾器,它用來將按鈕的寬度限制為「200 點」。
同樣的,要建立「Sign Up」與「Log In」按鈕的佈局時,我們將按鈕嵌入到 VStack 視圖中,如下所示:
VStack {
Button {
} label: {
Text("Sign Up")
}
.frame(width: 200)
.padding()
.foregroundStyle(.white)
.background(.indigo)
.clipShape(RoundedRectangle(cornerRadius: 10))
Button {
} label: {
Text("Log In")
}
.frame(width: 200)
.padding()
.foregroundStyle(.white)
.background(.gray)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
你可以將程式碼放在 Spacer() 視圖的後面,當完成變更後,你應該會在預覽窗格中看到兩個按鈕,如圖 4.18 所示。

設定預覽名稱與橫向預覽
Xcode 使用我們所選擇的模擬器來顯示UI 預覽,例如:我選擇「iPhone 15 Pro」作為模擬器,如果你選擇其他的模擬器,Xcode 會使用你所選擇的模擬器來渲染預覽。
預設上,預覽名稱設定為「Preview」,如果你想要使用不同的名稱該如何做呢?
我們來看一下預覽的程式碼:
#Preview {
ContentView()
}
這段程式碼是為了產生 ContentView 的預覽而編寫的。#Preview 巨集提供了許多可選參數來供開發者自訂預覽。
更新預覽的程式碼如下:
#Preview("ContentView") {
ContentView()
}
在上列的程式碼中,我們指定預覽名稱為「ContentView」,如果你查看預覽畫布,可能會注意到相應的名稱已經變更,如圖 4.19 所示。

另一個常見的問題是我們如何在預覽畫布中旋轉裝置?如果你需要橫向檢查 App UI, 你可以為 #Preview 插入另一個程式碼區塊。在 ContentView.swift 中插入下列的程式碼:
#Preview("ContentView (Landscape)", traits: .landscapeLeft) {
ContentView()
}
程式碼修改後,預覽窗格應該會顯示兩個標籤:「ContentView」與「ContentView (Landscape)」,這是一個非常棒的功能,可讓你預覽 UI,並評估其在兩個方向上的效果, 如圖 4.20 所示。

另外,你可以使用 「Variants」按鈕,並選取「Orientation Variants」,以直向模式及橫向模式來預覽 UI。此選項提供了一個額外的方式來評估 UI 在不同裝置方向下的外觀和功能。
取出視圖使程式碼有更好的結構
在我們繼續佈局UI 之前,讓我先教你一個組織程式碼的技巧。當你要建立一個包含多個元件的更複雜 UI 時,ContentView 中的程式碼最後會變成一個難以查看及除錯的巨大程式碼區塊,最佳的作法是將大塊的程式碼拆分成更小的程式碼區塊,如此程式碼會更易於閱讀及與維護。
Xcode 內建了重構 SwiftUI 程式碼的功能,例如:如果我們要將存放「Sign Up」與「Log In」按鈕的 VStack 取出,你可以按住鍵並點擊VStack,然後選擇「Extract Subview」來取出程式碼,如圖 4.21 所示。

Xcode取出程式碼區塊,並建立一個名為「ExtractedView」的預設結構。將「ExtractedView」重新命名為「VSignUpButtonGroup」,以賦予其更有意義的名稱,詳見圖 4.22。

這是開發 SwiftUI App 時非常有用的技術,透過將程式碼取出到單獨的子視圖中,你的程式碼結構現在更有條理了。看一下 ContentView 中的程式碼,它現在已經更簡潔且更易於閱讀。
使用尺寸類別調整堆疊視圖
你對如圖 4.20 所示的 App 橫向佈局有何看法呢?該佈局在 iPhone 裝置上看起來不佳, 要解決這個問題,你可以考慮並排放置按鈕,如此可釋出一些空間來放大圖片,並提升整體的設計,如圖 4.23 所示。

請記住,這些改變只適用於iPhone 橫向模式,對於 iPhone 直向模式,兩個按鈕的位置則保持不變,那麼你該如何做呢?
這會導引到一種名為「自適應佈局」(Adaptive Layout )的UI 設計觀念,透過自適應佈局,你的 App 可以讓 UI 自適應特定裝置或裝置方向。
要實作自適應佈局,Apple 引入一個名為「尺寸類別」(Size Classes )的觀念,這大概是使自適應佈局成為可能的一個最重要觀念了。「尺寸類別」是依據裝置的螢幕尺寸與方向來對裝置做分類。
尺寸類別識別垂直(高度)與水平(寬度)尺寸的顯示空間的相對量,其有兩種尺寸類別類型:「常規」(Regular)與「緊湊」(Compact),常規尺寸類別表示較大的螢幕空間, 緊湊尺寸類別則表示較小的螢幕空間。
透過使用尺寸類別來描述每個顯示尺寸,這將產生四個裝置象限:「常規寬度- 常規高度」(Regular width-Regular Height )、「常規寬度- 緊湊高度」(Regular width-Compact Height )、「緊湊寬度- 常規高度」(Compact width- Regular Height )、「緊湊寬度- 緊湊高度」(Compact width-Compact Height )。
圖 4.24 顯示了 iOS 裝置及其相對應的尺寸類別。

要表達一個顯示環境,你必須同時指定水平尺寸類別(Horizontal Size Class )以及垂直尺寸類別(Vertical Size Class )。例如:iPad 具有常規的水平(寬度)尺寸類別以及常規的垂直(高度)尺寸類別。對於我們的自訂條件,我們希望為 iPhone 提供特定的橫向佈局,換句話說,當垂直尺寸類別設定為「緊湊」(compact)時,我們可以改變按鈕的佈局。
那麼,我們如何才能找出裝置的垂直尺寸類別呢?SwiftUI 框架提供@Environment 屬性包裹器(Property Wrapper )來取得垂直尺寸類別,你可以插入下列的程式碼來取得目前的尺寸類別
@Environment(\.verticalSizeClass) var verticalSizeClass
當裝置方向發生變化,verticalSizeClass 的值就會自動更新。
使用這個變數,我們可以參考 verticalSizeClass 的值來變更按鈕群組的佈局。你可以將 VSignUpButtonGroup() 替換為下列的程式碼:
if verticalSizeClass == .compact {
HSignUpButtonGroup()
} else {
VSignUpButtonGroup()
}
當垂直尺寸類別設定為「.compact」時,我們透過呼叫 HSignUpButtonGroup() 來將按鈕群組水平對齊,這裡的HSignUpButtonGroup() 是我們將要實作的新視圖。
現在插入下列的程式碼來建立 HSignUpButtonGroup 視圖:
struct HSignUpButtonGroup: View {
var body: some View {
HStack {
Button {
} label: {
Text("Sign Up")
}
.frame(width: 200)
.padding()
.foregroundStyle(.white)
.background(.indigo)
.clipShape(RoundedRectangle(cornerRadius: 10))
Button {
} label: {
Text("Log In")
}
.frame(width: 200)
.padding()
.foregroundStyle(.white)
.background(.gray)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
HSignUpButtonGroup 的程式碼與 VSignUpButtonGroup 的程式碼幾乎相同,我們只需將 VStack 更改為HStack,即可並排佈局兩個按鈕。當你進行變更後,預覽應該會相應更新 UI,對於 iPhone 橫向模式,按鈕應該能水平對齊了,如圖 4.25 所示。

現在 UI 於 iPhone 橫向上看起來更好了,這就是我們如何利用尺寸類別來提供特定 UI,並對不同的螢幕大小微調UI 的方式。
保存向量資料
在我總結本章之前,我要介紹一個 Xcode 的功能, 即「保存向量資料」(Preserve Vector Data )。如前所述,在 iOS 開發中,我們比較傾向使用向量圖而不是點陣圖,因為向量圖不管怎麼縮放,圖片也不會失真,不過要注意的是這只有部分正確。
使用向量圖時,Xcode 會自動將圖片轉換為靜態圖片(@1x、@2x、@3x ),它非常類似於我們準備好的user2 圖片,但轉換工作是由 Xcode 處理,在這種情況下,放大圖片時,圖片品質會稍微受到影響。如果你試著在 iPad Pro(12.9 英吋)執行這個範例,則應該會發現圖片的品質不佳。
Xcode 內建了一個名為「保存向量資料」(Preserve Vector Data )的功能,可以讓你保存圖片的向量資料。預設上,這個選項是停用的,要啟用它的話,你可以至 Assets. xcassets,並選取其中一張圖片,在屬性檢閱器中勾選「Preserve Vector Data」核取方塊來啟用這個選項,如圖 4.26 所示。

現在,如果你再次在 iPad Pro(12.9 英吋)執行這個App,你會發現圖片的品質看起來好多了,圖 4.27 示範了啟用或停用該選項的圖片差異。

你的作業:建立新 UI
為了幫助你更加了解堆疊視圖與尺寸類別的工作原理,我們來進行一個練習,請試著去建立一個如圖4.28 所示的 UI,你可以從下列網址下載所需的圖片: http://www.appcoda.com/resources/swift4/student-tutor.zip

Credit: 背景圖片是由 Luka Dadiani 所提供。
給你一個提示,要實作背景圖片,你可以加上 background 修飾器如下:
VStack {
.
.
.
}
.background {
Image("background")
.resizable()
.ignoresSafeArea()
}
在修飾器內,你可以放置圖片作為背景,而 Image 視圖的 ignoresSafeArea 修飾器會將圖片擴展到整個螢幕。當你進行練習時,你就會了解我的意思。
本章小結
恭喜 !你已經完成本章,並掌握了如何使用堆疊視圖與尺寸類別來建立自適應 UI 的技能。
「堆疊視圖」是 SwiftUI 框架提供的極其強大的視圖元件,透過組合 VStack、HStack、ZStack,你可以輕鬆建立自適應不同螢幕尺寸的複雜 UI。本章只是對堆疊視圖進行介紹,之後當我們建立一個真實世界的 App 時,你將進一步學會更多使用堆疊視圖的高階佈局技術。
本文摘自《iOS 17 App程式設計實戰心法》(SwiftUI)》一書。如果你想更深入學習Swift程式設計和下載完整程式碼,你可以從 AppCoda網站 購買完整電子版。