#10 @ObservedObject的使用

KuoJed
彼得潘的 Swift iOS App 開發教室
7 min readJul 11, 2020

--

在SwiftUI中, 若我們需要畫面能夠跟參數連動, 通常直覺會是使用@State這個屬性裝飾器(Property Wrapper). 但上網看了一下, 建議使用的時機會是單一畫面中的簡單型別, 像是如果我們希望每按一次按鈕, 畫面顯示的數字能夠加一的話, 我們可以透過下面這段程式碼來達成.

struct ContentView: View {

@State var count = 0

var body: some View {

VStack{

Text("\(count)")

Button(action: {count = count + 1} ) {
Text("update")
}
}

}
}

但是如果今天是比較複雜的型別(透過class來定義), 且與多個畫面有連動時, 這時候會建議使用@ObservedObject這個屬性裝飾器來實現. 舉個例子, 透過class來宣告一個天氣的物件, 能夠顯示目前溫度, 並透過一個Bool來判斷天氣是否炎熱.

class Weather {
var isHot = true
var temp = 30
}

為了能夠使用@ObservedObject這個屬性裝飾器, 我們在宣告型別時, 需要先遵從ObservableObject這個協議(protocol). 另外, 我們必須告訴SwiftUI, 哪些參數是我們有興趣的(內容變更時需要重新載入), 所以必須在這些參數前面加上@Published這個關鍵字.

class Weather: ObservableObject {
@Published var isHot = true
@Published var temp = 30
}

最後, 在產生物件的時候, 就可以在變數前面加上@ObservedObject這個屬性裝飾器. 範例程式如下, 使用者可以透過按鈕來增加/降低溫度, 當溫度超過30度時, 則顯示天氣好熱, 低於30則顯示天氣舒適.

class Weather: ObservableObject {
@Published var isHot = true
@Published var temp = 30
}
struct ContentView: View {

@ObservedObject var weather = Weather()

var body: some View {

VStack (spacing: 30){

Text("現在溫度: \(weather.temp)")
Text(weather.isHot == true ? "天氣好熱" : "天氣舒適")

Button(action: {
weather.temp = weather.temp + 1
if weather.temp >= 30 {
weather.isHot = true
}

}) {
Text("升溫")
}

Button(action: {
weather.temp = weather.temp - 1
if weather.temp < 30 {
weather.isHot = false
}

}) {
Text("降溫")
}

}


}
}

@ObservedObject vs. @StateObject

相信透過上面這個簡單的例子, 你已經了解如何使用@ObservedObject這個屬性裝飾器. 但這邊還有一件重要的事要告訴你, 在iOS14又推出了一個新的屬性裝飾器(@StateObject)來提供更好的方式來實現這個功能.

疑!? 你心中或許會有個疑問, @ObservedObject有什麼問題嗎? 為什麼會需要另一個屬性裝飾器呢? 其實最大的問題就是透過@ObservedObject所產生的物件的生命週期無法被當前的View所管理, 在某些情況下可能會有不預期的問題發生. 或許聽起來很抽象, 但我們可以透過下面這個例子來看看會發生什麼問題.

@ObservedObject和@StateObject的使用方法一樣, 只有屬性裝飾器的關鍵字不同, 所以我們這邊先簡單的宣告一個型別為People的物件, 裡面僅只有簡單的屬性age.

class People: ObservableObject {
@Published var age = 0
}

我們分別使用@ObservedObject和@StateObject的方法來產生一個畫面, 當使用者按下按鍵後, people這個物件裡面的age數字會增加1. 從下面的程式碼中可以看出, 只有在屬性裝飾器的關鍵字不同, 其他部分都是相同的.

  • @ObservedObject
struct ObservedView: View {

@ObservedObject var people = People()

var body: some View {

VStack{

Text("\(people.age)")

Button(action: {people.age = people.age + 1}) {
Text("update (@ObservedObject)")
}
}

}

}
  • @StateObject
struct StateView: View {

@StateObject var people = People()

var body: some View {

VStack{

Text("\(people.age)")

Button(action: {people.age = people.age + 1}) {
Text("update (@StateObject)")
}
}

}

}

接著, 我們在主頁面(ContentView)把這兩個View同時加入, 並且在主頁面也增加一個按鈕, 以及@State的屬性裝飾器來動態增加主頁面顯示的數字.

struct ContentView: View {

@State var count = 0

var body: some View {

VStack(spacing: 50){

VStack{

Text("\(count)")

Button(action: {count = count + 1} ) {
Text("update")
}
}


ObservedView()

StateView()

}

}
}

當我們按下ObservedView和StateView的按鈕時, 功能看起來都正常, 對應到的兩個people的age參數都能夠隨著點擊次數而增加. 但當我們點擊主頁面ContentView的按鈕後發現, ObservedView()產生的數字會變成0 (物件生成的初始值), 表示當我們在更新ContentView的時候, 原本在ObservedView中的people物件又被重新生成了一次(生命週期少於當前的View).

這只是其中一種情況, 當今天如果是在NavigationLink或在在sheet這種切換頁面的情況下, 我們會發現@ObservedObject所產生的物件生命週期可能會長於目前View或是等於View的情況發生. 為了不讓這種不確定性影響我們想要的行為, 所以在iOS14中才推出了@StateObject這個新的屬性裝飾器來讓我們使用.

參考資料:

--

--

KuoJed
彼得潘的 Swift iOS App 開發教室

「沒有一件你努力過的事是白費的。」 當你這麼相信,並且實踐,就會成真。