UIKitとCombineを使用する場合はCombineCocoaも同時に使用するとユーザイベントが簡単に取得できて便利だったので記事にしました。
UIKitとCombineを使用する場合はCombineCocoaも同時に使用するとユーザイベントが簡単に取得できて便利だったので記事にしました。
こんにちは!
長期インターン生でiOSエンジニアのえつしです。
私は現在、iOSエンジニアとしての経験を積むために岡山からフルリモートでEMoshUでの開発を行っています。
業務としては12月にリリース予定のC向け自社アプリの開発に携わっています。
個人開発では使用しない技術を使用したり、CTO直々にコードレビューしていただけたりとかなりの好環境で働かせていただいてます!!!
さて、業務では、それぞれ「SwiftUIはまだ不安定である」「CombineはRxSwiftより機能が最小限で使いやすい」という理由から、StoryBoardとCombineを使用した開発を行なっています。
また、UIKitとCombineを同時に使用するにあたってCombineCocoaというライブラリを使用しています。
このCombineCocoa、業務で使ってかなり便利、というかUIKitとCombineを使うなら必須レベルだなと感じたので今回記事にさせていただきます。
主にUIKitのみを使う場合との対比をさせていただいておりますが、普段はRxSwiftを使っているけどCombineはどんな感じだろうと考えている方も雰囲気掴めると思います。
ぜひ最後までご覧ください!
CombineとはiOS13で登場したApple純正の非同期処理のフレームワークです。
Combineの登場までは非同期処理といえばRxSwiftでしたが、Apple公式のフレームワークということでCombineが選択される開発も増えているのではないでしょうか。
Combineでは以下の3つの要素を使用して処理を進めていきます。
処理の流れとしては、下の図のように①Publisherが発行したイベント(値)を②Operatorが加工して、③Subscriberが受け取って処理を行う、といった具合に進みます。
本記事ではCombineCocoaを使用してUIKitからPublisherを生やす話ですので、Publisherが主人公になります。
CombineCocoaはCombineをUIKitで簡単に使えるようにするライブラリです。
ユーザのUIパーツに対するアクションを簡単に拾うことができます。
UIKitのみの場合とCombine+CombineCocoaの場合でコードがどう変わるのか、実際に使って比較してみましょう。
今回私が使用する環境は以下の通りです。
また、実装内容は以下の通りです。
先に完成形を貼っておきます。
それでは実装していきましょう!
まず初めに、CombineCocoaの導入を行います。
導入はCocoaPods、Carthage、SwiftPackageManagerのどれでもOKです。
今回はSwiftPackageManagerを使用して導入します。
次に、StoryBoardではUITextFieldとUIButton、UILabelを配置しておきます。
StoryBoardはUIKitのみを使用する場合とUIKit+Combine+CombineCcocoaを使用する場合で共通です。
ここからは、内部の処理をViewControllerに書いていきます。
UIKitのみを使用する場合、UITextFieldへの入力を受け取るためにはaddTargetメソッドを使用して、実行する関数とUIControlのイベントを指定してあげます。
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc func textFieldDidChange(textField: UITextField) {
guard let text = textField.text else {
return
}
button.isEnabled = !text.isEmpty
}
また、IBActionを使用してUIButtonをタップした時の処理も記述してあげましょう。
@IBAction func buttonTapped(_ sender: Any) {
label.text = textField.text
}
これで完成です。全体を見てみましょう。
import Foundation
import UIKit
final class UIKitViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc func textFieldDidChange(textField: UITextField) {
guard let text = textField.text else {
return
}
button.isEnabled = !text.isEmpty
}
@IBAction func buttonTapped(_ sender: Any) {
label.text = textField.text
}
}
至って普通ですね。
強いて言えば@objcや@IBActionなどの修飾子が付いたメソッドがあるのでちょっと散らかって見えるかもしれません。
次にUIKit+Combine+CombineCocoaを見てみましょう。
CombineCocoaの導入によりUITextFieldからtextPublisherが生やせるようになっています。
textPublisherはUITextFieldに対してユーザからアクションがあった時にtextを流します。
これにより、addTargetをしなくてもtextの変更を取得することができます。
@IBAction func buttonTapped(_ sender: Any) {
label.text = textField.text
}
また、UIButtonからはtapPublisherが生やせます。
これはユーザによる.touchUpInsideのイベントを流します。
これによりIBActionを記述しなくてもユーザがUIButtonをタップした時の処理を実装できます。
button.tapPublisher // ボタンタップ時に呼ばれる
.sink(receiveValue: { [weak self] in
self?.label.text = self?.textField.text // ボタンタップでLabelに入力文字を表示
})
.store(in: &cancellables)
これでUIKitのみを使用した場合とほぼ同等の動作をします。
全体を見てみましょう。
import UIKit
import Combine
import CombineCocoa
class CombineViewController: UIViewController {
private var cancellables = Set()
@IBOutlet weak var label: UILabel!
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
private func bind() {
textField.textPublisher // 入力があるたび呼ばれる
.compactMap{ $0 } // nilを除去
.sink(receiveValue: { [weak self] text in
self?.button.isEnabled = !text.isEmpty // textが空で無ければボタンを有効化
})
.store(in: &cancellables)
button.tapPublisher // ボタンタップ時に呼ばれる
.sink(receiveValue: { [weak self] in
self?.label.text = self?.textField.text // ボタンタップでLabelに入力文字を表示
})
.store(in: &cancellables)
}
}
今回は処理をbind()という関数にまとめています。
UIKitのみを使用する場合と比較してだいぶ綺麗に書けたのではないでしょうか。
Combineを使用している場合、UIパーツから生やしたPublisher以外のPublisherをSubscribeしていることも多いと思います。
コードがスッキリして見やすくなったり同じ形式で処理を記述できるので開発のスピードが上がるなどメリットがありますね。
UIKitでは、UIパーツで起きたイベント処理の多くはDelegateメソッドとして実装することが多いと思います。
DelegateメソッドはiOSエンジニアとしては親しみがある実装方法ですが、記述する量が多かったり、呼び出し元のUIがわかりづらいなど不便な点もあります。
しかし、CombineCocoaを利用すると、これらのDelegateメソッドの不便な点を解決して、処理をPublisherにまとめることができます。
例えば、UIControlのサブクラスであるUIパーツからはcontrolEventPublisherを生やすことができるようになっています。
UITextFiledでの編集開始や編集終了時の処理を記述する場合を考えてみます。
まず、UIKitのみを使用する場合、UITextFiledDelegateを使用して実装できます。
この場合、extensionなどでViewControllerを切り分けて処理を記述する場合が多いのではないでしょうか。
extension UIKitViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
// 編集開始時の処理
}
func textFieldDidEndEditing(_ textField: UITextField) {
// 編集終了時の処理
}
}
Combine+CombineCocoaを使用する場合は、ControlEventPublisherを使用して以下のように記述できます。
textField.controlEventPublisher(for: .editingDidBegin)
.sink(receiveValue: {
// 編集開始時の処理
})
.store(in: &cancellables)
textField.controlEventPublisher(for: .editingDidEnd)
.sink(receiveValue: {
// 編集終了時の処理
})
.store(in: &cancellables)
これにより他のPublisherと並べて同様の形で処理を記述することができます。
場合にもよりますが、Delegateメソッドを使用する場合と比べて処理をスッキリと記述することができますね。
また、UITableViewのようにUIControlのサブクラスではないUIパーツのDelegateメソッドを代替するPublisherやUIBarButtonItemのようにタップ時のActionを指定するUIパーツをタップした時にイベントを流すPublisherもあります。
tableView.didSelectRowPublisher
.sink(receiveValue: { index in
// セルタップ時の処理
})
.store(in: &cancellables)
tableView.reachedBottomPublisher()
.sink(receiveValue: {
// 下までスクロールした時の処理(UIScrollViewのDelegateメソッド)
})
.store(in: &cancellables)
barButtonItem.tapPublisher
.sink(receiveValue: {
// ボタンタップ時の処理
})
.store(in: &cancellables)
もちろんこれは一例です。
例えばUITableViewの場合以下のデリゲートメソッドをCombineCocoaを使用して代替できるようになっています。
tableView(_:willDisplay:forRowAt:)
tableView(_:willDisplayHeaderView:forSection:)
tableView(_:willDisplayFooterView:forSection:)
tableView(_:didEndDisplaying:forRowAt:)
tableView(_:didEndDisplayingHeaderView:forSection:)
tableView(_:didEndDisplayingFooterView:forSection:)
tableView(_:accessoryButtonTappedForRowWith:)
tableView(_:didHighlightRowAt:)
tableView(_:didSelectRowAt:)
tableView(_:didDeselectRowAt:)
tableView(_:willBeginEditingRowAt:)
tableView(_:didEndEditingRowAt:)
UITableViewの場合はほとんど全てのデリゲートメソッドを代替できるようですね。
UIパーツによっては代替するPublisherが用意されていないデリゲートメソッドもあるようですが(例えば、UITextFieldのtextField(_ :shouldChangeCharactersIn:replacementString:)など)、まだCombineCocoaはprimalバージョンとのことなので、今後のアップデートに期待できます。
いかがでしたでしょうか!
CombineCocoaを中心に記事にさせていただいたので、Publisherを生やす話が多かったと思います。
CombineCocoaにはこの記事で紹介したPublisher以外にも有用なPublisherが多く存在します。
どんなPublisherがあるのかもっと知りたい!と思った場合は、個人的にはGitHub上のCombineCocoaの公式のソースコードをみるのがオススメです。
Swiftで書かれているので読みやすく、GitHubのREADMEに載ってないPublisherもかなりの数あります。
私自身も執筆のためにソースコードを読んでいて「こんなPublisherあったのか...」となり勉強になりました笑
今回は「もしUIKitでUIを作りCombineで非同期処理を行うなら絶対にCombineCocoaの導入を検討するべきではないだろうか」と題しまして記事を書かせていただきました。
「今までRxSwiftを使用していたが、Combineを導入するか悩んでいる」「UIKitと一緒に使いたいが使用感どうなんだろう」といった方の一助になれば幸いです。
最後までご覧いただきありがとうございました!
また、株式会社EMoshUではガンガン開発を行い共に成長していける仲間を募集しています!
会社のMissionや採用情報をご確認の上、自分に合っている・挑戦してみたい、と感じた方はぜひご連絡ください!!!
【開発ブログ】iOSアプリ内課金の開発におけるテストパターン|Engineering|EMoshU Blog|株式会社EMoshU
EMoshU Blog|iOSアプリ内課金の開発におけるテストパターンについてまとめました。