iOS 17 App 程式設計實戰心法(SwiftUI)

第 12 章
改進細節視圖、自訂字型及導覽列

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 所示。

圖 12.1. 下載喜愛的 Google 字型
圖 12.1. 下載喜愛的 Google 字型

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

圖 12.2.  將字型檔加到 Xcode 專案
圖 12.2.  將字型檔加到 Xcode 專案
Note: 是否強制建立一個子群組呢?沒有強制,這只是我組織資源檔的作法。

當你拖曳檔案到 Fonts 群組後,你會看到如圖 12.3 所示的對話框,確認勾選「Copy items if needed」選項以及「FoodPin」目標。

圖 12.3. 勾選「Copy item if needed」與目標
圖 12.3. 勾選「Copy item if needed」與目標

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

圖 12.4. 在 Target Membership 下勾選「FoodPin」選項
圖 12.4. 在 Target Membership 下勾選「FoodPin」選項

最後,我們需要在 Info.plist 檔中新增一個名為「Fonts provided by application」的新鍵。Info.plist 是 Xcode 專案的設定檔,為了使用自訂字型檔,你必須在設定中註冊它們。

預設上,Xcode 15 不會在專案導覽器中顯示Info.plist 檔,你必須點選「FoodPin」專案, 並選擇「FoodPin」目標,然後選取「Info」頁籤來顯示自訂的 iOS 目標屬性,如圖 12.5 所示。

圖 12.5.  自訂 iOS 目標屬性
圖 12.5.  自訂 iOS 目標屬性

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

圖 12.6. 註冊自訂字型檔
圖 12.6. 註冊自訂字型檔

以上是安裝自訂字型檔的過程。稍後,要使用自訂字型時,可以編寫程式碼如下:

.font(.custom("Nunito-Regular", size: 25))

若是想以動態型別使用自訂字型的話,可以編寫程式碼如下:

.font(.custom("Nunito-Regular", size: 35, relativeTo: .largeTitle))

.largeTitle 字型型別是從 35 點開始,並自動縮放字型。

改進細節視圖

你在上一章中所開發的細節視圖只顯示了餐廳的基本資訊,我們將顯示更多的資訊,例如:地址與電話號碼,並使UI 看起來更專業。請參考圖 12.7,UI 是否看起來好多了?

圖 12.7. 改進 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 來佈局元件,我將它分為三個部分:

  1. 特色圖片。
  2. 餐廳描述。
  3. 餐廳地址與電話。

特色圖片

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

圖 12.8. 顯示特色圖片
圖 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 所示。

圖 12.9. 加入心形圖片
圖 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 所示。

圖 12.10. 加入餐廳名稱與類型
圖 12.10. 加入餐廳名稱與類型

顯然的,我們必須使用 VStack 來垂直排列餐廳名稱與類型。同樣的,frame 修飾器是用來將 VStack 對齊左下角。

預設上,餐廳名稱與類型之間會出現相當大的間距,為了最小化這個間距,我們明確告知 VStack 將間距設定為「5 點」。

餐廳描述

要顯示餐廳描述,我們只需要使用帶有 .padding 修飾器的 Text 視圖。在根 VStack 視圖中插入下列的程式碼:

Text(restaurant.description)
    .padding()

你的預覽應該會顯示餐廳描述,如圖 12.11 所示。

圖 12.11. 顯示餐廳描述
圖 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 的預覽。

圖 12.12.  顯示餐廳描述
圖 12.12.  顯示餐廳描述

忽略安全區域

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

圖 12.13. 顯示餐廳描述
圖 12.13. 顯示餐廳描述

要解決這個問題,則將下列的修飾器加到 ScrollView:

.ignoresSafeArea()

.ignoresSafeArea 修飾器會告知 iOS 將細節視圖佈局在螢幕安全區域之外,如圖 12.14 所示。

圖 12.14.  使用 ignoresSafeArea
圖 12.14. 使用 ignoresSafeArea

當你變更完成後,特色圖片應該會一直被推到螢幕邊緣。為了讓細節視圖看起來更好,我們透過移除餐廳名稱來調整「返回」按鈕,如下所示:

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 所示。

圖 12.15. 預覽具有導覽列的細節視圖
圖 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 所示。

圖 12.16  自訂導覽列
圖 12.16 自訂導覽列

作業:修復錯誤

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

圖 12.17. 當餐廳被標記為最愛時顯示黃色心形圖片
圖 12.17. 當餐廳被標記為最愛時顯示黃色心形圖片

本章小結

太棒了 !你已經讀完了本章,我希望你喜歡本章的內容,並為你建立的 App 感到自豪。你已經成功開發一個精美的 App,儘管它可能不是非常複雜。你已經學會如何重新設計細節視圖來顯示更多的餐廳資訊,還探索如何運用自訂字型以及自訂導覽列標題。

本章介紹很多的內容,即使你已經等不及想要繼續下一章,我仍建議你要休息一下, 給自己一些時間來充分消化到目前為止介紹過的所有內容。花點時間放鬆一下,喝杯咖啡或你喜歡的飲料,給自己應有的休息。

在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考。 http://www.appcoda.com/resources/swift59/swiftui-foodpin-detail-view.zip