はじめに
iOS14がリリースされ、Swft自体のアップデートやSwiftUIのスタンダード化が進む昨今ですが、
iOS12は切れなくて、実務ではCombineを使えていない方も多いのではないでしょうか。
けど新しい技術試してみたいですよね。
SwiftUI+Combineはちょっとハードル高いけど、まずは慣れたUIKitでCombineだけでも試してみたいですよね!!
というわけで今回は以下の内容で記事を書いてみました。
- Combineの基礎概念について
- UIKitで使ってみる
お役にたてば幸いです。
※ とりあえずコードが見たいって場合は以下をご覧いただけると幸いです。
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: 値を受け取ってイベントを発火する。
図にするとこんな感じ。
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が発行されるようになります。
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に変換。
ということをやっています。
このように、Publisherが発行した値と受け取りたい値が異なる時に、その中間で値の変換やハンドリングをしてくれるのがOperatorです。
Operatorの種類については以下の参照してください。
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)
を呼んでいます。
※この辺は曖昧な理解で説明できないと思ったので、詳しくは@shizさんの記事を参照してください。
【iOS】Combineフレームワークまとめ
実行
ここまでのコードでTextFieldの値を受け取ったらViewModelにセットしてくれるようになりました!
まとめ
CombineはApple純正の非同期フレームワークであり、以下の特徴がある。
- 宣言的に書ける
- 時間の経過に応じて値を処理する
Combineには以下の主要な登場人物がいる。
- Publisher: 値を発行する。
- Operator: 発行した値を変換する。
- Subscriber: 値を受け取ってイベントを発火する。
おまけ
Combineに慣れるためにUIKitとCombineでAPI叩いて書籍管理をするアプリも作ってます。
Combineを組み込んだAPIクライアントやExtension化してより使いやすくしたPublisher等が載っているつもりなので、こちらもよければご覧ください