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進行注入管理.