UIKitでもCombineしたい!を叶えるCombineCocoaを試してみる
はじめに
iOS 14がリリースされてからしばらく経ち、皆さんが日々開発しているアプリにもiOS 13から使えるCombineフレームワーク が使えるようになってきたのではないでしょうか?Combineの使用用途としてまず挙がるのがネットワーキング等の非同期処理だと思います。一方でCombineはリアクティブプログラミングを実現する手段であるためUIKitでもCombineを使いたい!というニーズもあることでしょう。しかしながらCombineは標準ではUIKitをあまりサポートしていないため、自分でUIControlなどにPublisherを実装する必要があります。1つのアプリを開発する度に毎回Publisherを実装したくないですよね。そこで今回は自分でPublisherを実装しなくてもUIKitで簡単にCombineを使えるようにするライブラリ、CombineCocoaを試してみました。
開発環境
- macOS Catalina Version 10.15.6
- Xcode Version 12.1 (12A7403)
- iPhone 12 Pro iOS 14.1 シミュレーター
- CombineCocoa 0.2.1
What is CombineCocoa?
UIKitで簡単にCombineを使えるようにするライブラリです。MITライセンスで公開されています。
以下、リポジトリREADME.mdからの引用です。
CombineCocoa attempts to provide publishers for common UIKit controls so you can consume user interaction as Combine emissions and compose them into meaningful, logical publisher chains.
CombineCocoaはUIKitの各種コントロールに対してpublisherを生やしてくれているのでユーザーインタラクションをストリームイベントとして処理できる。そしてストリームなのでチェインできる。良さそうですね。
導入
早速導入してみましょう。CocoaPods、Swift Package Manager、Carthageを使ってインストールできますが、今回はXcodeのGUI操作でサクっとインストールできるSwift Package Managerを使ってインストールしました。
記事執筆時点の最新バージョンは 0.2.1
です。
Swift Package Managerを使った具体的なライブラリインストール方法は以下の記事を参照してください。
使ってみる
基本的にUIKitのコントロールに〜Publisher
というPublisherが生えているのでそれをsubscribeして流れてくる値を処理します。
各コントロールでどんなイベントを捕捉できるのか調べてみました。
UITextField
textField .textPublisher // AnyPublisher<String?, Never> .compactMap { $0 } // nilを処理したくないのでcompactMapを使ってnilを除去 .sink { text in // UI操作でtextが変更される度に呼ばれる print("text:", text) } .store(in: &subscriptions) textField .returnPublisher // AnyPublisher<Void, Never> .sink { // return buttonタップ時に呼ばれる print("did tap return key.") } .store(in: &subscriptions)
UISegmentedControl
segmentedControl .selectedSegmentIndexPublisher // AnyPublisher<Int, Never> .sink { index in // UI操作でselectedSegmentIndexが変更される度に呼ばれる。 // コードでの変更は通知されない。 print("index:", index) } .store(in: &subscriptions)
UIButton
button .tapPublisher // AnyPublisher<Void, Never> .sink { // ボタンタップ時に呼ばれる print("did tap button.") } .store(in: &subscriptions)
UIScrollView
以下サンプルはWKWebView
が持つ UIScrollView
を使用。
guard let url = URL(string:"https://dev.classmethod.jp/") else { return } let request = URLRequest(url: url) webView.load(request) webView .scrollView .contentOffsetPublisher // AnyPublisher<CGPoint, Never> .sink { point in // スクロールしてcontentOffsetが変わる度に呼ばれる print("point:", point) } .store(in: &subscriptions) webView .scrollView .reachedBottomPublisher() // AnyPublisher<Void, Never> .sink { // WebViewで一番下までスクロールした時に呼ばれる print("the bottom of the scrollview is reached.") } .store(in: &subscriptions)
UISlider
slider .valuePublisher // AnyPublisher<Float, Never> .sink { value in // UISliderのvalueが変わる度に呼ばれる print("value:", value) } .store(in: &subscriptions)
UIBarButtonItem
barButtonItem .tapPublisher // AnyPublisher<Void, Never> .sink { // ボタンタップ時に呼ばれる print("did tap barButtonItem.") } .store(in: &subscriptions)
UISwitch
`switch` .isOnPublisher // AnyPublisher<Bool, Never> .sink { isOn in // ON/OFFが変わる度に呼ばれる print("isOn:", isOn) } .store(in: &subscriptions)
UIStepper
stepper .valuePublisher // AnyPublisher<Double, Never> .sink { value in // UIStepperのvalueが変わる度に呼ばれる print("value:", value) } .store(in: &subscriptions)
UIDatePicker
datePicker .datePublisher // AnyPublisher<Date, Never> .sink { date in // UIDatePickerのdateが変わる度に呼ばれる print("date:", date) } .store(in: &subscriptions) datePicker .countDownDurationPublisher // AnyPublisher<TimeInterval, Never> .sink { duration in // UIDatePickerのcountDownDurationが変わる度に呼ばれる print("duration:", duration) } .store(in: &subscriptions)
UIRefreshControl
refreshControl .isRefreshingPublisher // AnyPublisher<Bool, Never> .sink { isRefreshing in // UIRefreshControlのisRefreshingが変わる度に呼ばれる print("isRefreshing", isRefreshing) if (isRefreshing) { DispatchQueue.main.asyncAfter(deadline: .now() + 3) { refreshControl.endRefreshing() } } } .store(in: &subscriptions)
UIPageControl
pageControl .currentPagePublisher // AnyPublisher<Int, Never> .sink { currentPage in // UIPageControlのcurrentPageが変わる度に呼ばれる print("currentPage:", currentPage) } .store(in: &subscriptions)
UITapGestureRecognizer
tapGestureRecognizer .tapPublisher // AnyPublisher<UITapGestureRecognizer, Never> .sink { recognizer in // UITapGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Tap Gesture Recognizer is triggered") } .store(in: &subscriptions)
UIPinchGestureRecognizer
pinchGestureRecognizer .pinchPublisher // AnyPublisher<UIPinchGestureRecognizer, Never> .sink { recognizer in // UIPinchGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Pinch Gesture Recognizer is triggered") } .store(in: &subscriptions)
UIRotationGestureRecognizer
rotationGestureRecognizer .rotationPublisher // AnyPublisher<UIRotationGestureRecognizer, Never> .sink { recognizer in // UIRotationGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Rotation Gesture Recognizer is triggered") } .store(in: &subscriptions)
UISwipeGestureRecognizer
swipeGestureRecognizer .swipePublisher // AnyPublisher<UISwipeGestureRecognizer, Never> .sink { recognizer in // UISwipeGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Swipe Gesture Recognizer is triggered") } .store(in: &subscriptions)
UIPanGestureRecognizer
panGestureRecognizer .panPublisher // AnyPublisher<UIPanGestureRecognizer, Never> .sink { recognizer in // UIPanGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Pan Gesture Recognizer is triggered") } .store(in: &subscriptions)
UIScreenEdgePanGestureRecognizer
screenEdgePanGestureRecognizer .screenEdgePanPublisher // AnyPublisher<UIScreenEdgePanGestureRecognizer, Never> .sink { recognizer in // UIScreenEdgePanGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Screen Edge Pan Gesture Recognizer is triggered") } .store(in: &subscriptions)
UILongPressGestureRecognizer
longPressGestureRecognizer .longPressPublisher // AnyPublisher<UILongPressGestureRecognizer, Never> .sink { recognizer in // UILongPressGestureRecognizerがジェスチャーを認識した時に呼ばれる print("the Long Press Gesture Recognizer is triggered") } .store(in: &subscriptions)
おわりに
今回はCombineCocoaでどんなイベントを処理できるのか確認してみました。注意点として、ユーザーインタラクションをストリームイベントとして処理できるライブラリなのでユーザー操作によるUIイベントしか処理できません。例えば UITextField
のtext
プロパティをコードで変更してもストリームには何も流れてきません。あくまでも対象は「ユーザー操作によるUIイベント」です。
個人的にめっちゃ便利だと思ったのはUIScrollView
のreachedBottomPublisher
です。
私が作成するアプリによくあるのが、WKWebView
でアプリの利用規約を表示して「ユーザーが最後まで規約を読んだかどうか」を「UIScrollViewを最後までスクロールしたかどうか」で判定することが多いのですがそのようなシチュエーションでめっちゃ使えそうですね。
今回ご紹介したCombineCocoa、記事執筆時点の最新バージョンがまだまだ0.2.1
ということもあり、今後更に機能追加される可能性もあります。
気になった方はチェックしてぜひPRを送ってみてはいかがでしょうか?
今回は以上です。