SwiftUI/Swift: 既存のプロジェクトをMVVMパターンに変更する

[体温を最速で入力するためのユーザーインターフェースの検討(その1) - Qiita](https://qiita.com/sYamaz/items/9ef8fceb5650fc7b7ad8)で体温入力のユーザーインターフェースを考えていました。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/88282c02-a58d-cef4-2a28-33411eafed30.gif)

前回は「とりあえず作った」だけの物だったので今後の変更やテストしやすさなどを考えてMVVMパターンに合わせていこうと思います。


本来このようなリファクタリングはユニットテストをあらかじめ作った上で実施していくものだと思いますが、あまりにテストしにくい形だったため今回は先に設計を変更することにしました。

環境は以下です

* Xcode 13
* Swift 5

## before

https://github.com/sYamaz/BodyTempLogger/tree/v0.0.1

クラス、構造体の関連を示すと以下のようなイメージです。

![名称未設定ファイル-before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/47e19a08-956c-bf77-3997-d931432c7399.png)

* AppDelegate；アプリケーション起動時の処理を行うクラス
* PostView：保存ボタン
* TapInputView：体温入力のためのボタンがあるView（と、その他分離できていない機能群）
  * その他の機能
    * ユーザー設定のためのシート表示
    * 現在入力している値の表示
* HealthCareRepository：HealthKitへのアクセスを担当するクラス
* HealthCareRepositoryDelegate：HealthKitへのアクセスのインターフェース（Protocol）。主にテストを容易にするためのもの

無計画にViewを作成しており、かつ状態は全てViewが持っています。

## after

MVVMでModel部分をどのように作るかが私の中で定まっていないですが今回は以下のようなパターンを考えます

![名称未設定ファイル-after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/04d6ea73-504e-0a7c-9ac7-fc475f72dbba.png)


* View
* ViewModel
* Model
  * Store: アプリケーションの状態を保持するクラス
    * Value: アプリケーションの状態（構造体）
  * Repository: 外部のAPIアクセスを隠蔽する。今回はHealthKit。

状態を構造体一発ではなくStoreとValueという形にしたのは状態の変更とそれを通知する機能を状態そのものから分離したかったためです。

このパターンをもとに、以下のように設計を変更しました

https://github.com/sYamaz/BodyTempLogger/tree/v0.0.3

![名称未設定ファイル-after2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/5ff1523f-0d7d-c91b-5d83-b4f0b1857f56.png)

ユーザー設定に関しては`PreferenceStore`, 入力中の体温値に関しては`TemperatureStore`が管理してます。

TapInputViewに詰め込んでいたユーザー設定や現在値の表示を分離し、ViewModelを作成しています



参考までに一番コードがシンプルなDisplayViewに関するView/ViewModel/Model(Store/Value)のコードを載せておきます。

### DisplayView(View)

```swift
import SwiftUI

extension DisplayView{
    private init(vm:DisplayViewModel, upperColor:Color, lowerColor:Color){
        self.vm = vm
        self.upperValueColor = upperColor
        self.lowerValueColor = lowerColor
    }
    
    func upperColor(_ color:Color) -> Self{
        return DisplayView(vm: self.vm, upperColor: color, lowerColor: self.lowerValueColor)
    }
    
    func lowerColor(_ color:Color) -> Self{
        return DisplayView(vm: self.vm, upperColor: self.upperValueColor, lowerColor: color)
    }
}

struct DisplayView: View {
    
    private let upperValueColor:Color
    private let lowerValueColor:Color
    
    @ObservedObject private var vm:DisplayViewModel
    
    init(vm:DisplayViewModel){
        self.vm = vm
        
        self.upperValueColor = Color.primary
        self.lowerValueColor = Color.primary
    }
    
    var body: some View {
        // 表示
        HStack(alignment: .firstTextBaseline, spacing: 4){
            Text("\(vm.temp.higher).")
                .foregroundColor(upperValueColor)
            Text("\(vm.temp.lower)")
                .foregroundColor(lowerValueColor)
            Text("℃").font(Font.system(size: 48))
        }.font(Font.system(size: 80))
    }
}
```

### DisplayViewModel(ViewModel)


```swift
import Foundation
import Combine
class DisplayViewModel: ObservableObject{
    private let healthCareRepository: HealthCareRepositoryDelegate
    @Published private (set) var temp:Temperature
    
    private var cancellable:AnyCancellable? = nil
    
    init(repo:HealthCareRepositoryDelegate, store:TemperatureStore) {
        self.healthCareRepository = repo
        self.temp = store.value
        
        self.cancellable = store.$value.sink(receiveValue: {t in self.temp = t})
    }
}

```

### TemperatureStore(Model - Store)

```swift
import Foundation
import Combine
class TemperatureStore: ObservableObject{

    @Published private (set) var value:Temperature
    
    init(_ initialValue:Temperature){
        self.value = initialValue
    }
    
    func update(_ closure:(Temperature) -> Temperature) -> Void{
        self.value = closure(self.value)
    }
}

```

使い方

```swift
let store:TemperatureStore
// 小数点以下の値を更新する
store.update({old in old.lower(9))
```


### Temperature(Model - Value)

今回の用途では、体温の小数点以上/以下それぞれ個別に変更するため以下のような作りにしてます

```swift
import Foundation

/// 入力中の体温を保持する構造体です
struct Temperature{
    /// 小数点以下
    let lower:Int
    
    /// 小数点以上
    let higher:Int
}

extension Temperature{
    public func lower(_ val:Int) -> Self{
        return .init(lower: val, higher: self.higher)
    }
    
    public func higher(_ val:Int) -> Self{
        return .init(lower: self.lower, higher: val)
    }
}

```

## 終わりに

私が現時点でそうだと思っているMVVM化できました。

Store-Value部分は使いやすいかどうか、テストしやすいかどうかなど今後検証してみたいところです。