データバインディングに関してはRxSwiftを基本的には使用してきました。
しかしiOS13からバインディングのライブラリとしてSwiftUIと同時にCombineが登場しました。
そこで使用感を比較するためにCombineを使った実装をしてみようと思います。
async/awaitの実装と組み合わせる場合など注意事項もあったのでご紹介いたします。
サンプルコードに関してはこちらの記事の続きです。
[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装
やること
ViewModelでasync/awaitを利用した通信処理でデータを取得してViewModelのプロパティの値を更新します。
そしてその値を元にUIが更新されるようにします。
@Published
...
@Published var title = ""
...
このようにプロパティに@Published
を付けることによって値を監視対象にすることができます。
ViewController側で値の更新をバインドするためには以下のようにします。
...
// ナビゲーションバーのタイトルにバインド
viewModel.$title
.map { Optional($0) }
.receive(on: DispatchQueue.main)
.assign(to: \.title, on: navigationItem)
.store(in: &cancellables)
...
$を付けることによってPublisher
として利用することでassign
で値の変更をバインドできます。
補足
UINavigationItem
のtitle
にバインドするためには上記のようにしますが
.assign(to: \.navigationItem.title, on: self)
これでもビルドが通ります。
しかしon:
にself
を指定すると何も対策しない場合メモリリークが発生します。
画面遷移の前に.store
しているcancellables
のcancel()
を呼んでおけば発生しないことは確認しています。
なるべくならself
を指定しない方がよさそうです。
実装
Qiitaの記事をAPIから取得してTextViewとナビゲーションバーのタイトルに反映させるという実装をします。
こちらの記事の実装を修正していきます。APIClient
とかはそちらで実装を確認していただけるとありがたいです。
[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装
import Foundation
import Combine
final class ArticleViewModel {
private let apiClient: APIClient
init(apiClient: APIClient = APIClient()) {
self.apiClient = apiClient
}
/// 記事タイトル
@Published var title = ""
/// 記事内容
@Published var body = ""
/// データ取得処理
func update() async { // ---(1)
do {
let articles = try await apiClient.fetchArticles()
guard let id = articles.first?.id else {
return
}
let article = try await apiClient.fetchArticleDetail(itemId: id)
title = article.title // ---(2)
body = article.body // ---(2)
} catch {
print(error)
}
}
}
import UIKit
import Combine
final class ArticleViewController: UIViewController {
/// ViewModel
var viewModel = ArticleViewModel()
/// cancellable
private var cancellables = Set<AnyCancellable>()
/// 記事詳細表示
@IBOutlet private weak var bodyTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$title
.map { Optional($0) }
.receive(on: DispatchQueue.main) // ---(3)
.assign(to: \.title, on: navigationItem)
.store(in: &cancellables)
viewModel.$body
.receive(on: DispatchQueue.main) // ---(3)
.assign(to: \.text, on: bodyTextView)
.store(in: &cancellables)
update()
}
private func update() {
Task.detached { // ----(4)
await self.viewModel.update()
}
}
}
---(1)&---(4)
ViewModel外でawaitで呼ぶ実装に変更しました。
Task{}
とTask.detached{}
の違いは、スレッドの変更がありかなしかというようなイメージで良いと思います。
Task{}内でのself.
記述は必要ですが[weak self]
で弱参照にしなくてもリークすることはないです。
---(2)&---(3)
await
呼び出しの前後でスレッドが違うので.receive(on: DispatchQueue.main)
が必要になります。これはViewModelを@MainActor
にすることで回避可能でしたがその場合だとしても御作法としてDispatchQueue.main
を明記した方が良いような気がします。
実行結果
まとめ
UIKitとの相性でいうと、RxSwiftの方が相性が良く、機能もそちらの方が充実しているためすぐさまCombineに移行できるかというと、もう少し勉強コストをかけたり工夫をしなければいけない部分は多いです。
ただ、Apple純正のフレームワークであるという点と、SwiftUIとの整合性も非常に高いという点、SwiftUIに移行することになってもスムーズに移行できるという点が非常にメリットとしては大きいです。
なので勉強していって今後損になるということはないと思いますので積極的に利用していこうかと思います。