【SwiftUI】芝生カレンダー作った

SwiftUIでヒートマップ形式のカレンダー(芝生、草とか言われてるやつ)を作ってみたので簡単な仕組みではありますがメモしておこうと思います。

# MMHeatmapライブラリ
![](https://user-images.githubusercontent.com/72431055/115141826-5572b580-a079-11eb-822b-4e05cf9273ca.png)
折角ライブラリを作ったので学習も兼ねてSwift Package Managerに対応し、公開してみました。

+ リポジトリ: https://github.com/s-n-1-0/MMHeatmap<br>
   導入方法とかはリポジトリのREADME.mdにあるので割愛します。

# View構造
最下層から順に説明します。

#### 1週間用VStack
まず1週間分(7つのセル)を次のような感じでVStackで縦に並べます。

```swift
VStack(spacing:2){
            ForEach(0..<7){ i in
                RoundedRectangle(cornerRadius: 2).frame(width: 10,height: 10).modifier(CellColorModifier(isRange: (i >= start && i <= end ) , value: values[i], maxValue: maxValue,minColor: style.minCellColor,baseColor: style.baseCellColor))
            }
        }
```
[MMHeatmapColumnView.swift](https://github.com/s-n-1-0/MMHeatmap/blob/main/Sources/MMHeatmap/MMHeatmapColumnView.swift)

プレビューしにくいのでこの階層ではDate型は扱わず、上のViewから与えられた表示範囲と値に従ってセルの色を決定します。
※CellColorModifierについては後述します。

#### 1ヵ月用HStack
次にHStackで1週間用VStackを週数分並べます。
1ヶ月分の週数の特定は、翌月の0日(つまりその月の最終日)を設定したDateComponentsをDate型にした後、Calendarで週数を求めることができます。

``` swift
var comp = DateComponents()
comp.year = 2021
comp.month = MM + 1
comp.day = 0
let date = calendar.date(from: comp)!
let weeks = calendar.component(.weekOfMonth, from: date)
```
#### 数ヶ月用HStack
最後に1ヶ月用HStackを指定範囲の月数分並べます。

# セルの色決定
データとなる数値配列の中で最大値の色をbaseColor、最小値0の色をminColorとしてbaseColorとminColorの中間色を求めます。
尚、データの値をnilとした場合はセルの色を透明(Color.clear)に設定しています。

次のモディファイアの

```swift
saturation = (saturation - secondSaturation) * pct + secondSaturation
brightness = (brightness - secondBrightness) * pct + secondBrightness
```
の部分で中間色(彩度、明度)を決定しています。
(このコードでは色相の中間色は求めてませんでした) 

```swift
fileprivate struct CellColorModifier:ViewModifier {
    init(isRange:Bool,value:Int?,maxValue:Int,minColor:UIColor,baseColor:UIColor) {
        self.isRange = isRange
        if let v = value{
        let pct:CGFloat = CGFloat(v) / CGFloat(maxValue)
            var secondHue:CGFloat = 0
            var secondSaturation:CGFloat = 0
            var secondBrightness:CGFloat = 0
            var secondAlpa:CGFloat = 0
        minColor.getHue(&secondHue, saturation: &secondSaturation, brightness: &secondBrightness, alpha: &secondAlpa)
        var hue:CGFloat = 0
        var saturation:CGFloat = 0
        var brightness:CGFloat = 0
        var alpha:CGFloat = 0
        baseColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
        saturation = (saturation - secondSaturation) * pct + secondSaturation
        brightness = (brightness - secondBrightness) * pct + secondBrightness
        self.rangeColor = Color(UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha))
        }else{
            self.rangeColor = Color(UIColor.clear)
        }
    }
    let isRange:Bool
    let rangeColor:Color
    func body(content: Content) -> some View {
        Group{
        if(isRange){
            content.foregroundColor(rangeColor)
        }else{
            content.foregroundColor(Color.clear)
        }
        }
    }
}
```

# Swift Package
次の記事を参考にしました。jjjkkkjjj様に感謝します。
https://qiita.com/jjjkkkjjj/items/727517263292ae7a3a87

ちなみにSwiftUIのプレビューをする際に大量のエラーが出る場合は、プレビューするファイル以外を閉じて仮想端末をMacからiPhoneやiPadにすると直りました。

# 終わりに
以上で基本的な芝生カレンダーを作ることができました。
改善するやる気が起きたらGeometryReaderを使ってMMHeatmapを画面サイズに応じたカレンダーに改良したいと思います。