前一篇是提到「SwiftUI 中的形狀元件與運用」,雖然本系列文章基本上沒有前後關聯,如果你是還沒讀過前一篇的讀者,也推薦你去讀讀。
在 MVVM 架構中,一個 SwiftUI view 會搭配一個 view model 。而這個 view model property 會加上 @ObservedObject 這個 property wrapper 。
struct NameCard: View {
@ObservedObject var viewModel: NameCardViewModel
var body: some View {
Text(viewModel.name)
}
}
class NameCardViewModel: ObservableObject {
@Published var name: String = "海德格救救救救我"
}
例如這個 view ,我們可以看到 NameCard 是直接使用 NameCardViewModel 這個具體型別 (concrete type/class) ,而不是 protocol 。
身為寫 Swift 的工程師,我們都愛 POP ,不是手繪的那個,而是蘋果在 WWDC15 開始提及的 "Protocol-Oriented Programming" 。因此我們通常會希望 view 會相依在抽象型別 (abstract type) 或在 Swift 我們叫它 protocol 。
struct NameCard: View {
@ObservedObject var viewModel: NameCardViewModelProtocol
var body: some View {
Text(viewModel.name)
}
}
protocol NameCardViewModelProtocol: ObservableObject {
var name: String { get }
}
class NameCardViewModel: NameCardViewModelProtocol {
@Published var name: String = "海德格救救救救我"
}
稍微改寫一下成這樣,但是在第二行 viewModel 宣告的地方會有錯誤
Type 'any NameCardViewModelProtocol' cannot conform to 'ObservableObject'
Use of protocol 'NameCardViewModelProtocol' as a type must be written 'any NameCardViewModelProtocol'
Replace 'NameCardViewModelProtocol' with 'any NameCardViewModelProtocol'
等等一些看了也沒有實質意義的錯誤訊息
簡而言之就是 @ObservedObject
和 protocol 不和,這時候的出口就是只能靠泛型 (generic) 進場解決。
struct NameCard<ViewModel: NameCardViewModelProtocol>: View {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. 加上泛型
@ObservedObject var viewModel: ViewModel
// ^^^^^^^^^ 2. 替換成泛型的型別
var body: some View {
Text(viewModel.name)
}
}
基本上只要這兩個步驟即可
如果有自定義的 init 方法,也只要把 parameter 的型別換成泛行所定義的名稱即可
init(viewModel: ViewModel) {
// ^^^^^^^^^
self.viewModel = viewModel
}
iOS 17 等在今年的更新的多了一個 Observable
可以簡化實作上的繁雜。但由於還沒進正式版,所以我認為目前的解法仍然是有效的。至於 OS 更新之後該怎麼辦,我們可以日後再擇篇詳述。
今天的 SwiftUI 大大小小雖然沒有和畫面有直接關係,但是提到了該怎麼解決當想要套用 MVVM 和 POP 時會碰到的問題。
如果有更好的解法,或是 SDK 更新需要做相對應得變更,歡迎留言回饋!
那今天的 SwiftUI 的大大小小就到這邊,
以上,明天見!
本篇使用到的 UI 元件和 modifiers 基本上沒有受到版本更新影響,因此在 Xcode 14 等環境下使用也是沒有問題的。