讀者:楊于葳
本文為《快速精通 iOS 16 程式設計:從零開始活用 Swift 與 SwiftUI 開發技巧》這本書的閱讀筆記與實作記錄的目錄,目的是記錄自己的學過程,以及幫助想利用 Swift 從頭建立一個自己心目中的 App 的人。以下是「CHAPTER 04 使用堆疊視圖設計UI」的筆記內容。
UI 指的是使用者介面(User Interface),在軟體開發中,UI 設計師負責設計和開發人與電腦系統之間的互動介面,讓使用者可以更加直覺的操作電腦系統。
我們第一個建立的 App 非常簡單,如果 App 的 UI 變得越來越複雜的時候,就會需要使用不同型態的堆疊視圖建立使用者介面,還要學習要怎麼做,才能讓 UI 相容各式各樣的螢幕大小。
本章會介紹所有類型的堆疊視圖,建立更全面的 UI,以及介紹顯示圖片的常見 SwiftUI 元件。
4.1 VStack、HStack與ZStack介紹
SwiftUI 提供三種不同類型的堆疊:
- HStack:x 軸方向
- VStack:y 軸方向
- ZStack:z 軸方向
堆疊視圖
4.2 範例App
如果之前有使用過 UIKit,就會知道使用自動佈局來建立相容所有螢幕尺寸的 UI 是必須的,而且自動布局對於初學者而言,非常複雜而且又不容易學會,好消息是 SwiftUI 不再使用自動佈局,建立相容所有螢幕尺寸的 UI 會變得非常容易。
4.3 建立新專案
開啟 Xcode 建立一個新的專案。選擇 iOS ➜ Application ➜ App ➜ Next,填寫以下資訊:
- 專案名稱(Project name):StackViewDemo(為什麼專案名稱的文字間都不空白呢❓❓❓)
- 團隊(Team):不更動。(❓❓❓)
- 組織識別碼(Organization Identifier):此為反向域名(❓❓❓)如果有自己的網站可以填入,如果沒有可以使用「com.」後面接自己的名字。
- 套件識別碼(Bundle Identifier):不需填寫,Xcode 會自動生成。
- 介面(Interface):選擇 SwiftUI。
- 語言(Language):選擇 Swift。
- 使用 Core Data(Use Core Data):不用勾選。
- 包含測試(Include Test):不用勾選。
輸入以上資訊後,設定資料夾儲存位置,再點選「Create」按鈕。
4.4 加入圖片至Xcode專案中
每個 Xcode 專案中都包含一個素材目錄(Assets),用來管理 App 使用的圖片和圖示。本章不會介紹 App 圖示和強調色(Accent Color)。
iOS 可支援「點陣圖」(Raster Image)及「向量圖」(Vector Image)。PNG 和 JPEG 都屬於點陣圖,放大後會出現品質不佳的問題,因此 Apple 建議開發者使用 PNG 圖檔時,要提供三種不同解析度的圖片。
本書範例圖片,總共有五個:
- user1.pdf:適用非視網膜螢幕的舊裝置 iPad 2。
- user2.png:適用非視網膜螢幕的舊裝置 iPad 2。
- user2@2x.png:@2x 這張圖片,適用於 iPhone SE/8/13/14。
- user2@3x.png:@3x 這張圖片有較高的解析度,適用於 iPhone 8 Plus 、iPhone 13/14 Pro、iPhone 13/14 Mac。
- user3.pdf:適用非視網膜螢幕的舊裝置 iPad 2。
Apple 開發者網站的 Scale factors
「向量圖」的格式通常是 PDF 或 SVG,我們可以用 Sketch 或 Pixelmator 來建立向量圖,不會因為放大圖片而失真。Sketch 和 Pixelmator 都是支援 Mac 系統的 UI 設計工具。Adobe 的 UI 設計工具叫 Adobe XD。
這裡為了示範,所以在範例圖片裡有兩種圖檔(PNG 和 PDF),真正在開發時只需要其中一種就可以了,通常會使用 PDF 檔,因為整體的檔案比較小,放大後也不會失真。
要把加入這些範例圖檔到素材目錄(Assets),只需要將這些圖片拖曳到裡面就可以了,系統會自動歸類這些圖片到不同的位置。
將圖片拖曳到素材目錄
系統會自動歸類
4.5 使用堆疊視圖佈局標題標籤
在 ContentView.swift 裡面建立兩個標籤,產生標題和副標題,並使用 foregroundColor 修飾器,把顏色設定成「.indigo」。
建立兩個標籤,並把標題色設定成 .indigo
程式碼如下:
import SwiftUI struct ContentView: View { var body: some View { VStack { Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.indigo) Text("Get help from experts in 15 minutes") } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
.indigo 色
4.6 使用留白與間距
接著要將標題與副標題移動到畫面的上方。首先,要在 VStack 嵌入另一個 VStack,方法是在原本的 VStack 標籤上,按住 command 鍵不放,選擇 Embed in VStack。接著插入留白視圖( Spacer ),會把標籤推到螢幕的頂部。
使用留白與間距
程式碼如下:
import SwiftUI struct ContentView: View { var body: some View { VStack { VStack { Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.indigo) Text("Get help from experts in 15 minutes") } Spacer() } .padding(.top, 30) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
4.7 使用圖片
在螢幕上顯示圖片的程式碼:
Image("user1")
在螢幕上顯示圖片
在螢幕上顯示的圖片,使用 resizable 修飾器,填滿可用區域:
Image("user1") .resizable()
使用 resizable 修飾器,填滿可用區域
如果想維持圖片原來的長寬比,可以使用 scakedToFit 修飾器,程式碼如下:
Image("user1") .resizable() .scaledToFit()
使用 scakedToFit 修飾器,維持圖片原來的長寬比
4.8 使用水平堆疊視圖來排列圖片
接下來要使用 HStack 讓圖片可以水平排列,程式碼如下:
HStack { Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() }
使用 HStack 讓圖片可以水平排列
如果感覺圖片太靠近邊緣了,可以使用 padding 修飾器,加一些間距到 HStack 裡,程式碼如下:
HStack { Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() } .padding(.horizontal, 20) Spacer() }
使用 padding 修飾器,加一些間距到 HStack 裡
仔細看這三張圖片,會發現他們並不是完全對齊的(最下方的線是參差不齊的),如果我們想要將三張圖片都以底部對齊,可以使用兩種參數調整。一個是 alignment,另一個是 spacing,只要輸入合適的參數,就可讓圖片以底部對齊。程式碼如下:
HStack(alignment: .bottom, spacing: 10){ Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() }
以底部對齊
4.9 在圖片下方加入標籤
在圖片下方加入標籤,程式碼如下:
Text("Need help with coding parblems. Register!")
在圖片下方加入標籤
接下來要調整文字與圖片之間的距離,我們要在 VStack 也加入一些間距,程式碼如下:
VStack(spacing:20) {
調整文字與圖片之間的距離
4.10 使用堆疊視圖佈局按鈕
接下來要在畫面的最下方加入兩個按鈕,這兩個按鈕的寬度固定都是200點。第一個按鈕是「紫色背景的 Sign Up」,第二個按鈕是「灰色背景的 Log In」,完整程式碼如下:
// // ContentView.swift // StackViewDemo // // Created by 楊于葳 on 2023/4/15. // import SwiftUI struct ContentView: View { var body: some View { VStack(spacing:20) { VStack{ Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.indigo) Text("Get help from experts in 15 minutes") } HStack(alignment: .bottom, spacing: 10){ Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() } .padding(.horizontal, 20) Text("Need help with coding parblems. Register!") Spacer() VStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } .padding(.top, 30) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
加入兩個按鈕
4.11 使用不同的裝置預覽UI
Xcode 會根據我們選擇的模擬器讓我們預覽 UI 的樣貌。如果我們想要再多個模擬器上預覽時,只要在預覽的程式碼進行修改就可以了,程式碼如下:
struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") .previewInterfaceOrientation(.landscapeLeft) ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro Max")) .previewDisplayName("iphone 14 Pro Max") ContentView() .previewDevice(PreviewDevice(rawValue: "iPad Air (5th generation)")) .previewDisplayName("iPad Air") } }
更新模擬器
其中,書中的 iPad 使用的是「iPad Air (4th generation)」,系統沒有反應,改成「5th generation」就可以看到變化了。
4.12 取出視圖使程式碼有更好的結構
當我們要建立一個包含許多複雜元件的 UI 時,程式碼最後會變成一個難以閱讀的巨大程式碼,最好的方式是把大區塊分割成小區塊,這樣會更方便日後的閱讀和維護。
例如,我們要將存放兩個按鈕的 VStack 取出,可以按住 command 鍵,點擊 VStack,選擇 Extract Subview。此時程式碼區塊會自動縮減成一行:
ExtractedView()
下方也會有一個 ExtractedView 區塊
struct ExtractedView: View { var body: some View { VStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } }
為了方便日後閱讀,我們可以把預設名稱 ExtractedView 改成 VSignUpButtonGroup,讓我們日後容易辨識。
預設名稱 ExtractedView 改成 VSignUpButtonGroup
完整程式碼如下:
// // ContentView.swift // StackViewDemo // // Created by 楊于葳 on 2023/4/15. // import SwiftUI struct ContentView: View { var body: some View { VStack(spacing:20) { VStack{ Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.indigo) Text("Get help from experts in 15 minutes") } HStack(alignment: .bottom, spacing: 10){ Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() } .padding(.horizontal, 20) Text("Need help with coding parblems. Register!") Spacer() VSignUpButtonGroup() } .padding(.top, 30) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") .previewInterfaceOrientation(.landscapeLeft) ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro Max")) .previewDisplayName("iphone 14 Pro Max") ContentView() .previewDevice(PreviewDevice(rawValue: "iPad Air (5th generation)")) .previewDisplayName("iPad Air") } } struct VSignUpButtonGroup: View { var body: some View { VStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } }
4.13 使用尺寸類別調整堆疊視圖
自適應佈局(Adaptive Layout)可以讓 UI 自動適應特定的裝置或方向,要達到這個目的就要使用「尺寸類別」(Size Classes)的觀念,定義「常規」(Regular)和「緊湊」(Compact)。
透過尺寸類別,可以描述不同的顯示尺寸,並將他們區分成四個象限:
- 「常規寬度 - 常規高度」(Regular width - Regular height)
- 「常規寬度 - 緊湊高度」(Regular width - Compact height)
- 「緊湊寬度 - 常規高度」(Compact width - Regular height)
- 「緊湊寬度 - 緊湊高度」(Compact width - Compact height)
不同的裝置參照表,可至 Apple 開發者網站查看。
iPhone 14 Pro 的顯示尺寸
@Enviroment 屬性包裹器(Property Wrapper)可以用來取得垂直尺寸的類別,將結果儲存在 verticalSizeClass 變數中,之後只要裝置發生變化, verticalSizeClass 的值就會自動更新。
@Environment 是一個屬性包裝器,提供了一種在應用程式的層次結構中傳遞數據的方式。
\.verticalSizeClass 是一個 key path,表示垂直尺寸的類別。
var verticalSizeClass 是一個變數,用來存儲從 Environment 中獲取的垂直尺寸類別。
@Environment(\.verticalSizeClass) var verticalSizeClass
透過 var verticalSizeClass 變數,我們可以將 VSignUpButtonGroup() 替換成下列程式碼:
if verticalSizeClass == .compact { HSignUpButtonGroup() } else { VSignUpButtonGroup() }
橫放時,按鈕呈現左右水平的樣子
調整過後的程式碼,在介面是橫放時按鈕會呈現左右水平的樣子,直放的時候按鈕會呈現上下擺放的位置,完整程式碼如下:
// // ContentView.swift // StackViewDemo // // Created by 楊于葳 on 2023/4/15. // import SwiftUI struct ContentView: View { @Environment(\.verticalSizeClass) var verticalSizeClass var body: some View { VStack(spacing:20) { VStack{ Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.indigo) Text("Get help from experts in 15 minutes") } HStack(alignment: .bottom, spacing: 10){ Image("user1") .resizable() .scaledToFit() Image("user2") .resizable() .scaledToFit() Image("user3") .resizable() .scaledToFit() } .padding(.horizontal, 20) Text("Need help with coding parblems. Register!") Spacer() if verticalSizeClass == .compact { HSignUpButtonGroup() } else { VSignUpButtonGroup() } } .padding(.top, 30) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") .previewInterfaceOrientation(.landscapeRight) ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro Max")) .previewDisplayName("iphone 14 Pro Max") ContentView() .previewDevice(PreviewDevice(rawValue: "iPad Air (5th generation)")) .previewDisplayName("iPad Air") } } struct VSignUpButtonGroup: View { var body: some View { VStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } } struct HSignUpButtonGroup: View { var body: some View { HStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } }
4.14 保存向量資料
保存向量資料(Preserve Vector Data)的功能,可以讓我們保存圖片的向量資料。這項功能預設是停用的,如果要啟用它,可以到 Assets.xcassets,選取其中一張圖片,在屬性檢閱器中,勾選「Preserve Vector Data」來啟用這個功能。
啟用保存向量資料的功能
4.15 你的作業:建立新UI
ch4 作業:建立新UI
完整程式碼如下:
空
// // ContentView.swift // StackViewDemo // // Created by 楊于葳 on 2023/4/15. // import SwiftUI struct ContentView: View { @Environment(\.verticalSizeClass) var verticalSizeClass var body: some View { VStack(spacing:20) { VStack{ Text("Instant Developer") .fontWeight(.medium) .font(.system(size: 40)) .foregroundColor(.white) Text("Get help from experts in 15 minutes") .foregroundColor(.white) } HStack(alignment: .bottom, spacing: 10){ Image("student") .resizable() .scaledToFit() Image("tutor") .resizable() .scaledToFit() } .padding(.horizontal, 50) Text("Need help with coding parblems? Register!") .foregroundColor(.white) Spacer() if verticalSizeClass == .compact { HSignUpButtonGroup() } else { VSignUpButtonGroup() } } .background{ Image("background") .resizable() .ignoresSafeArea() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro")) .previewDisplayName("iphone 14 Pro") .previewInterfaceOrientation(.landscapeRight) ContentView() .previewDevice(PreviewDevice(rawValue: "iphone 14 Pro Max")) .previewDisplayName("iphone 14 Pro Max") ContentView() .previewDevice(PreviewDevice(rawValue: "iPad Air (5th generation)")) .previewDisplayName("iPad Air") } } struct VSignUpButtonGroup: View { var body: some View { VStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } } struct HSignUpButtonGroup: View { var body: some View { HStack { Button { } label: { Text("Sign Up") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.indigo) .cornerRadius(10) Button { } label: { Text("Log In") } .frame(width:200) .padding() .foregroundColor(.white) .background(Color.gray) .cornerRadius(10) } } }
一開始沒有參考提示的架構的時候,我所想到的方法,是使用ZStack的方式完成,沒想到也可以直接用 background 的方式設定。
4.16 本章小結
終於完成第四章所有的小節了!在這一章學會如何使用「堆疊視圖」和「尺寸類別」來建立自適應 UI,這可以說是本書最多節次的一個章節了。我發現新版本的書,在作業的要求上,完成難度似乎沒有舊版本還來得高,而且作業內容感覺更有趣了!