精通 SwiftUI - iOS 17 版

第 5 章
了解滾動視圖與建立輪播 UI

從前一章中,我相信你現在應該了解如何使用堆疊建立一個複雜的UI。當然,在你能夠熟練 SwiftUI 的運用之前,需要許多的練習才行。因此,在我們深入研究 ScrollView, 學習如何讓視圖滾動之前,我們先進行一個挑戰,來作為本章的開始。你的任務是建立一個卡片視圖(card view ),如圖5.1 所示。

圖 5.1. 卡片視圖
圖 5.1. 卡片視圖

透過使用堆疊、圖片與文字視圖,你應該能夠建立 UI。雖然我會逐步示範如何實作, 但請先花一些時間來思考如何完成這個任務,以及找出自己的解決方案。

當你完成卡片視圖的建立之後,我將與你討論 ScrollView,並使用卡片視圖建立一個可滾動的介面,圖 5.2 即是完成後的 UI。

圖 5.2. 使用 ScrollView 建立一個滾動式UI
圖 5.2. 使用 ScrollView 建立一個滾動式UI

建立一個卡片式 UI

如果你還沒有開啟 Xcode,請開啟它並使用 App 模板 (於 iOS 下)。來建立一個新專案。在下一個螢幕畫面中,設定專案名稱為「SwiftUIScrollView」(或是任何你喜歡的名稱),並填入所需要的值。請確認已選取「Interface」選項中的「SwiftUI」。

到目前為止,我們在 ContentView.swift 檔中撰寫使用者介面的程式碼,程式碼撰寫在這裡完全沒有任何問題,不過我要介紹一個整理程式碼的較佳方式。為了實作卡片視圖, 我們另外為它建立一個單獨檔案,在專案導覽器中,於 SwiftUIScrollView 按右鍵,並選擇「New File...」,如圖 5.3 所示。

圖 5.3. 建立一個新檔案
圖 5.3. 建立一個新檔案

如圖 5.4 所示,在「User Interface」區塊,選取「SwiftUI View」模板,然後點選「Next」來建立檔案。將檔案名稱命名為 CardView,並將其儲存在專案資料夾中。

圖 5.4 選取 SwiftUI View 模板
圖 5.4 選取 SwiftUI View 模板

CardView.swift 中的程式碼與 ContentView.swift 中的程式碼很相似。同樣的,你可以在畫布中預覽 UI,如圖5.5 所示。

圖 5.5. 就像ContentView.swift 一樣,你可以在畫布中預覽CardView.swift
圖 5.5. 就像ContentView.swift 一樣,你可以在畫布中預覽CardView.swift

準備圖片檔

現在,我們準備要撰寫卡片視圖的程式碼。但是,首先你需要準備圖檔,並將其匯入素材目錄。如果你不想要準備自己的圖片,則可以至 https://www.appcoda.com/resources/swiftui/SwiftUIScrollViewImages.zip 下載範例圖片檔,將圖片檔解壓縮後,選取 Assets,並所有圖片其拖曳至素材目錄。

圖 5.6 將圖片檔加入素材目錄
圖 5.6 將圖片檔加入素材目錄

實作卡片視圖

現在切回 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 所示。

圖 5.7. 將圖片與文字視圖嵌入到 VStack 中
圖 5.7. 將圖片與文字視圖嵌入到 VStack 中

還沒有完成,尚有幾件事情需要實作。首先,如果文字敘述區塊要與圖片的邊緣對齊,該如何做呢?

依照我們所學,我們可以在一個 HStack 嵌入文字視圖的 VStack,然後我們將使用一個留白( Spacer )來將VStack 往左推,我們來看看是否可行。

如果你已經變更程式碼,如圖 5.8 所示,這個文字視圖的VStack 會對齊螢幕的左側。

圖 5.8. 文字敘述的對齊
圖 5.8. 文字敘述的對齊

最好是在 HStack 周圍加入一些間距( padding )。插入 padding 修飾器如下,如圖 5.9 所示 :

圖 5.9. 加入一些文字敘述的間距
圖 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 所示。

圖 5.10. 加入邊框與圓角
圖 5.10. 加入邊框與圓角

讓卡片視圖更具彈性

雖然目前卡片視圖看起來沒問題,但我們將圖片與文字寫死( Hard Code)在程式中, 為了讓它更具彈性,我們要重構程式碼。首先,在 CardView 宣告 image、category、heading 與author 這些變數:

var image: String
var category: String
var heading: String
var author: String

接下來,將 ImageText 視圖的值以下列變數替代:

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 導入了一些變數,當使用它時,必須指定參數給它。

圖 5.11. 呼叫 CardView 時缺少參數
圖 5.11. 呼叫 CardView 時缺少參數

因此,以下列程式碼來取代:

#Preview {
    CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
}

錯誤將可被修正,現在你已經建立了一個能接受不同圖片及文字的彈性 CardView

ScrollView 的介紹

再看一下圖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 所示。

圖 5.12 在一個VStack 中嵌入卡片視圖
圖 5.12 在一個VStack 中嵌入卡片視圖

要讓內容可以滾動,SwiftUI 提供一個名為 ScrollView 的視圖。當內容嵌入在一個 ScrollView 時,它變得可以滾動,因此你需要做的是在一個 ScrollView 內加入一個 VStack,以使視圖可以滾動。在預覽畫布中,你可以拖曳這些視圖來滾動內容。

圖 5.13. 使用 ScrollView
圖 5.13. 使用 ScrollView

作業 #1

你的任務是加入標題( header )至目前的滾動視圖( scroll view )中,結果如圖 5.14 所示。如果你完全暸解了 VstackHstack,你應該有能力建立這個佈局。

圖 5.14. 作業#1
圖 5.14. 作業#1

使用水平 ScrollView 建立輪播式 UI

預設上,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)
            }
        }

    }
}

我們在上列的程式碼中做了三個變更:

  1. 我們傳送一個 .horizontal 值,以在 ScrollView 中使用一個水平滾動視圖。
  2. 由於我們使用一個水平滾動視圖,因此我們還需要將堆疊視圖從 VStack 變更為 HStack
  3. 對於每個卡片視圖,我們將框架的寬度設定為「300點」。這是必要的,因為要顯示的圖片太寬

變更程式碼之後,你將看到卡片視圖以水平排列且可以滾動,如圖 5.15 所示。

圖 5.15 輪播 UI
圖 5.15 輪播 UI

隱藏滾動指示器

在滾動視圖時,螢幕底部附近有一個滾動指示器。這個指示器預設是顯示的。如果你想要隱藏它,你可以將 ScrollView 的程式碼變更如下:

ScrollView(.horizontal, showsIndicators: false)

透過指定 showIndicatorsfalse,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%。

作業 #2

最後有一個作業,修改目前的程式碼,並如圖 5.16 所示來重新排列。請注意,當使用者滾動卡片視圖時,用戶應該可以看到標題和日期。

圖 5.16. 視圖靠上對齊
圖 5.16. 視圖靠上對齊

在本章所準備的範例檔中,有完整的專案與作業解答可以下載: