SwiftUIでカレンダーを実装する

SwiftUIでカレンダーを実装します。
画像のような感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/05a0e69e-f162-f5a9-7dbd-81326d8686f5.png" width="400" />

iOS16から導入された`UICalendarView`を`UIViewRepresentable`に準拠させることでカレンダーを実装しようと思います。

また、今回の実装するカレンダーの要件は以下です。
1. カレンダーを表示する(UIはあまり気にしない)
2. 日付タップ時に何かしらの処理を行えるようにする

### `UIViewRepresentable`
まずSwiftUIで扱えるようにします。

```swift
struct UICalendarViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> UICalendarView {
        let view = UICalendarView()
        return view
    }
    
    func updateUIView(_ uiView: UICalendarView, context: Context) {
    }
}
```

### `UICalendarSelectionSingleDateDelegate`
日付タップを検知します。
`UICalendarView`のデリゲートを使います。

```swift
struct UICalendarViewRepresentable: UIViewRepresentable {
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UICalendarView {
        let view = UICalendarView()
        let selection = UICalendarSelectionSingleDate(delegate: context.coordinator)
        view.selectionBehavior = selection
        return view
    }
    
    func updateUIView(_ uiView: UICalendarView, context: Context) {
    }
    
    class Coordinator: NSObject, UICalendarSelectionSingleDateDelegate {        
        private let parent: UICalendarViewRepresentable
        
        init(_ parent: UICalendarViewRepresentable) {
            self.parent = parent
        }
        
        func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
            print("did select")
        }
    }
}
```

### ViewModel
日付タップを外部のViewに通知することでViewModelでハンドリングできるようにします。
上記の`didSelectDate`デリゲートメソッドにCombineを使ってイベントを通知して、ViewModelで購読します。

```swift
func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
    parent.didSelectDateSubject.send()
}
```

```swift:ViewModel
private func subscribeDidSelectDate() {
    didSelectDateSubject
        .receive(on: DispatchQueue.main)
        .sink {
            print("did select")
        }
        .store(in: &cancellables)
}
```

全体のコードはこちらです。
Playgroundでそのまま実行できます。

```swift:MyPlayground.swift
import SwiftUI
import PlaygroundSupport
import Combine

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel = .init()

    var body: some View {
        UICalendarViewRepresentable(didSelectDateSubject: viewModel.didSelectDateSubject)
    }
}

class ContentViewModel: ObservableObject {
    private(set) var didSelectDateSubject: PassthroughSubject<Void, Never> = .init()
    private var cancellables: Set<AnyCancellable> = []
    
    init() {
        subscribeDidSelectDate()
    }
    
    private func subscribeDidSelectDate() {
        didSelectDateSubject
            .receive(on: DispatchQueue.main)
            .sink {
                print("did select")
            }
            .store(in: &cancellables)
    }
}

struct UICalendarViewRepresentable: UIViewRepresentable {
    private let didSelectDateSubject: PassthroughSubject<Void, Never>
    
    init(didSelectDateSubject: PassthroughSubject<Void, Never>) {
        self.didSelectDateSubject = didSelectDateSubject
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UICalendarView {
        let view = UICalendarView()
        let selection = UICalendarSelectionSingleDate(delegate: context.coordinator)
        view.selectionBehavior = selection
        return view
    }
    
    func updateUIView(_ uiView: UICalendarView, context: Context) {
    }
    
    class Coordinator: NSObject, UICalendarSelectionSingleDateDelegate {        
        private let parent: UICalendarViewRepresentable
        
        init(_ parent: UICalendarViewRepresentable) {
            self.parent = parent
        }
        
        func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
            parent.didSelectDateSubject.send()
        }
    }
}

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
```


## 参考
https://www.fuwamaki.com/article/353#%E5%8D%98%E4%B8%80%E6%97%A5%E4%BB%98%E9%81%B8%E6%8A%9E