[iOS]いきなり！RxSwift (当方はSwift初心者でいきなりRxSwift！) [実践編1] カウンターアプリを作ろう

#はじめに
前回は、RxSwiftの概要だけでお腹いっぱいになってしまいました。今回は実食に移りたいと思います。

#環境
Xcode10.3
Swift5.0.1
RxSwift 4.3.1
RxCocoa 4.3.1

#作るもの

前回に引き続き、

[比較して学ぶRxSwift入門](https://www.amazon.co.jp/%E6%AF%94%E8%BC%83%E3%81%97%E3%81%A6%E5%AD%A6%E3%81%B6RxSwift%E5%85%A5%E9%96%80-%E6%8A%80%E8%A1%93%E3%81%AE%E6%B3%89%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA%EF%BC%88NextPublishing%EF%BC%89-%E9%AB%99%E6%A9%8B-%E5%87%8C-ebook/dp/B07LGMNXS3)

こちらの書籍から、カウンターアプリを作っていきます。
機能としては、

・カウントの値を見ることができる
・カウントアップができる
・カウントダウンができる
・リセットができる

になります。


#準備

1.プロジェクト作成

-Xcodeを起動 
-Create a new Xcode project 
　＞Single View App
　＞Product Name:CounterApp
　＞完了。すぐにプロジェクトを閉じます。

2.ターミナルを起動して、ディレクトリに移動

```
$ cd CounterApp
```

3.Podfile作成/編集

```
$ pod init
$ vi Podfile
```

```:Podfile
# platform :ios, '9.0'

target 'CounterApp' do
  use_frameworks!

  pod 'RxSwift',    '~> 4.3.1' #この行を追加
  pod 'RxCocoa',    '~> 4.3.1' #この行を追加  

end
````


4.ライブラリのインストール

```
$ pod install
```

5.プロジェクトを開く
必ずCounterApp.xcworkspaceから起動する(.xcodeprojから起動した場合、導入したライブラリーが使えません)


#Storyboardを削除！？
Storyboardを取り除き、ViewController + xibスタイルで開発をしていくとのことです。
著者様によりますと、Storyboardのデメリットとして、

- アプリが大きくなるほど画面遷移が複雑になり見辛くなる
- 都度、ViewControllerの生成が面倒
- チーム開発になると*.storyboardがconflictしまくる

だそうです。当方、チーム開発経験がないため、わからず。
とりあえず、著者様のおっしゃる通りに進めてみましょう。

1.Main.storyboardの削除
/CounterApp/Main.storyboardをDelete > Move to Trash

2.Info.plist
Info.plistを開く ＞ Main storyboard file base nameの項目を削除(マイナスボタン)

3.AppDelegateの修正

```AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
    /* 追加 ここから */
    self.window = UIWindow(frame: UIScreen.main.bounds)
    let navigationController = UINavigationController(rootViewController: ViewController())
    self.window?.rootViewController = navigationController
    self.window?.makeKeyAndVisible()
    /* 追加 ここまで */
    
    return true
}
```

4.ViewController.xibの作成
- New File ＞ View ＞ Save As: ViewController.xib ＞ Create
- ViewController.xibを開く
- Placeholders > File's Ownerを選択
- ClassにViewControllerを指定
- OutletのviewとXibのViewを接続
- Build & Run > 成功でOK

#レイアウト修正

ボタンを3つ > IBActionとしてViewController.swiftに接続
ラベルを1つ > IBOutletとしてViewController.swiftに接続
以下のように配置します。
<img width="932" alt="スクリーンショット 2019-07-26 18.38.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468725/873cbdf9-908f-73d1-518e-5aed36bd7003.png">

↓接続後はこんな形になります。

```ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var countLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()

    }

    @IBAction func countUp(_ sender: Any) {
    }
    
    @IBAction func countDown(_ sender: Any) {
    }
    
    @IBAction func countReset(_ sender: Any) {
    }
}

```

#パターン1:callbackを利用
```ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var countLabel: UILabel!
    
    private var viewModel: CounterViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = CounterViewModel()

    }

    @IBAction func countUp(_ sender: Any) {
        viewModel.incrementCount(callback: { [weak self] count in
            self?.updateCountLabel(count)
        })
    }
    
    @IBAction func countDown(_ sender: Any) {
        viewModel.decrementCount(callback: {[weak self] count in
            self?.updateCountLabel(count)
        })
    }
    
    @IBAction func countReset(_ sender: Any) {
        viewModel.resetCount(callback: {[weak self] count in
            self?.updateCountLabel(count)
        })
    }
    
    private func updateCountLabel(_ count: Int) {
        countLabel.text = String(count)
    }
}

class CounterViewModel {
    private(set) 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)
    }
}


```

#パターン2:delegateを利用
```ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var countLabel: UILabel!
    
    private let presenter = CounterPresenter()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.attachView(self)
    }

    @IBAction func countUp(_ sender: Any) {
        presenter.increamentCount()
    }
    
    @IBAction func countDown(_ sender: Any) {
        presenter.decrementCount()
    }
    
    @IBAction func countReset(_ sender: Any) {
        presenter.resetCount()
    }
}

extension ViewController: CounterDelegate {
    func updateCount(count: Int) {
        countLabel.text = String(count)
    }
}

protocol CounterDelegate  {
    func updateCount(count: Int)
}

class CounterPresenter {
    private var count = 0 {
        didSet {
            delegate?.updateCount(count: count)
        }
    }
    
    private var delegate: CounterDelegate?
    
    func attachView(_ delegate: CounterDelegate) {
        self.delegate = delegate
    }
    
    func detachView() {
        self.delegate = nil
    }
    
    func increamentCount() {
        count += 1
    }
    
    func decrementCount() {
        count -= 1
    }
    
    func resetCount() {
        count = 0
    }
}


```

#パターン3:RxSwiftを利用
```ViewController.swift
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var countUpButton: UIButton!
    @IBOutlet weak var countDownButton: UIButton!
    @IBOutlet weak var countResetButton: UIButton!
    
    private let diposeBag = DisposeBag()
    private var viewModel: CounterRxViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewModel()
    }
    
    private func setupViewModel() {
        viewModel = CounterRxViewModel()
        let input = CounterViewModelInput(
            countUpButton: countUpButton.rx.tap.asObservable(),
            countDownButton: countDownButton.rx.tap.asObservable(),
            countResetButton: countResetButton.rx.tap.asObservable()
        )
        viewModel.setup(input: input)
        
        //iOSはメインスレッドでUI操作のため、Observable(BehaviorRelay)をDriverに変換して、監視するらしい
        viewModel.outputs?.counterText
            .drive(countLabel.rx.text)
            .disposed(by: diposeBag)
    }
}

struct CounterViewModelInput {
    let countUpButton: Observable<Void>
    let countDownButton: Observable<Void>
    let countResetButton: Observable<Void>
}

protocol CounterViewModelOutput {
    var counterText: Driver<String?>  {get}
}

protocol CounterViewModelType {
    var outputs: CounterViewModelOutput? {get}
    func setup(input: CounterViewModelInput)
}


//ViewModel
class CounterRxViewModel: CounterViewModelType {
    var outputs: CounterViewModelOutput?
    
    private let countRelay = BehaviorRelay<Int>(value: 0)
    private let initialCount = 0
    private let disposeBag = DisposeBag()
    
    init() {
        self.outputs = self
        resetCount()
    }
    
    func setup(input: CounterViewModelInput) {
        input.countUpButton
            .subscribe(onNext: { [weak self] in
                self?.incrementCount()
            })
            .disposed(by: disposeBag)
        
        input.countDownButton
            .subscribe(onNext: { [weak self] in
                self?.decrementCount()
            })
            .disposed(by: disposeBag)
        
        input.countResetButton
            .subscribe(onNext: { [weak self] in
                self?.resetCount()
            })
            .disposed(by: disposeBag)
    }
    
    private func incrementCount() {
        let count = countRelay.value + 1
        countRelay.accept(count)
    }
    
    private func decrementCount() {
        let count = countRelay.value - 1
        countRelay.accept(count)
    }
    
    private func resetCount() {
        countRelay.accept(initialCount)
    }
}

extension CounterRxViewModel: CounterViewModelOutput {
    var counterText: Driver<String?> {
        return countRelay
            .map {"\($0)" }
            .asDriver(onErrorJustReturn: nil)
    }
}

```