從前一章中,我相信你現在應該了解如何使用堆疊建立一個複雜的UI。當然,在你能夠熟練 SwiftUI 的運用之前,需要許多的練習才行。因此,在我們深入研究 ScrollView, 學習如何讓視圖滾動之前,我們先進行一個挑戰,來作為本章的開始。你的任務是建立一個卡片視圖(card view ),如圖5.1 所示。
透過使用堆疊、圖片與文字視圖,你應該能夠建立 UI。雖然我會逐步示範如何實作, 但請先花一些時間來思考如何完成這個任務,以及找出自己的解決方案。
當你完成卡片視圖的建立之後,我將與你討論 ScrollView
,並使用卡片視圖建立一個可滾動的介面,圖 5.2 即是完成後的 UI。
如果你還沒有開啟 Xcode,請開啟它並使用 App 模板 (於 iOS 下)。來建立一個新專案。在下一個螢幕畫面中,設定專案名稱為「SwiftUIScrollView」(或是任何你喜歡的名稱),並填入所需要的值。請確認已選取「Interface」選項中的「SwiftUI」。
到目前為止,我們在 ContentView.swift
檔中撰寫使用者介面的程式碼,程式碼撰寫在這裡完全沒有任何問題,不過我要介紹一個整理程式碼的較佳方式。為了實作卡片視圖, 我們另外為它建立一個單獨檔案,在專案導覽器中,於 SwiftUIScrollView
按右鍵,並選擇「New File...」,如圖 5.3 所示。
如圖 5.4 所示,在「User Interface」區塊,選取「SwiftUI View」模板,然後點選「Next」來建立檔案。將檔案名稱命名為 CardView
,並將其儲存在專案資料夾中。
CardView.swift
中的程式碼與 ContentView.swift
中的程式碼很相似。同樣的,你可以在畫布中預覽 UI,如圖5.5 所示。
現在,我們準備要撰寫卡片視圖的程式碼。但是,首先你需要準備圖檔,並將其匯入素材目錄。如果你不想要準備自己的圖片,則可以至 https://www.appcoda.com/resources/swiftui/SwiftUIScrollViewImages.zip 下載範例圖片檔,將圖片檔解壓縮後,選取 Assets
,並所有圖片其拖曳至素材目錄。
現在切回 CardView.swift
檔。若你再看一下圖 5.1,這個卡片視圖是由兩個部分組成, 視圖上部是圖片,而視圖下部是文字敘述。
讓我們從圖片開始。我將使圖片可調整大小,以縮放來填滿螢幕,同時保持長寬比。你可以撰寫程式碼如下:
struct CardView: View {
var body: some View {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
如果你忘記了這兩個修飾器是什麼,請返回並閱讀有關 Image
物件的章節。接下來,我們來實作文字敘述部分,你可以撰寫程式碼如下:
VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
顯然的,你需要使用 Text
來建立文字視圖。由於我們在敘述中實際上有三個垂直排列的文字視圖,因此我們使用一個 VStack
來嵌入它們。對於 VStack
,我們指定對齊方式為 .leading
,這會將文字視圖對齊堆疊視圖的左側。
這些文字的修飾器皆在有關 Text
物件的章節討論過。如果你對任何修飾器有疑問的話,可以回去參考。但是,這裡會特別提到有關 .primary
與 .secondary
顏色。
雖然你可在 foregroundColor
修飾器指定標準顏色,像是 .black
與 .purple
,但 iOS 提供一套系統顏色,其中包含主色( primary color )、輔色( secondary color )、第三級色( tertiary color )等變化,透過使用此顏色變化,你的 App 可以輕鬆支援淺色模式與深色模式。舉例而言,文字視圖的主色預設設定為淺色模式的黑色。當 App 切換到深色模式, 主色將被調整為白色,這是由 iOS 自動調整,因此你無須另外編寫寫支援深色模式的程式碼,我們將在後面的章節中深入探討深色模式。
為了將圖片與這些文字視圖垂直排列,我們使用 VStack
來嵌入它們,目前的佈局如圖 5.7 所示。
還沒有完成,尚有幾件事情需要實作。首先,如果文字敘述區塊要與圖片的邊緣對齊,該如何做呢?
依照我們所學,我們可以在一個 HStack
嵌入文字視圖的 VStack
,然後我們將使用一個留白( Spacer
)來將VStack
往左推,我們來看看是否可行。
如果你已經變更程式碼,如圖 5.8 所示,這個文字視圖的VStack 會對齊螢幕的左側。
最好是在 HStack
周圍加入一些間距( padding )。插入 padding
修飾器如下,如圖 5.9 所示 :
最後是邊框部分。我們在前面的章節中討論過如何繪製圓角邊框。我們可以使用 overlay
修飾器,並使用RoundedRectangle
來畫出邊框。以下是完整的程式碼:
struct CardView: View {
var body: some View {
VStack {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/255, opacity: 0.1), lineWidth: 1)
)
.padding([.top, .horizontal])
}
}
除了邊框之外,我們也在頂部、左側、右側分別加入了一些間距。現在,你應該已經建立好卡片視圖的佈局,如圖 5.10 所示。
雖然目前卡片視圖看起來沒問題,但我們將圖片與文字寫死( Hard Code)在程式中, 為了讓它更具彈性,我們要重構程式碼。首先,在 CardView
宣告 image、category、heading 與author 這些變數:
var image: String
var category: String
var heading: String
var author: String
接下來,將 Image
與 Text
視圖的值以下列變數替代:
VStack {
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading) {
Text(category)
.font(.headline)
.foregroundColor(.secondary)
Text(heading)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by \(author)".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
更改完成後,你將在 #Preview
中看到一個錯誤,如圖 5.11 所示。這是因為我們在 CardView
導入了一些變數,當使用它時,必須指定參數給它。
因此,以下列程式碼來取代:
#Preview {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
}
錯誤將可被修正,現在你已經建立了一個能接受不同圖片及文字的彈性 CardView
。
再看一下圖5.2,這就是我們要實作的使用者介面。首先,你可能認為我們可以使用一個 VStack
來嵌入四個卡片視圖。你可以切換到 ContentView.swift
,於 body 內插入以下的程式:
VStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
}
如果你這樣做的話,這些卡片視圖將被擠壓,以填滿螢幕,因為 VStack
是不可滾動的,如圖 5.12 所示。
要讓內容可以滾動,SwiftUI 提供一個名為 ScrollView
的視圖。當內容嵌入在一個 ScrollView
時,它變得可以滾動,因此你需要做的是在一個 ScrollView
內加入一個 VStack
,以使視圖可以滾動。在預覽畫布中,你可以拖曳這些視圖來滾動內容。
你的任務是加入標題( header )至目前的滾動視圖( scroll view )中,結果如圖 5.14 所示。如果你完全暸解了 Vstack
與 Hstack
,你應該有能力建立這個佈局。
預設上,ScrollView
允許你以垂直方向滾動內容。另外,它還支援水平方向的可滾動內容。我們來了解如何進行一些修改,以將目前的佈局轉換為輪播(carousel )UI。
更新 ContentView
如下:
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
// 作業#1的程式碼
HStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
.frame(width: 300)
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
.frame(width: 300)
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
.frame(width: 300)
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
.frame(width: 300)
}
}
}
}
我們在上列的程式碼中做了三個變更:
.horizontal
值,以在 ScrollView
中使用一個水平滾動視圖。VStack
變更為 HStack
。變更程式碼之後,你將看到卡片視圖以水平排列且可以滾動,如圖 5.15 所示。
在滾動視圖時,螢幕底部附近有一個滾動指示器。這個指示器預設是顯示的。如果你想要隱藏它,你可以將 ScrollView
的程式碼變更如下:
ScrollView(.horizontal, showsIndicators: false)
透過指定 showIndicators
為 false
,iOS 將不再顯示該指示器。
如果你再次閱讀一下程式碼,所有的 CardViews
是以 .frame
修飾器來限制其寬度為 300 點,是否有其他簡化的方式,並移除重複的程式碼呢?SwiftUI 框架提供了開發者群組視圖(Group
view)的功能,可以將相關內容群組起來。更重要的是,你可以將修飾器加至群組,所有嵌入群組內的視圖皆能夠同步做產生效果。
舉例而言,你可以將 HStack
內的程式重寫如下來完成同樣的結果:
HStack {
Group {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
}
.frame(width: 300)
}
如圖 5.15 所示,第一張卡片的標題被截斷了,該如何修正這個問題呢? SwiftUI 中可以使用 .minimumScaleFactor
修飾器來自動縮小文字。你可以切換至 CardView.swift,並於 Text(標題)
加上以下這個修飾器:
.minimumScaleFactor(0.5)
SwiftUI 會自動縮小文字來相容可用的空間。這邊的值設定了視圖所允許的最小縮放量。以這個例子來看,SwiftUI 能夠將文字盡量縮至原來大小的 50%。
最後有一個作業,修改目前的程式碼,並如圖 5.16 所示來重新排列。請注意,當使用者滾動卡片視圖時,用戶應該可以看到標題和日期。
在本章所準備的範例檔中,有完整的專案與作業解答可以下載: