Swiftでカレンダーを作る

今更カレンダーなんて作りたくなかったけど、ライブラリ使いたくないし意外とコードが落ちてなくて
[こちら](https://qiita.com/sakuran/items/3c2c9f22cbcbf4aff731)の記事を参考にしたが結構めちゃくちゃだったので。
自分のように最小限コピペで動かせるものが欲しい人もいると思うので共有します。


![file.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166241/436be5eb-90a3-e5f1-ee0a-ffc2269555d9.gif)

https://github.com/churabou/SwiftCalendar


### Extension
日付と色のextensionです。

``` Ex.swift

extension UIColor {
    class var lightBlue: UIColor {
        return UIColor(red: 92.0 / 255, green: 192.0 / 255, blue: 210.0 / 255, alpha: 1.0)
    }
    
    class var lightRed: UIColor {
        return UIColor(red: 195.0 / 255, green: 123.0 / 255, blue: 175.0 / 255, alpha: 1.0)
    }
}


extension Date {
    func string(format: String) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = format
        return formatter.string(from: self)
    }
}

```






### MonthDateManager

``` MonthDateManager.swift

import Foundation

final class MonthDateManager {
    
    private let calendar = Calendar.current
    private (set) var days: [Date] = []
    private var firstDate: Date! {
        didSet {
           days = createDaysForMonth()
        }
    }
    
    var monthString: String {
        return firstDate.string(format: "YYYY/MM")
    }
    
    init() {
        var component = calendar.dateComponents([.year, .month], from: Date())
        component.day = 1
        firstDate = calendar.date(from: component)
        days = createDaysForMonth()
    }
    
    func createDaysForMonth() -> [Date] {
        // 月の初日の曜日
        let dayOfTheWeek = calendar.component(.weekday, from: firstDate) - 1
        // 月の日数
        let numberOfWeeks = calendar.range(of: .weekOfMonth, in: .month, for: firstDate)!.count
        // その月に表示する日数
        let numberOfItems = numberOfWeeks * 7 

        return (0..<numberOfItems).map { i in
            var dateComponents = DateComponents()
            dateComponents.day = i - dayOfTheWeek
            return calendar.date(byAdding: dateComponents, to: firstDate)!
        }
    }
    
    func nextMonth() {
        firstDate = calendar.date(byAdding: .month, value: 1, to: firstDate)
    }
    
    func prevMonth() {
        firstDate = calendar.date(byAdding: .month, value: -1, to: firstDate)
    }
}

```



### CalendarCell

セルはラベルだけです。
土曜日は青くするといった要件はモデルの初期化に打ち込みます。

``` CalenderCell.swift

import UIKit

final class CalendarCell: UICollectionViewCell {
    
    private var label: UILabel = {
        let it = UILabel()
        it.textAlignment = .center
        it.translatesAutoresizingMaskIntoConstraints = false
        return it
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: contentView.topAnchor),
            label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            label.leftAnchor.constraint(equalTo: contentView.leftAnchor),
            label.rightAnchor.constraint(equalTo: contentView.rightAnchor),
            ])
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(model: Model) {
         label.text = model.text
         label.textColor = model.textColor
    }
}


extension CalendarCell {

    struct Model {
        var text: String = ""
        var textColor: UIColor = .black
    }
}

extension CalendarCell.Model {
    
    init(date: Date) {
        let weekday = Calendar.current.component(.weekday, from: date)
        if weekday == 1 {
            textColor = .lightRed
        } else if weekday == 7 {
            textColor = .lightBlue
        } else {
            textColor = .gray
        }
        text = date.string(format: "d")
    }
}
```



### ViewController

``` ViewController.swift

final class ViewController: UIViewController {
    
    private let dateManager = MonthDateManager()
    private let weeks = ["日","月", "火", "水", "木", "金", "土"]
    private let itemSize: CGFloat = (UIScreen.main.bounds.width - 60) / 7
    
    private lazy var calenderCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.minimumLineSpacing = 10
        layout.minimumInteritemSpacing = 10
        layout.itemSize = CGSize(width: itemSize, height: 50)
        let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.register(CalendarCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.delegate = self
        collectionView.dataSource = self
        return collectionView
    }()
    
    private func setUpNavigationBar() {
        navigationController?.navigationBar.barTintColor = UIColor(red: 255/255, green: 132/255, blue: 214/255, alpha: 1)
        navigationController?.navigationBar.tintColor = .white
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "next",
            style: .plain,
            target: self,
            action: #selector(actionNext)
        )
        
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            title: "back",
            style: .plain,
            target: self,
            action: #selector(actionBack)
        )
        title = dateManager.monthString
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        calenderCollectionView.frame.size.width = view.bounds.width
        calenderCollectionView.frame.size.height = 500
        view.addSubview(calenderCollectionView)
        setUpNavigationBar()
    }
    
    @objc private func actionNext() {
        dateManager.nextMonth()
        calenderCollectionView.reloadData()
        title = dateManager.monthString
    }
    
    @objc private func actionBack() {
        dateManager.prevMonth()
        calenderCollectionView.reloadData()
        title = dateManager.monthString
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return section == 0 ? weeks.count : dateManager.days.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CalendarCell
        if indexPath.section == 0 {
            let day = weeks[indexPath.row]
            let model = CalendarCell.Model(text: day, textColor: .black)
            cell.configure(model: model)
        } else {
            let date = dateManager.days[indexPath.row]
            cell.configure(model: CalendarCell.Model(date: date))
        }
        return cell
    }
}

extension ViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if indexPath.section == 0 {
            return
        }
        title = dateManager.days[indexPath.row].string(format: "YYYY/MM/dd")
    }
}

```

![file.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166241/eda15263-65fe-8e7e-8869-cb714f5253ba.gif)


今回は参考元の記事を尊重したコードを公開しましたが
実際には前月の日付は表示したくない、スクロールしたいとかいった要望があると思います。


にしても今更カレンダーなんて作りたくなかった。
素直にライブラリ使えばよかった。