この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
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を送ってみてはいかがでしょうか?
今回は以上です。