Keyword: swiftUI,Coroutine Scope
既然我們將拉取網路資料的部分下放到了shared中的新ViewModel,那們ObservableObject的工作就簡單許多,只要提供顯示使用的資訊即可.
首先.我們把網路目前的狀態利用三個Published通知UI,分別代表DataState中的各數據.讀取中,讀取到的資料,錯誤訊息
class CafeItemObservableModel: ObservableObject {
private var viewModel: iOSCafeViewModel? = nil
@Published var loading = false
@Published var cafeList: Array<CafeResponseItem>? = nil
@Published var error: String? = nil
}
然後我們昨天建立iOSCafeViewModel,需要提供一個Interface,讓資料可以溝通,我們在viewModel的建構子中提供給他.順便當有資料來的時候,印出一些訊息.
func activate(){
viewModel = iOSBasicViewModel { [weak self] dataState in
self?.loading = dataState.loading//讀取
self?.cafeList = dataState.data//讀取到的資料
self?.error = dataState.exception//發生的錯誤訊息
if let cafeList = dataState.data{
print("size: \(cafeList.count)")//印出收到的值的個數
}
if let errorMessage = dataState.exception{
print("exception: \(errorMessage)")//印出錯誤內容
}
}
}
最後,在離開頁面時,提供一個方法讓iOS端呼叫,避免Coroutine Leak的問題發生
func deactivate() {//在頁面離開的時候呼叫,避免Leak
viewModel?.onDestroy()
viewModel = nil
}
全部就會像這樣
class CafeItemObservableModel: ObservableObject {
private var viewModel: iOSBasicViewModel? = nil
@Published var loading = false
@Published var cafeList: Array<CafeResponseItem>? = nil
@Published var error: String? = nil
func activate(){
viewModel = iOSBasicViewModel { [weak self] dataState in
self?.loading = dataState.loading
self?.cafeList = dataState.data
self?.error = dataState.exception
if let cafeList = dataState.data{
print("size: \(cafeList.count)")
}
if let errorMessage = dataState.exception{
print("exception: \(errorMessage)")
}
}
}
func deactivate() {
viewModel?.onDestroy()
viewModel = nil
}
}
接下來也來改寫ContentView,除了使用新的來源外,還能讓SwiftUI在讀取和呈現時有些特別的效果.
先重寫一下每一行的樣式,改為使用shared內的ViewModel的數據.
struct CafeRowView : View{
var cafeResponseItem: CafeResponseItem
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(cafeResponseItem.name).font(.headline)
Text(cafeResponseItem.address).font(.subheadline)
}
}
}
}
然後是使用這個樣式的ListContent,在發生錯誤與讀取時,這個ListContent會更換顯示內容,而正常狀態下,就是普通的List
struct CafeListContent : View{
var loading: Bool
var cafeList: Array<CafeResponseItem>?
var error: String?
var body: some View {
ZStack {
VStack {
if let cafeList = cafeList {//正常收到,使用建立的RowView
List(cafeList, id: \.self){cafe in
CafeRowView(cafeResponseItem:cafe)
}
}
if let error = error {//發生錯誤,顯示錯誤訊息
Text(error)
.foregroundColor(.red)
}
}
if (loading) {//讀取中,顯示Loading的文字
Text("Loading...")
}
}
}
}
最後最外層的View,沒有實際的畫面,但是負責讓數據跟observableModel綁定,以及在View出現時進行讀取,View消失時取消Coroutine避免Leak.
struct CafeListScreen :View {
@ObservedObject var observableModel = CafeItemObservableModel()
var body: some View{
CafeListContent(//讓CafeListContent的值與observableModel內的數據綁定
loading: observableModel.loading,
cafeList: observableModel.cafeList,
error: observableModel.error
)
.onAppear(perform: {//畫面出現時,開始讀取
observableModel.activate()
})
.onDisappear(perform: {//畫面消失時,停止讀取
observableModel.deactivate()
})
}
}
最後讓Content View改成這個新的CafeListScreen就可以啦!如果檔名不同記得去iOSApp檔案修改,就在ContentView的同路徑下.
可以注意到雖然不像CoroutineScope或是Android的LifecycleObserver強制把起點終點都訂好,但是ContentView還是提供了一個接近的onAppear與onDisappear來管理View的生命週期.但是有個缺點,沒有強制性,所以新人仍然有可能會漏掉onDisappear,造成Leak
今天就到這裡,明天我們會回到Kotlin本身.我們會使用Koin進行注入管理.