LoginSignup
1
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-04-26

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

サンプルコードに関してはこちらの記事の続きです。
[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装

やること

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で値の変更をバインドできます。

補足

UINavigationItemtitleにバインドするためには上記のようにしますが

.assign(to: \.navigationItem.title, on: self)

これでもビルドが通ります。
しかしon:selfを指定すると何も対策しない場合メモリリークが発生します。
画面遷移の前に.storeしているcancellablescancel()を呼んでおけば発生しないことは確認しています。
なるべくならselfを指定しない方がよさそうです。

実装

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

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を明記した方が良いような気がします。

実行結果

まとめ

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2