15
16

More than 1 year has passed since last update.

UIKitでもCombineを使いたい。

Last updated at Posted at 2020-09-30

はじめに

iOS14がリリースされ、Swft自体のアップデートやSwiftUIのスタンダード化が進む昨今ですが、
iOS12は切れなくて、実務ではCombineを使えていない方も多いのではないでしょうか。

けど新しい技術試してみたいですよね。
SwiftUI+Combineはちょっとハードル高いけど、まずは慣れたUIKitでCombineだけでも試してみたいですよね!!

というわけで今回は以下の内容で記事を書いてみました。

  • Combineの基礎概念について
  • UIKitで使ってみる

お役にたてば幸いです。:bow:

※ とりあえずコードが見たいって場合は以下をご覧いただけると幸いです。:bow:
toya108/CombineBookManagerApp

環境

この記事は以下の動作環境で動作確認しています。

  • Xcode12
  • Swift5.3

Combineって何?

一言で表すとApple純正の非同期フレームワークです。
AppleDeveloperの説明を引用します。

The Combine framework provides a declarative Swift API for processing values over time.

「Combineフレームワークは時間の経過に応じて値を処理するための宣言型のAPIを提供します。」みたいな意味です。

  • 宣言的に書ける
  • 時間の経過に応じて値を処理する

あたりがCombineの主機能であり、非同期フレームワークの性質でもありそうです。

Combineの登場人物を知る

Combineを使っててみる前に、全体の流れとCombineを使う上で知っておかなければいけない登場人物を紹介します。

登場人物

  • Publisher: 値を発行する。
  • Operator: 発行した値を変換する。
  • Subscriber: 値を受け取ってイベントを発火する。

図にするとこんな感じ。

Untitled Diagram.png

Rxやリアクティブプログラミングでは上記の概念をよくマーブルダイアグラムで表現しますが、
個人的には上の図のように登場人物を1-1-1で出した図で概念を捉えた方が頭に入りやすいです。

とりあえず触ってみよう

登場人物が分かったのでとりあえず触って見ましょう。

よく見るログイン画面です。
MVVMで設計している場合、ViewでTextFieldから受け取ったメールアドレスをViewModelにバインドする必要があります。
その時の実装を例にして、Publisher、Operator、Subscriberの解説と実装例を見ていきましょう。

Publisher

まずはPublisherです。
https://developer.apple.com/documentation/combine/publisher

Publisherは値を発行できるすごいやつなので、Viewで受け取ったメールアドレスを発行することができます。

まずはTextFieldのtextに変更があったら値を発行するPublisherを作って見ましょう。

import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!

    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        // textDidChangeNotificationが通知されたら`mailAddressTextField`というobjectを発行する。
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)

    }
}

importも忘れないようにしましょう。
これでtextFieldに変更があった時にmailAddressTextFieldというobjectが発行されるようになります。

Untitled Diagram (1).png

Operator

続いてOperatorいきましょう。
Operatorは値を変換できるすごいやつなので、上でPublisherが発行したUITextFieldのobjectも変換できちゃいます。
TextFieldの値のViewModelにバインドする時に、ViewModelに渡すのはただのStringで十分です。
なので、Operatorを使ってUITextFieldをStringに変換して見ましょう。

import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!

    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)
            .compactMap { $0.object as? UITextField } // UITextFieldのキャストに失敗してnilになったら弾く
            .map { $0.text ?? "" } // UITextFieldからtextを取り出してStringに変換
    }
}
  • .compactMapでUITextField以外のインスタンスを排除。
  • .mapでUITextFieldからtextを取得し、Stringに変換。

ということをやっています。

Untitled Diagram (2).png

このように、Publisherが発行した値と受け取りたい値が異なる時に、その中間で値の変換やハンドリングをしてくれるのがOperatorです。
Operatorの種類については以下の参照してください。
* 【iOS】Combineフレームワークまとめ

Subscriber

最後はSbscriberです。
Sbscriberは値を受け取って色々できるすごいやつなので、Publisherから流れてきたメールアドレスのStringをViewModelにセットできます。

import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!

    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)
            .compactMap { $0.object as? UITextField }
            .map { $0.text ?? "" }
            .removeDuplicates() // 重複した値は排除
            .eraseToAnyPublisher()
            .receive(on: RunLoop.main)
            .assign(to: \.mailAddress, on: viewModel) // viewModelの\.mailAddressに値を代入
            .store(in: &binding)
    }
}

final class LoginViewModel {
    var mailAddress: String = "" {
        didSet {
            print(mailAddress)
        }
    }
}

色々やっているように見えますが、ポイントは2つです。
* .assign(to: \.mailAddress, on: viewModel)で受け取ったStringをViewModelにセットしている。
* .store(in: &binding)でインスタンスを監視キャンセル可能にしている。

assignメソッドはPublisherから生えており、受け取った値をkeypathで定義されたオブジェクトへ渡しつつSubscriberを生成できます。
ただそのままだとメモリが解放されないため、よしなに監視をキャンセルされるように.store(in: &binding)を呼んでいます。

Untitled Diagram (3).png

※この辺は曖昧な理解で説明できないと思ったので、詳しくは@shizさんの記事を参照してください。
【iOS】Combineフレームワークまとめ

実行

ここまでのコードでTextFieldの値を受け取ったらViewModelにセットしてくれるようになりました!

まとめ

CombineはApple純正の非同期フレームワークであり、以下の特徴がある。

  • 宣言的に書ける
  • 時間の経過に応じて値を処理する

Combineには以下の主要な登場人物がいる。

  • Publisher: 値を発行する。
  • Operator: 発行した値を変換する。
  • Subscriber: 値を受け取ってイベントを発火する。

おまけ

Combineに慣れるためにUIKitとCombineでAPI叩いて書籍管理をするアプリも作ってます。
Combineを組み込んだAPIクライアントやExtension化してより使いやすくしたPublisher等が載っているつもりなので、こちらもよければご覧ください:bow:

toya108/CombineBookManagerApp

15
16
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
15
16