そもそもReactiveSwift とは
Reactive Extensionsでの実装をするためのライブラリで、よく比較対象にあがるのはRxSwiftです。
大きな特徴としては、HotなObservable, ColdなObservableそれぞれ異なる型を持っています。
なぜReactiveSwiftなのか?
ちゃんとした理由があるとかではないです。今回やろうと思ったきっかけという感じになるのでご了承ください。
採用面接時に RxSwift ではなく ReactiveSwift を使用しているとの話があり、その際に先ほど挙げたHot/ColdなObservableが型から違ってHot/Coldを意識した実装が可能という話を聞き、気になっていました。安直ですが、それが今回使ってみようと思った理由です。
依存関係
作ってみた結果
ファイル構成
ReactiveSwiftSample
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── DataSource.swift
├── Info.plist
├── SceneDelegate.swift
├── UITableView+Reactive.swift
├── ViewController.swift
└── ViewModel.swift
基本的にみてもらえると良いのは以下の4つのファイルです。
ViewController.swift
ViewModel.swift
-
DataSource.swift
-
UITableView
を使用して画面に表示するものを決める
-
-
UITableView+Reactive.swift
-
UITableView
のReactiveCocoa拡張
-
コード説明
ViewController
※ Storyboardを使用することを前提としています。
import UIKit
class ViewController: UITableViewController {
let viewModel = ViewModel()
let dataSource: DataSource
init() {
fatalError()
}
required init?(coder: NSCoder) {
self.dataSource = DataSource(viewModel: viewModel)
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.reactive.setDataSource(dataSource)
viewModel.viewDidLoadObserver.send(value: ())
}
}
tableView.reactive.setDataSource(dataSource)
使用する データソースをtableViewにセットしています。よくやる
tableView.dataSource = self // selfはUITableViewDataSourceに準拠している
と同じものと考えてもらって大丈夫です。
viewModel.viewDidLoadObserver.send(value: ())
viewModel
に viewDidLoad
の状態を知らせます。 RxSwift
の AnyObserver#onNext
でやっているようなことと同じ感じです。
ViewModel
import ReactiveSwift
import QiitaAPIKit
class ViewModel {
typealias Elements = QiitaAPIKit.ArticleRequest.Response
// Input
let viewDidLoadObserver: Signal<Void, Never>.Observer
// Output
let items = MutableProperty<Elements>([])
init() {
let disposable = SerialDisposable()
let viewDidLoadSignal = Signal<Void, Never>.pipe()
self.viewDidLoadObserver = viewDidLoadSignal.input
let producer = SignalProducer<Elements, Never> { observer, _ in
QiitaAPIKit.ArticleRequest(requestQueryItem: .init(query: "iOS")).request { (result) in
switch result {
case .success(let response):
observer.send(value: response)
case .failure:
observer.send(value: [])
}
}
}
disposable.inner = CompositeDisposable([
self.items <~ viewDidLoadSignal.output
.flatMap(.latest, { _ in producer })
.observe(on: UIScheduler())
])
}
var numberOfSections: Int {
return 1
}
func item(at indexPath: IndexPath) -> Elements.Element {
return items.value[indexPath.row]
}
func numberOfItems(in section: Int) -> Int {
return items.value.count
}
}
この ViewModel
ではAPIを叩いてデータを取得したり、テーブルビューに表示する内容について取得する プロパティ、 メソッドを定義しています。
MutableProperty
変更可能な値を保持し、値の変更を監視するものです。RxSwiftの BehaviorSubject
や BehaviorRelay
と同じようなものと捉えてもらって大丈夫だと思います。
Signal<Void, Never>.pipe()
RxSwiftで PublishSubjectを生成し、Observer, Observableに分けて使用するのと同じようなことができます。 input
でObserver, output
でObservableが取得できます。
SignalProducer<Elements, Never>.init
ReactiveSwiftでRxSwiftの Observable.create
と同じようなことをするには SignalProducer
のinitializerを使用します。クロージャの引数にObserverが渡されるので、そこに対して send
関数で値を流してあげます。
<~
この例では MutableProperty
である items
に取得した Elements
をバインドしています。返り値に Disposable?
が返されます。
DataSource
通常DataSourceを書く時と大体同じです。 ReactiveDataSource
に準拠し、viewModelで持っている値を取得したいのでinitialize時にviewModelを渡すようにしています。
import UIKit
import ReactiveSwift
class DataSource: NSObject, ReactiveDataSource {
var backgroundView: SignalProducer<UIView?, Never> = .init { UIView() }
var elementsChanged: Signal<(), Never> {
return viewModel.items.map { _ in }.signal
}
var emptyView: UIView = UIView()
var dataSource: UITableViewDataSource {
self
}
var isScrollEnabled: Property<Bool> = .init(value: true)
let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
}
extension DataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = viewModel.item(at: indexPath)
cell.textLabel?.text = item.title
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfItems(in: section)
}
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.numberOfSections
}
}
UITableView+Reactive
こちらのissue(Make it easier to work with table views / collection views #3608)からほぼそのまま持ってきました。
import ReactiveCocoa
import ReactiveSwift
protocol ReactiveDataSource: AnyObject, UITableViewDataSource {
var backgroundView: SignalProducer<UIView?, Never> { get }
var elementsChanged: Signal<(), Never> { get }
var emptyView: UIView { get }
var isScrollEnabled: Property<Bool> { get }
}
extension Reactive where Base: UITableView {
/// Set up a reactive data source binding. Uses the binding target internally.
func setDataSource<T: ReactiveDataSource>(_ dataSource: T) {
self.dataSource.action(dataSource)
}
/// A binding target that allows you to dynamically update the data source over time.
var dataSource: BindingTarget<ReactiveDataSource> {
let disposable = SerialDisposable()
lifetime += disposable
return makeBindingTarget { base, dataSource in
disposable.inner?.dispose()
base.dataSource = dataSource
disposable.inner = CompositeDisposable([
base.reactive.backgroundView <~ dataSource.backgroundView,
base.reactive.isScrollEnabled <~ dataSource.isScrollEnabled,
base.reactive.reloadData <~ dataSource.elementsChanged,
])
base.reloadData()
}
}
var backgroundView: BindingTarget<UIView?> {
return makeBindingTarget { $0.backgroundView = $1 }
}
}
まとめ
今回ReactiveSwiftを触ってみて正直なところこのくらいだとRxSwiftとの差異は大きくは感じませんでした。
現状は「書き方が違うもの。」の認識が強いので今後は「書き方以外の差異」を見つけて使いこなしていきたいです。