iOS
Swift
RxSwift

Callback, Delegateと比較して学ぶRxSwift入門 カウンターアプリ編

概要

  • 簡単なアプリをテーマに、Callback, Delegate, RxSwift それぞれで実装したパターンを比較して、RxSwiftを学ぶ記事です

書かない

ターゲットの目安

  • プログラミング歴1年以上(種類問わず)
  • Swift による iOS アプリの開発経験が少しだけある(3ヶ月〜1年未満)
  • RxSwiftライブラリを使った開発をしたことがない

環境

  • OSX High Sierra
  • Xcode 9.4
  • Swift 4.1
  • cocoapods 1.5.3
  • carthage 0.30.1

今回のテーマ

  • テーマ
    • カウンターアプリ
  • 機能
    • カウントダウン
    • カウントアップ
    • リセット
  • アーキテクチャ
    • MVVM

イメージ

f.gif

実装

CallBack

import UIKit

class CallBackViewController: UIViewController {

    @IBOutlet weak var countLabel: UILabel!

    let viewModel = CallBackViewModel()

    @IBAction func countUp(_ sender: Any) {
        viewModel.incrementCount(callback: { [weak self] count in
            self?.updateLabel(count: count)
        })
    }

    @IBAction func countDown(_ sender: Any) {
        viewModel.decrementCount(callback: { [weak self] count in
            self?.updateLabel(count: count)
        })
    }

    @IBAction func reset(_ sender: Any) {
        viewModel.resetCount(callback: { [weak self] count in
            self?.updateLabel(count: count)
        })
    }

    private func updateLabel(count: Int) {
        countLabel.text = "コールバックパターン: \(count)"
    }
}

class CallBackViewModel {
    private var count = 0

    func incrementCount(callback: (Int) -> ()) {
        count += 1
        callback(count)
    }

    func decrementCount(callback: (Int) -> ()) {
        count -= 1
        callback(count)
    }

    func resetCount(callback: (Int) -> ()) {
        count = 0
        callback(count)
    }
}

  • 良い
    • 記述が簡単
  • 悪い
    • ボタンを増やすたびにメソッドが増えていく
    • ラベルの文字変更処理も増える

Delegate

import UIKit

class DelegateViewController: UIViewController {
    @IBOutlet weak var countLabel: UILabel!

    let viewModel = DelegateViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.delegate = self
    }

    @IBAction func countUp(_ sender: Any) {
        viewModel.incrementCount()
    }

    @IBAction func countDown(_ sender: Any) {
        viewModel.decrementCount()
    }

    @IBAction func resetCount(_ sender: Any) {
        viewModel.resetCount()
    }

}

extension DelegateViewController: SimpleTapDelegate {
    func updateCount(count: Int) {
        countLabel.text = "Delegateパターン: \(count)"
    }
}

protocol SimpleTapDelegate {
    func updateCount(count: Int)
}

class DelegateViewModel {
    private var count = 0 {
        didSet {
            delegate?.updateCount(count: count)
        }
    }

    var delegate: SimpleTapDelegate?

    func incrementCount() {
        count += 1
    }

    func decrementCount() {
        count -= 1
    }

    func resetCount() {
        count = 0
    }
}
  • 良い
    • 処理を委譲できる
    • increment, decrement, resetがデータの処理だけに集中できる
  • 悪い
    • ボタンを増やすたびにメソッドが増えていく

RxSwift/RxCocoa

import UIKit
import RxSwift
import RxCocoa

class RxViewController: UIViewController {

    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var countUpButton: UIButton!
    @IBOutlet weak var countDownButton: UIButton!
    @IBOutlet weak var countResetButton: UIButton!

    private let disposeBag = DisposeBag()

    let viewModel = RxViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewController()
        setupViewModel()
    }

    private func setupViewController() {
        countUpButton.rx.tap
            .asDriver()
            .drive(onNext: { [weak self] in
                self?.viewModel.incrementCount()
            })
            .disposed(by: disposeBag)

        countDownButton.rx.tap
            .asDriver()
            .drive(onNext: { [weak self] in
                self?.viewModel.decrementCount()
            })
            .disposed(by: disposeBag)

        countResetButton.rx.tap
            .asDriver()
            .drive(onNext: { [weak self] in
                self?.viewModel.resetCount()
            })
            .disposed(by: disposeBag)
    }

    private func setupViewModel() {
        viewModel.countRelay
            .asDriver()
            .map { return "Rxパターン: \($0)"}
            .drive(countLabel.rx.text)
            .disposed(by: disposeBag)
    }

}

class RxViewModel {
    let countRelay = BehaviorRelay<Int>(value: 0)

    func incrementCount() {
        let count = countRelay.value + 1
        countRelay.accept(count)
    }

    func decrementCount() {
        let count = countRelay.value - 1
        countRelay.accept(count)
    }

    func resetCount() {
        countRelay.accept(initialCount)
    }

}

  • 良い
    • increment, decrement, resetがデータの処理に集中できる
    • ViewModelはViewControllerのことを考えなくても良くなる
      • 👉例: delegate?.updateCount(count: count) のようなデータの更新を伝えなくても良くなる
    • データとUIを bind することでデータ更新時UI更新!を意識しなくても良くなる
  • 悪い
    • コード量が他パターンより多い (絶対悪ってわけではないけど)
    • Outletが増える

総行数

pattern line
CallBack 50
Delegate 58
RxSwift 68

まとめ

  • 今回のようなすごくシンプルなアプリではCallBackパターンが1番シンプルに書くことができる
    • 逆に、RxSwiftで書くと1番複雑(≒コード量が多く)なる