[iOS][Swift]CombineでUIKitのUI更新を実装してみる

データバインディングに関してはRxSwiftを基本的には使用してきました。
しかしiOS13からバインディングのライブラリとしてSwiftUIと同時にCombineが登場しました。
そこで使用感を比較するためにCombineを使った実装をしてみようと思います。
async/awaitの実装と組み合わせる場合など注意事項もあったのでご紹介いたします。

サンプルコードに関してはこちらの記事の続きです。
[[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装](https://qiita.com/bu-ka/items/258099de42aae50f4d77)

# やること
ViewModelでasync/awaitを利用した通信処理でデータを取得してViewModelのプロパティの値を更新します。
そしてその値を元にUIが更新されるようにします。

# `@Published`
```ViewModel.swift
...
@Published var title = ""
...
```
このようにプロパティに`@Published`を付けることによって値を監視対象にすることができます。

ViewController側で値の更新をバインドするためには以下のようにします。
```ViewController.swift
...
        // ナビゲーションバーのタイトルにバインド
        viewModel.$title
            .map { Optional($0) }
            .receive(on: DispatchQueue.main)
            .assign(to: \.title, on: navigationItem)
            .store(in: &cancellables)
...
```
$を付けることによって`Publisher`として利用することで`assign`で値の変更をバインドできます。
##### 補足
`UINavigationItem`の`title`にバインドするためには上記のようにしますが
```swift
.assign(to: \.navigationItem.title, on: self)
```
これでもビルドが通ります。
しかし`on:`に`self`を指定すると何も対策しない場合メモリリークが発生します。
画面遷移の前に.`store`している`cancellables`の`cancel()`を呼んでおけば発生しないことは確認しています。
なるべくなら`self`を指定しない方がよさそうです。

# 実装
Qiitaの記事をAPIから取得してTextViewとナビゲーションバーのタイトルに反映させるという実装をします。
こちらの記事の実装を修正していきます。`APIClient`とかはそちらで実装を確認していただけるとありがたいです。
[[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装](https://qiita.com/bu-ka/items/258099de42aae50f4d77)

```ArticleViewModel.swift
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)
        }
    }
}
```
```ArticleViewController.swift
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`を明記した方が良いような気がします。

#### 実行結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/e10a6db2-2ead-35e5-9b6c-57f0a2f32902.png" width="160">

# まとめ
UIKitとの相性でいうと、RxSwiftの方が相性が良く、機能もそちらの方が充実しているためすぐさまCombineに移行できるかというと、もう少し勉強コストをかけたり工夫をしなければいけない部分は多いです。
ただ、Apple純正のフレームワークであるという点と、SwiftUIとの整合性も非常に高いという点、SwiftUIに移行することになってもスムーズに移行できるという点が非常にメリットとしては大きいです。
なので勉強していって今後損になるということはないと思いますので積極的に利用していこうかと思います。