
Failure is an option here. If things are not failing, you are not innovating enough.
– Elon Musk
在上一章中,我們介紹了評分畫面來供使用者對餐廳進行評分,而評分按鈕目前缺乏功能,所需的行為是當選擇評分時,評分視圖會自行關閉,並且所選評分顯示在細節視圖中,如圖 16.1 所示。在本章中,我們將深入探討這個功能的實作細節。

最重要的是, 我將會簡要介紹一下 Combine, 它是與 SwiftUI 一起導入的框架。Combine 可以讓你輕鬆監看單一物件並取得其變更通知。當與 SwiftUI 結合時,我們無須編寫任何的程式碼,便可觸發視圖的更新,SwiftUI 與Combine 無縫地協同工,處理幕後的一切。
現在我們來看如何處理評分的選擇。如果你開啟 Restaurant.swift, 你會注意到 Restaurant 結構目前沒有用於儲存使用者評分的屬性,我們可以為 Restaurant 結構新增一個名為「rating」的新屬性,如下所示:
var rating: Rating?
rating 變數被定義為可選型別,這是因為使用者可能不會為餐廳評分。隨著這個變化, 我們還需要更新 init 方法如下:
init(name: String, type: String, location: String, phone: String, description: String, image: String, isFavorite: Bool = false, rating: Rating? = nil) {
self.name = name
self.type = type
self.location = location
self.phone = phone
self.description = description
self.image = image
self.isFavorite = isFavorite
self.rating = rating
}
現在我們回到 ReviewView.swift。要偵測使用者的選擇,我們可以將 .onTapGesture 修飾器加到 HStack 視圖,並將其放在 .animation 修飾器之後:
.onTapGesture {
self.restaurant.rating = rating
self.isDisplayed = false
}
糟糕 !Xcode 立即提示錯誤訊息給我們,如圖 16.2 所示。

錯誤的原因在於 restaurant 變數是不可變的,這表示我們不允許直接更新更新其值,那麼我們該如何更新restaurant 的 rating 屬性呢?
我們應該用 @State 標註 restaurant 變數?雖然這可以解訣錯誤,並讓我們更新評分,但是這個變更只在ReviewView 中可見,我們如何將評分的變化通知細節視圖,以便它可以顯示使用者選擇的評分呢?
Combine 框架有一個名為「ObservableObject」的協定。透過採用這個協定,每其屬性值發生變化時,物件本身就可以通知其他視圖。
要使用 ObservableObject,我們需要對 Restaurant 結構進行一些變更。我們必須宣告 Restaurant 為類別而不是結構,才能採用 ObservableObject。
現在開啟 Restaurant.swift,並將內容替換如下:
import Combine
class Restaurant: ObservableObject {
enum Rating: String, CaseIterable {
case awesome
case good
case okay
case bad
case terrible
var image: String {
switch self {
case .awesome: return "love"
case .good: return "cool"
case .okay: return "happy"
case .bad: return "sad"
case .terrible: return "angry"
}
}
}
@Published var name: String
@Published var type: String
@Published var location: String
@Published var phone: String
@Published var description: String
@Published var image: String
@Published var isFavorite: Bool = false
@Published var rating: Rating?
init(name: String, type: String, location: String, phone: String, description: String, image: String, isFavorite: Bool = false, rating: Rating? = nil) {
self.name = name
self.type = type
self.location = location
self.phone = phone
self.description = description
self.image = image
self.isFavorite = isFavorite
self.rating = rating
}
}
如果你將修改後的程式碼和原來的 Restaurant 結構做比較,它們看起來非常相似, Rating 列舉與 init 方法保持不變,唯一的差異是我們宣告 Restaurant 為一個類別,並採用 ObservableObject 協定。此外,所有的屬性都標註 @Published。
@Published 是一個和 ObservableObject 一起使用的屬性包裹器, 當屬性標記為 @Publisher 時,它表示發布者(在本例中為 Restaurant 類別)應該在該屬性值發生變更時通知所有訂閱者(即視圖)。
Restaurant 類別與原來的結構非常一致,因此你無須對其他程式碼進行任何的變更。最重要的是,Xcode 不再對ReviewView 中加入的程式碼提出任何異議了:
.onTapGesture {
self.restaurant.rating = rating
self.isDisplayed = false
}
點擊「Play」按鈕,並在模擬器上測試 App,你應該能執行它,而不會出現任何錯誤。
現在切換到 RestaurantDetailView.swift,我們必須更新細節視圖,以顯示所選的評分。找到顯示餐廳名稱與類型的程式碼:
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(.black)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .bottomLeading)
.foregroundStyle(.white)
.padding()
要顯示評分圖示,則用 HStack 視圖包裹 VStack,然後我們使用 Image 視圖來渲染評分。完整的程式碼如下所示:
HStack(alignment: .bottom) {
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(.black)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .bottomLeading)
.foregroundColor(.white)
.padding()
if let rating = restaurant.rating, !showReview {
Image(rating.image)
.resizable()
.frame(width: 60, height: 60)
.padding([.bottom, .trailing])
.transition(.scale)
}
}
.animation(.spring(response: 0.2, dampingFraction: 0.3, blendDuration: 0.3), value: restaurant.rating)
在顯示圖片視圖之前,我們使用 if let 來驗證 restaurant.rating 是否有值。如果沒有評分, 我們將不會顯示圖片視圖。要修飾評分圖片的外觀,我們加入了 animation 修飾器並應用彈簧動畫。
這就是 ObservableObject 的強大之處。回想一下,Restaurant 的rating 屬性是用 @Published 標註,每當有任何值發生變化時,該物件都會通知所有相關的視圖。
在模擬器或預覽窗格上執行 App,當你在評分畫面中選擇評分後,細節視圖就會以漂亮的動畫來顯示相應的圖片,如圖 16.3 所示。

在本章中,我簡要介紹了 Combine 框架。在 ObservableObject 的幫助下,我們可以輕鬆監看物件值的變化。然而,Combine 框架還有更多值得探討的內容,如果你想要深入了解 Combine 框架的話,我建議你觀看下列的影片:
在本章所準備的範例檔中,有最後完整的Xcode 專案可供你下載參考: http://www.appcoda.com/resources/swift59/swiftui-foodpin-observableobject.zip 。