FSCalendarでGoogleCalendar風のUIを作成する

## はじめに
この記事はSwiftのカレンダーライブラリである[FSCalendar](https://github.com/WenchaoD/FSCalendar.git)を使ってGoogleCalendarのようなUIを実現する方法について解説しています。

## 環境
* Xcode15.0.1 
* Swift5.9

## 完成図
![Simulator Screenshot - iPhone 15 Plus - 2023-12-20 at 20.46.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/793dbf88-e361-a731-86d5-07b1f940a632.png)

Google Calendarを意識しました。

![Screenshot 2023-12-20 at 15.07.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/0eb24ffa-9d88-60b1-018e-65c31f8aea07.png)

## 開発の手順
1. FSCalendarの準備
1. カスタムのカレンダーセルを作成
1. TableViewセルを作成
1. 予定の反映

## 1. FSCalendarの準備
FSCalendarのインポート、およびStoryboardの準備をします。（詳細は省略します。）
![Screenshot 2023-12-20 at 21.23.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/4af284d1-f2eb-5d4c-eb8e-b8d30d6bc8e5.png)

## 2. カスタムのカレンダーセルを作成
デフォルトで登録されているセルを使用せず、自分でカレンダーのセルを作成します。
FSCalendarCellというクラスがあるはずです。今回はStoryboardで作成したいので、`Also create XIB file`も忘れずにチェックします！
![Screenshot 2023-12-20 at 21.32.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/5b0c62a1-f6d0-1c6e-8dc8-e21cb76448af.png)

作成されたセルにLabelとTableViewを配置しました。今回は複数の予定をTableViewで表示します。

![Screenshot 2023-12-20 at 21.36.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/378bb079-7e42-e8b3-e104-00c9f995c8df.png)

セルのサイズは50*75くらいがうまくおさまるというところでしょうか。後にオートレイアウトで調整します。

## 3. TableViewセルを作成
今回はGoogle Calendar風のUIにしたので、このように作りました。先ほどCalendar Cellの高さを75に設定したので、TableView Cellの高さは20にしておきました。2つほど予定が表示できる計算です。
![Screenshot 2023-12-20 at 21.39.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741705/09105052-9184-274a-bead-39c4fb3b29c4.png)

コードはパーツを宣言して関連付けしておけばOKです。
```Swift: CalendarEventTableViewCell
import UIKit

class CalendarEventTableViewCell: UITableViewCell {
    @IBOutlet var eventLabel: UILabel!
    @IBOutlet var eventImageView: UIImageView!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
    
}

```

## 4. Calendar Cellの内容

Calendar Cellは難しくありません！CollectionViewCellやTableViewCellの内容を書くのと同じ容量で書けます。

```Swift: 
import UIKit
import FSCalendar

class CustomCalendarCell: FSCalendarCell, UITableViewDataSource, UITableViewDelegate {
    
    @IBOutlet var dateLabel: UILabel!
    @IBOutlet var eventTableView: UITableView!
    var eventList: [TaskObject]! //日付ごとの予定を格納する配列を作成
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.titleLabel.isHidden = true //Calendar Cellのデフォルトの日付を消す
        self.loadTableView()
        
    }

    //TableViewの初期設定を関数にまとめました。
    func loadTableView() {
        self.eventTableView.delegate = self
        self.eventTableView.dataSource = self
        self.eventTableView.register(UINib(nibName: "CalendarEventTableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
        self.eventTableView.rowHeight = 20
        self.eventTableView.allowsSelection = false //TableViewセルを選択不可にする
        self.eventTableView.separatorStyle = .none
        self.eventTableView.reloadData()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.eventList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CalendarEventTableViewCell
        cell.eventLabel.text = eventList[indexPath.row].taskname
        return cell
    }
    
}
```

ちょっとしたポイントとして、self.eventTableView.allowsSelection = false でTableViewCellを選択不可にすることがあります。
セルを選択できてしまうと、日付を選択しようとした時にTableViewのセルが反応してしまい鬱陶しいので...

TableViewに表示する内容は`var eventList: [TaskObject]!`という配列でFSCalendarを置いているViewControlllerから受け取ります。今回は省略しますが、日々の予定をTaskObjectというStructで作りました。Structについて詳しくは[こちら](https://qiita.com/yuinchirn/items/98b568d595650eca3334)から。
```Swift
struct TaskObject{
    let taskname:String
    let deadline:Date
    let uid:String
    let finished:Bool
}
```

## 5. CalendarViewControllerのコードを書く
基本的なFSCalendarの設定は終わっている前提で、カスタムの CalendarCellを登録するにあたって特筆すべき点だけ書きます！

```Swift: CalendarViewController
override func viewDidLoad() {
        super.viewDidLoad()
        
        setCalendar()
    }
    func setCalendar(){
        self.calendar.dataSource = self
        self.calendar.delegate = self
        self.calendar.collectionView.register(UINib(nibName: "CustomCalendarCell", bundle: nil),forCellWithReuseIdentifier: "cell") //Calendar カスタムセルを登録する
        calendar.rowHeight = 75 
        calendar.headerHeight = 20
        calendar.appearance.selectionColor = .clear //選択した日付の色が変わるのを防ぐ（透明にする）
        calendar.reloadData()
    }
```
基本的にカスタムセルを登録する方法はTableViewやCollectionViewと同じです！
FSCalendarの日付選択を無効化することはできなさそうなので、`calendar.appearance.selectionColor = .clear`で選択した際の色を透明にして見かけ上選択していないようにしています。

また、同様にセルの選択も瞬時に解除されるように以下コードを加えました。

```Swift: CalendarViewController
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        calendar.deselect(date)
    }
```

最後にCalendarCellの中身を書きます。

```Swift: CalendarViewController
func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {
        let cell = calendar.dequeueReusableCell(withIdentifier: "cell", for: date, at: position) as! CustomCalendarCell
        cell.dateLabel.text = String(getDay(date).2)
        let tasksForDate = task.filter { task in
                Calendar.current.isDate(task.deadline, inSameDayAs: date)
            }
            
        cell.eventList = tasksForDate
        
        return cell
    }
```
各セルの日付と、その日付に紐づく予定をCalendarCellに渡しています。TableViewCellやCollectionViewCellと同じですね！
```Swift
cell.dateLabel.text = String(getDay(date).2)
```
で、各セルの日付を取得し自分で作った日付のラベルに表示しています。
参考までに、getDayはこのように実装しています。
```Swift: CalendarViewController
func getDay(_ date:Date) -> (Int,Int,Int){
        let tmpCalendar = Calendar(identifier: .gregorian)
        let year = tmpCalendar.component(.year, from: date)
        let month = tmpCalendar.component(.month, from: date)
        let day = tmpCalendar.component(.day, from: date)
        return (year,month,day)
    }
```

次に、読み込んだ予定を日付でフィルタリングします。
```Swift: CalendarViewController
let tasksForDate = task.filter { task in
                Calendar.current.isDate(task.deadline, inSameDayAs: date)
    }
            
cell.eventList = tasksForDate
```
絞り込んだ予定をCalendarCellに送り、CalendarCellからさらにTableViewCellに送られて表示されるというわけです！

## まとめ
意外と簡単でしたね！CustomCellの使い方に慣れていればできそうです。