SwiftUIでカレンダービューを作り（LazyVGridを使って100行未満のコードで）

これはSwiftUIを使ったカレンダービュー作成に関する記事です。LazyVGridを使用します。

![F9972033-1538-4111-A8A2-893FF787B8CC.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/412af246-6ce2-bef4-f2ab-ed78a6a9575b.jpeg)

## 月の切り替えと日付の選択を可能にするSwiftUIのカレンダービューをSwiftパッケージ化しました

https://github.com/mszpro/Selectable-Calendar-View

## ビュー構造を作る

カレンダービューでは、列数7で`LazyVGrid`を使用します。つまり、7要素毎に改行を追加します。

これを7に設定するのは、1週間の日数が7日だからです。

```swift
struct CalendarView: View {
    
    // 表示される月の任意の日に設定します（通常は1日）
    let monthToDisplay: Date
    
    init(monthToDisplay: Date) {
        self.monthToDisplay = monthToDisplay
    }
    
    var body: some View {
        LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) {
            // TODO
        }
    }
    
}
```

`LazyVGrid`では、まず平日の名称を表示し、その後、現在の月のすべての日をループ表示します。

```swift
LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) {
    // Week day labels
    ForEach(["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"], id: \.self) { weekdayName in
        Text(weekdayName)
    }
    // Day number text
    Section {
        ForEach(monthToDisplay.getDaysForMonth(), id: \.self) { date in
            // Only display days of the given month
                    if Calendar.current.isDate(date, equalTo: monthToDisplay, toGranularity: .month) {
                        Text("\(date.getDayNumber())")
                            .padding(8)
                            .foregroundColor(.white)
                            .background(Color.blue)
                            .cornerRadius(8)
                            .id(date)
                    } else {
                        Text("\(date.getDayNumber())")
                            .padding(8)
                            .foregroundColor(.white)
                            .background(Color.blue)
                            .cornerRadius(8)
                            .hidden()
                    }
        }
    }
}
```

## 任意の月のすべての日を取得

上記の関数において、`getDaysForMonth()`は`Date`オブジェクトの拡張関数です。この関数は月の第一週の初めの日から最終週の最後の日までの日付を含む配列を生成します。

月の最初の週または最後の週は、前月または翌月の日を含む場合があるため、結果の配列をフィルター処理して、今月の日だけを含むようにします。

```swift
extension Date {
    
    func getDaysForMonth() -> [Date] {
        guard
            let monthInterval = Calendar.current.dateInterval(of: .month, for: self),
            let monthFirstWeek = Calendar.current.dateInterval(of: .weekOfMonth, for: monthInterval.start),
            let monthLastWeek = Calendar.current.dateInterval(of: .weekOfMonth, for: monthInterval.end)
        else {
            return []
        }
        let resultDates = Calendar.current.generateDates(inside: DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end),
                                                         matching: DateComponents(hour: 0, minute: 0, second: 0))
        return resultDates
    }
    
}


extension Calendar {

    func generateDates(inside interval: DateInterval, matching components: DateComponents) -> [Date] {
        var dates = [interval.start]
        enumerateDates(startingAfter: interval.start,
                       matching: components,
                       matchingPolicy: .nextTime
        ) { date, _, stop in
            if let date = date {
                if date < interval.end {
                    dates.append(date)
                } else {
                    stop = true
                }
            }
        }
        return dates
    }
}

```

## カレンダーを表示する

SwiftUIビューで、`CalendarView`ビューコンポーネントを起動します。 `monthToDisplay`入力では、表示したい月の任意の日を指定することができます。

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            if let targetMonth = Calendar.current.date(byAdding: .month, value: 0, to: Date()) {
                Text(targetMonth.getYearMonthString())
                CalendarView(monthToDisplay: targetMonth)
            }
        }
    }
}
```

---

https://gist.github.com/mszpro/008348305f3ddbf61297ac244497834d

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">