[Swift]RxSwift　ストップウォッチを作ってみた

# RxSwiftの実装練習
今回も引き続きRxSwiftの学習課題として、前回作成したカウンターアプリに少し機能を追加してストップウォッチアプリを作成しました。以下に実装したコードと利用した技術を紹介します。

## 仕様
| <img src="https://github.com/user-attachments/assets/6c81fb4a-beb2-461b-85c5-f61bf32fad61" width="300" /> |

- 秒数管理はUserDefaultsを利用してます。これによりアプリを閉じた後でも最後に10秒~60秒の指定した秒数が保持された状態になります
- MVVMアーキテクチャを採用

**利用したUI**
- Label
    - カウントの秒数を表示
- Button
    - スタートボタン
        - 押下するとカウントが始まります
    - ストップボタン
        - カウント止めます
    - リセットボタン
        - 最初に指定した秒数へ戻すことが可能となります
- Picker
    - 秒数を設定します。秒数は10秒から60秒の間で設定可能

## コード

**TimerViewController**
ユーザーがアプリケーションを操作する画面を提供し、ユーザーの操作をViewModelに通知します。また、データの表示やレイアウトの管理も行います。

```Swift:TimerViewController.swift

import UIKit
import RxSwift
import RxCocoa

final class TimerViewController: UIViewController {
    
    @IBOutlet private weak var countLabel: UILabel!
    @IBOutlet private weak var timePicker: UIPickerView!
    @IBOutlet private weak var startButton: UIButton!
    @IBOutlet private weak var stopButton: UIButton!
    @IBOutlet private weak var resetButton: UIButton!
    let settingArray = [10, 20, 30, 40, 50, 60]
    let settingKey = "timer_value"
    let viewModel = TimerViewModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        timePicker.delegate = self
        timePicker.dataSource = self
        bind(to: viewModel)
        timeSetting()
    }
}

private extension TimerViewController {
    func bind(to viewModel: TimerViewModel) {
        startButton.rx.tap
            .bind(to: viewModel.input.startButton)
            .disposed(by: disposeBag)
        
        stopButton.rx.tap
            .bind(to: viewModel.input.stopButton)
            .disposed(by: disposeBag)
        
        resetButton.rx.tap
            .bind(to: viewModel.input.resetButton)
            .disposed(by: disposeBag)
        
        viewModel.output.timerLabel
            .drive(countLabel.rx.text)
            .disposed(by: disposeBag)
        
    }
    
    func timeSetting() {
        let settings = UserDefaults.standard
        let timerValue = settings.integer(forKey: settingKey)
        let initialTime = timerValue != 0 ? timerValue : 10
        countLabel.text = "残り\(initialTime)秒"
        viewModel.input.countdownTime.accept(TimeInterval(initialTime))
        
        for row in 0..<settingArray.count {
            if settingArray[row] == initialTime {
                timePicker.selectRow(row, inComponent: 0, animated: true)
            }
        }
    }
}

extension TimerViewController: UIPickerViewDataSource, UIPickerViewDelegate {
    
    // pickerの列を指定
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    // pickerの行数を指定
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return settingArray.count
    }
    // pickerの中身を表示させる内容を指定
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return String(settingArray[row])
    }
    // pickerの選択時に実行
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let selectedValue = settingArray[row]
        countLabel.text = "残り\(selectedValue)秒"
        UserDefaults.standard.set(selectedValue, forKey: settingKey)
        viewModel.input.countdownTime.accept(TimeInterval(selectedValue))
    }
    
}
```

**TimerViewModel**
各ボタンの押下に応じてタイマーの操作を`TimerManager`に伝え、得られたデータをVC側に反映します。

```Swift:TimerViewModel.swift
import Foundation
import RxSwift
import RxCocoa

protocol TimerViewModelInputs: AnyObject {
    var startButton: PublishRelay<Void> { get }
    var stopButton: PublishRelay<Void> { get }
    var resetButton: PublishRelay<Void> { get }
    var countdownTime: BehaviorRelay<TimeInterval> { get }
}

protocol TimerViewModelOutputs: AnyObject {
    var timerLabel: Driver<String> { get }
}

protocol TimerViewModelType: AnyObject {
    var input: TimerViewModelInputs { get }
    var output: TimerViewModelOutputs { get }
}

final class TimerViewModel: TimerViewModelType, TimerViewModelInputs, TimerViewModelOutputs {
    
    var input: TimerViewModelInputs { return self }
    var output: TimerViewModelOutputs { return self }
    
    var startButton = PublishRelay<Void>()
    var stopButton = PublishRelay<Void>()
    var resetButton = PublishRelay<Void>()
    var countdownTime = BehaviorRelay<TimeInterval>(value: 0)
    
    var timerLabel: Driver<String>
    
    private let timerManager: TimerManager
    let disposeBag = DisposeBag()
    
    init() {
        timerManager = TimerManager(countdownTime: countdownTime)
        timerLabel = countdownTime
            .map { "残り \(Int($0)) 秒" }
            .asDriver(onErrorJustReturn: "Error")
        
        // タイマーを開始する
        startButton
            .subscribe(onNext: { [weak self] in
                self?.timerManager.startTimer()
            })
            .disposed(by: disposeBag)
        
        // タイマーを停止する
        stopButton
            .subscribe(onNext: { [weak self] in
                self?.timerManager.stopTimer()
            })
            .disposed(by: disposeBag)
        
        // タイマーをリセットする
        resetButton
            .subscribe(onNext: { [weak self] in
                self?.timerManager.resetTimer()
                self?.countdownTime.accept(TimeInterval(self?.timerManager.displayUpdate() ?? 0))
            })
            .disposed(by: disposeBag)
    }
}
```

**TimerManager**
タイマー操作のロジックを担当し、可読性と役割を明確にします。

```Swift:TimerManager.swift
import Foundation
import RxSwift
import RxCocoa

final class TimerManager {
    
    private var countdownTime: BehaviorRelay<TimeInterval>
    private var timer: Timer?
    private var count: Int = 0
    let settingKey = "timer_value"
    
    init(countdownTime: BehaviorRelay<TimeInterval>) {
        self.countdownTime = countdownTime
    }
    
    // タイマーを開始する処理
    func startTimer() {
        if let nowTimer = timer {
            if nowTimer.isValid {
                return
            }
        }
        // タイマーをスタート
        timer = Timer.scheduledTimer(timeInterval: 1.0,
                                     target: self,
                                     selector: #selector(timerInterrupt(_:)),
                                     userInfo: nil,
                                     repeats: true)
    }
    
    // タイマーが1秒ごとに呼ばれる処理
    @objc func timerInterrupt(_ timer: Timer) {
        count += 1
        
        let remainingTime = displayUpdate()
        if remainingTime <= 0 {
            count = 0
            // タイマー停止
            timer.invalidate()
        }
        countdownTime.accept(TimeInterval(remainingTime))
    }
    
    // タイマーを停止する処理
    func stopTimer() {
        if let nowTimer = timer {
            if nowTimer.isValid {
                nowTimer.invalidate()
            }
        }
    }
    
    // タイマーをリセットする処理
    func resetTimer() {
        count = 0
        let initialValue = UserDefaults.standard.integer(forKey: settingKey)
        countdownTime.accept(TimeInterval(initialValue))
        _ = displayUpdate()
    }
    
    func displayUpdate() -> Int {
        let settings = UserDefaults.standard
        let timerValue = settings.integer(forKey: settingKey)
        let remainCount = timerValue - count
        return remainCount
    }
}

```

# まとめ
現状、RxSwiftのよく利用するメソッドを中心に実装してます。引続きRxSwiftをさらに深く理解するため実装を行い備忘録としてまとめる予定です。
またMVVMアーキテクチャとUserDefaultsなどの技術もこの学習と通じて学んでいます。MVVMとUserDefaultsについても今後のアウトプットとして整理していけたらと考えています。