LoginSignup
2

More than 3 years have passed since last update.

ReactiveSwift を使ってQiitaの記事一覧を表示するアプリを作った

Posted at

そもそも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: ())

viewModelviewDidLoad の状態を知らせます。 RxSwiftAnyObserver#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の BehaviorSubjectBehaviorRelay と同じようなものと捉えてもらって大丈夫だと思います。

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との差異は大きくは感じませんでした。
現状は「書き方が違うもの。」の認識が強いので今後は「書き方以外の差異」を見つけて使いこなしていきたいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2