40.%d\n Xcode — Combine
Combine Practice
響應式編程試圖處理即時的數據變化,將其反應到UI上,無論採用KVO、觀察者模式或其他,這種數據跟UI的連動稱為data binding;而MVVM從MVC演化至今,我們有許多處理問題的方法,目的大致都希望程式碼簡單易讀,容易維護,方便加入更多功能,這當中用到許多OOP以來的概念,像是依賴注入,權責分離,工廠模式⋯⋯等,在使用過觀察者模式、RxSwift後,開始覺得響應式編程是一種趨勢,也看到這種趨勢是藉由dot syntax去叫出各個方法,方法裡運用閉包再做一些事,只要導入framework就方便使用,做好準備後,寫程式時便可將更多注意事項放到UI上,其他的就叫做“ 使用它 ”。
不斷透過dot syntax去呼叫方法是種看來神奇的方式,然而它只是將程式碼封裝起來,就好比在Xcode裡不見到過去C中的int main( ),它並不是沒有,只是封裝起來了,這邊慢慢來看這種方式如何達成的。
URLSession.shared
.dataTask(with: urlStr!) { data, resp, error in }.resume()
URLSession.shared.dataTaskPublisher(for: urlStr!)
.map { $0.data }
.sink { comp in
} receiveValue: { data in
}
首先,Xcode內建的URLSession是個open class,shared是一個singleton,為保證我們在程式裡不必建構多個實例,因為它做的都是相同事情,將url代入dataTask,你就可以處理數據、錯誤及網路響應,當然,後者常常被忽略,最後,因為前面的動作只是創建了dataTask,還需要一次的啟動,後面的resume也是一個方法,如果把後面兩個方法透過縮排,跟下面不斷地方法看起來也沒什麼不同了。
URLSession呼叫dataTask時回傳的是一個class,而Combine在呼叫dataTaskPublisher是回傳一個封裝過的struct,如此,我們可把各種方法分得清清楚楚了,但為何呢?
我們把創建流程等複雜但重複的情事丟到subscribe這個閉包裡,使用時只要呼叫閉包即可;但就像我們在寫函式一樣,如果閉包裡還有逃逸閉包就可以多做一些事了,最後再裝到struct裡,像這樣:
struct 我的結構 {
let subscribe = { 逃逸閉包 in
各種流程 & 使用逃逸閉包
啟動流程
}我的結構.subscribe { 閉包的data in
做一些事(如:處理data)
}
現在,我們從Combine裡看看下面這段
訂閱關係
let subscription =
發布者
URLSession.shared.dataTaskPublisher(for: url)
操作者
.map { $0.data }
訂閱者
.sink(receiveCompletion: { completion in },
receiveValue: { data in // 處理 data... })
如果把let task = URLSession.shared.dataTask視作訂閱關係,那麼將url代入就是發布,最後才是訂閱,這就像將urlComponents的scheme、host、path透過組裝變成一個url一樣,訂閱就是做了組裝這個動作。
所以回到上面,訂閱就只是訂閱,那我們可以把流程直接裝到struct裡面
我的結構 { 逃逸閉包 in
各種流程 & 使用逃逸閉包
啟動流程
}.subscribe { 閉包的data in
做一些事
}
於是,這個結構大概就剩下這樣
struct Publisher<T> {
let subscribe: (@escaping (T) -> Void) -> Void
}
使用泛型,可以保證訂閱時可以送出各種型別的東西,到這步,使用這個struct是這樣的
Publisher<我的東西> { 逃逸閉包 in
各種流程 & 使用逃逸閉包
}.subscribe { 我的東西的東西 in
處理東西
}
前面只是使用urlsession,現在可以塞更多其他東西。但到這邊為止,我們只學了半套,因為另外創建一個struct是不夠直覺的,雖然它可以代入各種,像是urlSession、notificationCenter,如果要使用它,反而該在原有的東西裡去擴充它。
extension URLSession {
func dataTaskPublisher(with url: URL) -> Publisher<Data> {
return Publisher<Data> { handler in
let task = self.dataTask(with: url) { data, resp, err in if let data = data {
handler(data)
}
}
task.resume() }
}}
擴充的方法回傳我們建立的struct,方法裡直接return這個struct,就可以讓這個struct去寫urlSession要做的事,還記得嗎?回傳的既然是struct,自然就可以透過dot syntax去呼叫subscribe了,像下面這樣:
URLSession.shared.dataTaskPublisher(with: urlStr) .subscribe { data in 處理 data}
這可不是combine裡的喔,是我們自己寫的,大致是藉由Publisher這個struct,來擴充urlSession另寫一個func,使之回傳的是Publisher,那麼便可在這個func裡直接寫Publisher裡的流程,再使用訂閱( subscribe )處理得到的data即可。
在大概知道combine是怎麼來的,接下來就學著用它了
這個畫面是透過點擊button來秀出表情符號與狀聲詞,頁面其實只是一個tableView,cell有一個IBOutlet,兩個IBAction,先看看如何獲取IBOutlet的
func getAnimals() {
// getAnimalsToken =
NetworkingService.getAnimals()
// .receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Publisher stopped observing")
case .failure(let error):
print("This is any error passed to our future", error)
}
}, receiveValue: { [weak self] (animals) in
self?.animals = animals
// self?.secondTableView.reloadData()
}
// .cancel()
)
}
先看sink這個訂閱動作,有completion跟value兩個閉包,completion主要是處理錯誤,value主要是處理回傳值。如果只這樣用,會跳出下面的警示
可能有兩種作法,一個是建立AnyCancellable作為整個流程的開頭,一個是在最後呼叫cancel( )這個方法。
receive這個方法可以控制排程,例如讓receiveValue在主線程動作。
回頭看NetworkingService.getAnimals( )
enum NetworkingService {
static func getAnimals() -> Future<[Animal], Error> {
return Future { promise in
let animals: [Animal] = [.dog, .cat, .frog, .panda, .lion]
promise(.success(animals))
}
}
}
在主要的方法處回傳一個class,就像上面回傳struct的用法,就可以在return裡寫處理資料的動作,這邊用enum挺有意思的,不過用struct或class也是可以的。
接著再看看AlertService
enum AlertService {
static func showAlert(with message: String, in viewController: UIViewController?) { let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) alert.addAction(.init(title: "OK", style: .cancel)) viewController?.present(alert, animated: true)
}
}
主要是藉由警示視窗彈出message與ok,按下ok關掉警示視窗。
接著看看cell
func populate(with animal: Animal) {
self.animal = animal
nameLabel.text = animal.name
}extension SecondTableViewCell {
enum Action {
case showEmoji(Animal)
case makeNoise(Animal)
}
}
這個方法即是setupCell,把VC裡拿到的animals透過下標為indexPath.row丟回cell裡。
並且兩個button的action為,按下button傳送了動作,像這樣
var actionPublisher = PassthroughSubject<Action, Never>()@IBAction func didTapShowEmojiButton() {
guard let animal = animal else {
return
}
actionPublisher.send(.showEmoji(animal))
}
將animal丟進showEmoji裡,那麼,VC在dataSource就會這樣寫
cell.actionPublisher.sink(receiveValue: { [weak self] action in
switch action {
case .showEmoji(let animal):
self?.showEmoji(for: animal)
case .makeNoise(let animal):
self?.makeNoise(for: animal)
}
}).store(in: &tokens)
最後.store(in: &tokens)的作法就像是RxSwift的disposeBag。
以下reference
最後附上