
To create something exceptional, your mindset must be relentlessly focused on the smallest detail.
- Giorgio Armani
目前的細節視圖可能看起來很基本,但是將其提升為如章名頁所示的視圖,不是很好嗎?在本章中,我們將進一步改進細節視圖,以顯示更多的餐廳資訊,另外你還將學到在 SwiftUI 中使用自訂字型的知識。
本章將會介紹很多的內容,你可能需要幾個小時來進行這個專案,我建議你擱置其他的事情,使自己全神貫注於此。如果你準備好了,我們繼續調整細節視圖來做出令人印象深刻的外觀。
首先下載本章所準備的 FoodPin 專案 http://www.appcoda.com/resources/swift59/swiftui-foodpin-detailview-starter.zip ,這個專案是以上一章所完成的內容為基礎,不過我修改了 Restaurant 結構來加入另外兩個屬性: phone 與 description。
struct Restaurant {
var name: String
var type: String
var location: String
var phone: String
var description: String
var image: String
var isFavorite: Bool
init(name: String, type: String, location: String, phone: String, description: String, image: String, isFavorite: Bool = false) {
self.name = name
self.type = type
self.location = location
self.phone = phone
self.description = description
self.image = image
self.isFavorite = isFavorite
}
init() {
self.init(name: "", type: "", location: "", phone: "", description: "", image: "", isFavorite: false)
}
}
最重要的是, 我已經更新了餐廳資料的完整地址及電話號碼, 詳情可參考 RestaurantListView.swift 檔:
@State var restaurants = [ Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: "cafedeadend", isFavorite: false),
Restaurant(name: "Homei", type: "Cafe", location: "Shop B, G/F, 22-24A Tai Ping San Street SOHO, Sheung Wan, Hong Kong", phone: "348-233423", description: "A little gem hidden at the corner of the street is nothing but fantastic! This place is warm and cozy. We open at 7 every morning except Sunday, and close at 9 PM. We offer a variety of coffee drinks and specialties including lattes, cappuccinos, teas, and more. We serve breakfast, lunch, and dinner in an airy open setting. Come over, have a coffee and enjoy a chit-chat with our baristas.", image: "homei", isFavorite: false),
...
]
由於我們為 Restaurant 結構加入了兩個新屬性,因此 #Preview 程式碼區塊也會更新。
#Preview {
RestaurantDetailView(restaurant: Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: "cafedeadend", isFavorite: true))
}
這就是起始專案的所有更改。在繼續閱讀下一節的內容之前,請花一些時間熟悉這些變更。
San Francisco 字型是在 2014 年 11 月推出,並成為 iOS App 中的預設字型。如果你在 Google Font ( https://fonts.google.com))中找到開源字型,並想要運用在你的 App 中, 則該怎麼做呢?
Xcode 讓開發者可非常輕鬆使用自訂字型,你只需要將自訂字型檔加到你的 Xcode 專案中即可。例如:你喜歡在App 中使用 Nunito 字型,則你可以訪問下列網址: https://fonts.google.com/specimen/Nunito,點選「Download Family」來下載字型檔,如圖 12.1 所示。

現在回到 Xcode, 並開啟起始專案( 如果還沒開啟的話), 在專案導覽器中的「FoodPin」資料夾上按右鍵,並選擇「New Group」,將群組命名為「Resources」。接下來,在「Resources」資料夾上按右鍵,並選擇「New Group」來新增一個子群組,然後為子群組命名為「Fonts」。選取「Nunito-Regular.ttf」與「Nunito-Bold.ttf」,並將它們加入 Fonts 群組;如果你想要使用所有的字型樣式,則將所有的字型檔加入群組中,如圖12.2 所示。

Note: 是否強制建立一個子群組呢?沒有強制,這只是我組織資源檔的作法。
當你拖曳檔案到 Fonts 群組後,你會看到如圖 12.3 所示的對話框,確認勾選「Copy items if needed」選項以及「FoodPin」目標。

當你按下「Finish」按鈕後,這些字型檔將出現在專案導覽器中,為了確保你的 App 可以使用這些字型檔,按住command 鍵並選取所有的字型檔,然後在檔案檢閱器(File Inspector )中確認已啟用 Target Membership 下的「FoodPin」選項,如果沒有的話,則請勾選這個選項,如圖 12.4 所示。

最後,我們需要在 Info.plist 檔中新增一個名為「Fonts provided by application」的新鍵。Info.plist 是 Xcode 專案的設定檔,為了使用自訂字型檔,你必須在設定中註冊它們。
預設上,Xcode 15 不會在專案導覽器中顯示Info.plist 檔,你必須點選「FoodPin」專案, 並選擇「FoodPin」目標,然後選取「Info」頁籤來顯示自訂的 iOS 目標屬性,如圖 12.5 所示。

接下來,將游標放在「Bundle name」上,然後你會看到一個「+」按鈕,點選它來加入一個新鍵,將鍵名設定為「Fonts provided by application」,並將 item 0 的值填寫為「Nunito- Bold.ttf」,然後點擊「+」按鈕來加入另一個項目,而 item 1 的值設定為「Nunito-Regular. ttf」,如圖 12.6 所示。

以上是安裝自訂字型檔的過程。稍後,要使用自訂字型時,可以編寫程式碼如下:
.font(.custom("Nunito-Regular", size: 25))
若是想以動態型別使用自訂字型的話,可以編寫程式碼如下:
.font(.custom("Nunito-Regular", size: 35, relativeTo: .largeTitle))
.largeTitle 字型型別是從 35 點開始,並自動縮放字型。
你在上一章中所開發的細節視圖只顯示了餐廳的基本資訊,我們將顯示更多的資訊,例如:地址與電話號碼,並使UI 看起來更專業。請參考圖 12.7,UI 是否看起來好多了?

現在切換到 RestaurantDetailView.swift,並更改程式碼。我們不再需要使用 ZStack 視圖,因此將其替換如下:
ScrollView {
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
}
}
}
我們使用滾動視圖來保存餐廳資訊,因為內容可能會超出螢幕高度。在 ScrollView 中, 我們將使用一個 VStack 來佈局元件,我將它分為三個部分:
在 ScrollView 中,我們使用 VStack 來排列 UI 元件。在 VStack 中,第一個元件是特色圖片,插入下列的程式碼來建立圖片視圖:
VStack(alignment: .leading) {
Image(restaurant.image)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 445)
}
這段程式碼非常簡單,我們建立一個 Image 視圖來載入餐廳圖片。為了縮放圖片,我們使用 scaledToFill 模式。frame 修飾器是用來控制圖片的大小,我們將高度限制為「445 點」,如圖 12.8 所示。

接下來,我們需要在圖片上疊加一些餐廳資訊。你可能知道,我們可以使用 overlay 修飾器來實作它,將 overlay 修飾器加到 Image 視圖,如下所示:
.overlay {
VStack {
Image(systemName: "heart")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topTrailing)
.padding()
.font(.system(size: 30))
.foregroundColor(.white)
.padding(.top, 40)
}
}
我們從心形圖片開始,它是 SF Symbols 提供的系統圖片。你應該非常熟悉 foregroundStyle、font 與 padding 等修飾器,棘手的部分是 frame 修飾器,它的作用是什麼呢?
有幾種方式可將心形圖片放置在視圖的右上角,這裡我們使用 frame 修飾器來處理對齊,透過將 alignment 的值設定為「.topTrailing」,我們便可以將心形圖片移到右上角, 如圖 12.9 所示。

特色圖片的部分尚未完成,我們還需要佈局餐廳名稱與類型。在心形圖片視圖的後面插入下列的程式碼:
VStack(alignment: .leading, spacing: 5) {
Text(restaurant.name)
.font(.custom("Nunito-Regular", size: 35, relativeTo: .largeTitle))
.bold()
Text(restaurant.type)
.font(.system(.headline, design: .rounded))
.padding(.all, 5)
.background(Color.black)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .bottomLeading)
.foregroundStyle(.white)
.padding()
如果你已正確加入程式碼了,則應該會在預覽中看到餐廳名稱與類型,如圖 12.10 所示。

顯然的,我們必須使用 VStack 來垂直排列餐廳名稱與類型。同樣的,frame 修飾器是用來將 VStack 對齊左下角。
預設上,餐廳名稱與類型之間會出現相當大的間距,為了最小化這個間距,我們明確告知 VStack 將間距設定為「5 點」。
要顯示餐廳描述,我們只需要使用帶有 .padding 修飾器的 Text 視圖。在根 VStack 視圖中插入下列的程式碼:
Text(restaurant.description)
.padding()
你的預覽應該會顯示餐廳描述,如圖 12.11 所示。

對於餐廳地址與電話,我們將使用 HStack 視圖來安排佈局。我們繼續在根 VStack 視圖中插入下列的程式碼:
HStack(alignment: .top) {
VStack(alignment: .leading) {
Text("ADDRESS")
.font(.system(.headline, design: .rounded))
Text(restaurant.location)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading) {
Text("PHONE")
.font(.system(.headline, design: .rounded))
Text(restaurant.phone)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal)
並排建立兩個視圖的訣竅是使用 HStack 視圖。為了讓兩個視圖的寬度相等,每個 VStack 視圖(HStack 視圖內)的框(frame )的寬度設定為「.infinity」。
如果你所做的變更正確,Xcode 應該會顯示類似圖 12.12 的預覽。

細節視圖看起來很棒,但是你是否試過執行 App 呢?當你從清單視圖導覽到細節視圖時,特色圖片將顯示在導覽列的正下方,而我們想要做的是在導覽列後面顯示圖片。

要解決這個問題,則將下列的修飾器加到 ScrollView:
.ignoresSafeArea()
.ignoresSafeArea 修飾器會告知 iOS 將細節視圖佈局在螢幕安全區域之外,如圖 12.14 所示。

當你變更完成後,特色圖片應該會一直被推到螢幕邊緣。為了讓細節視圖看起來更好,我們透過移除餐廳名稱來調整「返回」按鈕,如下所示:
Text("\(Image(systemName: "chevron.left"))")
如果你想在導覽視圖中預覽細節視圖,則可以編輯 RestaurantDetailView_Previews 結構如下:
#Preview {
NavigationStack {
RestaurantDetailView(restaurant: Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "G/F, 72 Po Hing Fong, Sheung Wan, Hong Kong", phone: "232-923423", description: "Searching for great breakfast eateries and coffee? This place is for you. We open at 6:30 every morning, and close at 9 PM. We offer espresso and espresso based drink, such as capuccino, cafe latte, piccolo and many more. Come over and enjoy a great meal.", image: "cafedeadend", isFavorite: false))
}
.tint(.white)
}
這會將細節視圖嵌入到導覽視圖中,使你無須執行 App 即可預覽其外觀及感覺,如圖 12.15 所示。

我們已經向你簡要展示了如何自訂導覽列,但是我還想對你進一步說明一些自訂功能。目前版本的 SwiftUI 版本仍然不支援原生的各種自訂功能,例如:要更改導覽列標題的字型顏色,我們需要恢復使用 UIKit。
我們來看看如何實作自訂功能。開啟 FoodPinApp.swift,並插入下列的新方法:
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.systemRed, .font: UIFont(name: "ArialRoundedMTBold", size: 35)!]
navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.systemRed, .font: UIFont(name: "ArialRoundedMTBold", size: 20)!]
navBarAppearance.backgroundColor = .clear
navBarAppearance.backgroundEffect = .none
navBarAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
UINavigationBar.appearance().compactAppearance = navBarAppearance
}
使用這個 init() 方法後,App 將在 App 啟動期間執行自訂的程式碼。為了自訂導覽列的字型與顏色, 我們建立 UINavigationBarAppearance 的實例, 並設定我們喜愛的字型及背景顏色。當我們設定好外觀物件後,我們將其指定給 UINavigation 的 standardAppearance、compactAppearance 與 scrollEdgeAppearance 屬性, 這是你可以在 SwiftUI 專案中自訂導覽列的方式。
執行 App 來快速測試一下,導覽列的標題應該改為紅色了,如圖 12.16 所示。

你是否在目前的 App 中發現錯誤呢?在清單視圖中,試著將餐廳標記為最愛,當你點擊該餐廳並導覽至細節視圖時,心形圖片不會變成黃色,而你的任務便是要修復這個錯誤,如圖 12.17 所示。

太棒了 !你已經讀完了本章,我希望你喜歡本章的內容,並為你建立的 App 感到自豪。你已經成功開發一個精美的 App,儘管它可能不是非常複雜。你已經學會如何重新設計細節視圖來顯示更多的餐廳資訊,還探索如何運用自訂字型以及自訂導覽列標題。
本章介紹很多的內容,即使你已經等不及想要繼續下一章,我仍建議你要休息一下, 給自己一些時間來充分消化到目前為止介紹過的所有內容。花點時間放鬆一下,喝杯咖啡或你喜歡的飲料,給自己應有的休息。
在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考。 http://www.appcoda.com/resources/swift59/swiftui-foodpin-detail-view.zip。