
If you're interested in the living heart of what you do, focus on building things rather than talking about them.
- Ryan Freitas, About.me
首次啟動 App 時,通常會包含一系列導覽畫面或教學,這些畫面引導使用者了解 App 的特色及功能。有些人認為對於導覽畫面的需求,就意味著 App 設計的失敗,但是就我個人而言,我發現大多數的導覽畫面都很有用,且不討厭它們,關鍵是要保持簡潔,避免冗長且無聊的教學,我不會爭論你是否應該在 App 中包含導覽畫面,我只是想向你展示如何做出這個功能。
App 開發者利用導覽畫面,不僅可以展示 App 的功能,還可以引導使用者完成初始設定過程,例如:啟動通知與選擇顏色主題,導覽畫面的範例如圖 20.1 所示。

在本章中,我們將討論如何使用 TabView 來建立導覽畫面。當我提到標籤視圖,你可能會立即想到一個帶有標籤列的 App,不過使用 SwiftUI,TabView 可用來顯示一個具有多個標籤的介面,並且提供的不僅是一個標準的標籤介面,透過更改其樣式,你可以輕鬆將標籤視圖轉換為頁面滾動視圖(Paged Scrolling View )。
我們開始吧 !
我們瀏覽一下導覽畫面,App 一共顯示三個導覽畫面,使用者可透過在螢幕上滑動或點擊「Next」按鈕來在頁面之間導覽。
在最後的導覽畫面中,它顯示一個「Get Started」按鈕,當使用者點擊該按鈕,導覽畫面將會關閉,並且不再顯示。此外,使用者隨時可點擊「Skip」按鈕來略過導覽畫面,圖 20.2 是導覽畫面的螢幕截圖。

要建立導覽畫面,你需要先準備好圖片。首先下載本章所準備的圖片集( http://www.appcoda.com/resources/swift53/onboarding.zip ),然後匯入所有圖片( .svg )至素材目錄,請確保你有為每張圖片啟用「Preserve Vector Data」選項,如圖 20.3 所示。

和往常一樣,我們將為導覽畫面建立一個單獨視圖。在專案導覽器中的「View」資料夾上按右鍵,並選擇「New File...」,然後選取「SwiftUI View」模板,將檔案命名為「TutorialView.swift」。
對於導引視圖(Tutorial View )的每個頁面,佈局都非常相似,因此我們建立一個名為「TutorialPage」的子視圖,其顯示特色圖片、標題與子標題。在 TutorialView.swift 檔案中,插入下列的程式碼片段:
struct TutorialPage: View {
let image: String
let heading: String
let subHeading: String
var body: some View {
VStack(spacing: 70) {
Image(image)
.resizable()
.scaledToFit()
VStack(spacing: 10) {
Text(heading)
.font(.headline)
Text(subHeading)
.font(.body)
.foregroundStyle(.gray)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 40)
Spacer()
}
.padding(.top)
}
}
這段程式碼非常簡單,我們使用 VStack 視圖來排列圖片、標題與子標題。我們使用 Spacer() 將元件與螢幕頂部對齊。
要預覽 TutorialPage 視圖,則插入下列的預覽程式碼:
#Preview("TutorialPage", traits: .sizeThatFitsLayout) {
TutorialPage(image: "onboarding-1", heading: "CREATE YOUR OWN FOOD GUIDE", subHeading: "Pin your favorite restaurants and create your own food guide")
}
透過將一些測試資料傳送給 TutorialPage 視圖,Xcode 應該能渲染該預覽,如圖 20.4 所示。

現在已經建立了 TutorialPage 視圖,我們可以開始建立頁面導覽視圖。在 TutorialView 結構中,宣告以下的變數來存放標題、子標題與圖片:
let pageHeadings = [ "CREATE YOUR OWN FOOD GUIDE", "SHOW YOU THE LOCATION", "DISCOVER GREAT RESTAURANTS" ]
let pageSubHeadings = [ "Pin your favorite restaurants and create your own food guide",
"Search and locate your favorite restaurant on Maps",
"Find restaurants shared by your friends and other foodies"
]
let pageImages = [ "onboarding-1", "onboarding-2", "onboarding-3" ]
要使用 TabView 來建立頁面滾動視圖,則可使用下列的程式碼片段更新 body 變數:
TabView {
ForEach(pageHeadings.indices, id: \.self) { index in
TutorialPage(image: pageImages[index], heading: pageHeadings[index], subHeading: pageSubHeadings[index])
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
在 TabView 中,我們使用 TutorialPage 來顯示導覽畫面的每個頁面。.tag 修飾器為每個頁面提供唯一的索引。要將標準標籤視圖轉換為頁面滾動視圖,你只需要將標籤視圖的樣式設定為「.page」。
indexViewStyle 修飾器用來指定頁面指示器的樣式。在本例中,我們設定其值為 .page(backgroundDisplayMode: .always),以確保標籤視圖始終顯示頁面圓點。
現在你可以在預覽窗格中測試這個 App,你可以向左滑動或向右滑動,以在不同頁面之間瀏覽,如圖 20.5 所示。

SwiftUI 並沒有提供任何的修飾器來設定圓點的顏色,我們必須依賴 UIKit API。在 TutorialView 中,如果你想要變更頁面指示器的顏色,則新增 init() 方法:
init() {
UIPageControl.appearance().currentPageIndicatorTintColor = .systemIndigo
}
我們設定現行圓點的顏色為「.systemIndigo」,如果你再次測試 App,你應該會看到頁面指示器,如圖 20.6 所示。

導引視圖中還缺少了一些元素,即「Next」、「Skip」與「Get Started」按鈕。當點擊「Next」按鈕時,App 應該會導覽至導覽畫面的下一頁,但是我們如何編寫程式碼來使用TabView 進行頁面間的切換呢?
訣竅是透過綁定到目前的頁面索引來初始化 TabView,如此標籤視圖將會監看目前頁面索引的任何變化,並自動滾動至指定的頁面索引。
我們來看如何實作。首先,宣告一個狀態變數來追蹤目前的頁面索引:
@State private var currentPage = 0
我們設定目前的頁面索引為「0」(即導覽畫面的第一頁)。接下來,宣告下列的變數來從環境中取得 .dismiss:
@Environment(\.dismiss) var dismiss
稍後,我們將使用它來關閉導引視圖。要在頁面指示器的下方新增按鈕,我們使用 VStack 包裹 TabView,更新 body 變數如下:
VStack {
TabView(selection: $currentPage) {
ForEach(pageHeadings.indices, id: \.self) { index in
TutorialPage(image: pageImages[index], heading: pageHeadings[index], subHeading: pageSubHeadings[index])
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
.animation(.default, value: currentPage)
VStack(spacing: 20) {
Button(action: {
if currentPage < pageHeadings.count - 1 {
currentPage += 1
} else {
dismiss()
}
}) {
Text(currentPage == pageHeadings.count - 1 ? "GET STARTED" : "NEXT")
.font(.headline)
.foregroundStyle(.white)
.padding()
.padding(.horizontal, 50)
.background(Color(.systemIndigo))
.cornerRadius(25)
}
if currentPage < pageHeadings.count - 1 {
Button(action: {
dismiss()
}) {
Text("Skip")
.font(.headline)
.foregroundStyle(Color(.darkGray))
}
}
}
.padding(.bottom)
}
我們對上列的程式碼做了一些更改:
在預覽中執行 App 來快速測試一下,你現在可以使用滑動手勢與「Next」按鈕來在導引視圖之間導覽。

如前所述,導引視圖應該要在使用者首次啟動 App 時出現,因此我們需要對 Restaurant ListView 進行一些修改,我們將使用 sheet 修飾器來顯示導引視圖。
切換到 RestaurantListView.swift,並宣告一個狀態變數:
@State private var showWalkthrough = true
這個狀態變數指示是否應出現導引視圖。接下來,我們加入另一個 sheet 修飾器到導引堆疊:
.sheet(isPresented: $showWalkthrough) {
TutorialView()
}
現在我們在模擬器上執行 App 來快速測試。當 App 啟動時,你應該會看到導引視圖, 如圖 20.8 所示。很酷,對吧?

現在導覽畫面已經開始運作了,但是每當啟動 App 時,它都會出現。理想的情況下, 導覽畫面或教學只應在使用者第一次啟動 App 時顯示,為此我們需要找到一個儲存狀態的方式,以指示使用者是否看過導覽畫面。
我們應在哪裡保存這個狀態呢?
你已經學過 SwiftData 了,因此你可能希望將這個狀態儲存在本地資料庫中,雖然這是一個選項,但是還有一種更簡單的方式可儲存應用程式與使用者設定。
iOS SDK 提供 UserDefaults 類別來管理使用者的預設資料庫讓你持久性儲存鍵值對。藉由 SwiftUI 框架,開發者可以使用 @AppStorage 屬性包裹器,來輕鬆對預設資料庫讀取與寫入值,從而簡化了流程。
要使用 @AppStorage,可以編寫程式碼如下:
@AppStorage("hasViewedWalkthrough") var hasViewedWalkthrough: Bool = false
這會在使用者的預設資料庫中建立一個新實體,鍵設定為「hasViewedWalkthrough」, 值設定為「false」。SwiftUI 將會持續監看 hasViewedWalkthrough 的值,並相應更新 UI, 而且當我們更新其值時,更新後的值也會寫入使用者的預設資料庫中。
現在將上列的程式碼插入 RestaurantListView 中,根據 hasViewedWalkthrough 的值, App 會決定是否啟動導引視圖。將 onAppear 修飾器加到導覽堆疊:
.onAppear() {
showWalkthrough = hasViewedWalkthrough ? false : true
}
當清單視圖出現時,我們檢查 hasViewedWalkthrough 的值來看是否應開啟導引視圖。
此外,將 showWalkthrough 的預設值更新為「false」,因為我們現在依賴 hasViewed Walkthrough 來決定 showWalkthrough 的值。
@State private var showWalkthrough = false
現在切換到 TutorialView 並宣告下列的變數:
@AppStorage("hasViewedWalkthrough") var hasViewedWalkthrough: Bool = false
在使用者閱讀完導覽畫面後,我們需要更新 hasViewedWalkthrough 的值為「true」,因此在「Next」按鈕的動作閉包中插入下列這行程式碼,以將狀態更新為「true」:
hasViewedWalkthrough = true
你可以將程式碼放在執行 dismiss() 之前。同一行程式碼也應該加到「Skip」按鈕的動作閉包中,如圖 20.9 所示。
是時候來進行測試了,在模擬器中執行這個 App,你應該會在啟動 App 時看到導覽畫面。

在本章中,我們介紹了 TabView 的基本知識,並示範了顯示頁面視圖的用法。我們也探討了 @AppStorage 屬性包裹器在讀取和寫入值到使用者的預設系統時的便利性。藉由 SwiftUI 的這些強大功能,現在你已經掌握為使用者首次啟用 App 時建立導覽或教學畫面的知識。
在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考:http://www.appcoda.com/resources/swift59/swiftui-foodpin-walkthrough.zip 。