Skip to content

Instantly share code, notes, and snippets.

@agibson73
Last active February 23, 2019 02:41
Show Gist options
  • Save agibson73/8f76d4a9d4381e432a4e94af7f01dd4e to your computer and use it in GitHub Desktop.
Save agibson73/8f76d4a9d4381e432a4e94af7f01dd4e to your computer and use it in GitHub Desktop.
import Cocoa
class ViewController: NSViewController,NSCollectionViewDelegate,NSCollectionViewDataSource,NSCollectionViewDelegateFlowLayout {
private let concurrentQueue = DispatchQueue(label: "calendarQueue", attributes: .concurrent)
//next 2 variables or speed and smoothness in loading
private var currentSection : Int!
private var currentOffset = 0
private let calendar = Calendar.current
// var because we make some alterations on load
private var today = Date()
//default values but can be changed on start
var endDate = Date(timeIntervalSinceNow: 3153600000)
var startDate = Date(timeIntervalSince1970: 0)
//this is an array of dates for you to do your decorations
var dates = [Date]()
// when true the calendar will scroll to today
var scrollToToday = true
//our collectionview
@IBOutlet weak var collectionView: NSCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
normalizeToday()
configureCollectionView()
// on ios this is not needed but at the moment i don't know how to fix resizing on mac
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSWindowWillStartLiveResize, object: nil, queue: OperationQueue.main, using: {
notificaiton in
self.collectionView.collectionViewLayout?.invalidateLayout()
})
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSWindowDidResize, object: nil, queue: OperationQueue.main, using: {
notificaiton in
self.collectionView.collectionViewLayout?.invalidateLayout()
})
}
// could be unused
func setStartAndEndDate(startDate:Date?,endDate:Date?){
if startDate != nil{
self.startDate = startDate!
}
if endDate == nil{
self.endDate = endDate!
}
}
func normalizeToday(){
let dt = Date()
let dtComps = calendar.dateComponents([.year,.month,.day], from: dt)
today = calendar.date(from: dtComps)!
}
private func configureCollectionView() {
collectionView.layer?.backgroundColor = NSColor.white.cgColor
collectionView.register(NSNib(nibNamed: "DayCell", bundle: nil), forItemWithIdentifier: "dayCell")
collectionView.delegate = self
collectionView.dataSource = self
let boxSize = max(collectionView!.superview!.bounds.width, collectionView!.superview!.bounds.height)
let size = boxSize/7
let layout = NSCollectionViewFlowLayout()
layout.itemSize = NSSize(width: size, height: size)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.headerReferenceSize = NSSize(width: self.collectionView.bounds.width, height: 100)
collectionView.maxNumberOfColumns = 7
collectionView.collectionViewLayout = layout
view.wantsLayer = true
}
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return self.numberOfMonthsToLoad(startDate, endDate: endDate)
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
let offset = self.getShiftInSection(section)
let totalDays = self.daysInSection(section)
return offset + totalDays
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let cell = collectionView.makeItem(withIdentifier: "dayCell", for: indexPath) as! DayCell
var day : Date?
if (indexPath as NSIndexPath).item >= self.getShiftInSection((indexPath as NSIndexPath).section){
// special background on today only
self.dateForIndexPath(indexPath, completion: { (date) in
DispatchQueue.main.async {
day = date
if self.isToday(day!) == true{
cell.view.layer?.backgroundColor = NSColor.green.cgColor
}else{
cell.view.layer?.backgroundColor = NSColor.white.cgColor
}
let components = self.calendar.component(.day, from: day!)
cell.dayNumberLabel.stringValue = "\(components)"
}
})
}else{
//style for blank cell
cell.dayNumberLabel.stringValue = ""
}
cell.view.layer?.shouldRasterize = true
return cell
}
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
let size = collectionView.bounds.width/7
return NSSize(width: size, height: size)
}
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> NSView {
let header = collectionView.makeSupplementaryView(ofKind: NSCollectionElementKindSectionHeader, withIdentifier: "MonthHeader", for: indexPath) as! MonthHeader
header.bounds.size = NSSize(width: collectionView.bounds.width, height: 100)
let formatter = DateFormatter()
formatter.dateFormat = "MMM YYYY"
header.monthLabel.stringValue = formatter.string(from: self.getMonthAsDateForSection((indexPath as NSIndexPath).section))
//print("The header size is \(header.bounds.height)")
return header
}
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
return CGSize(width: 0, height: 100)
}
func scrollToDate(date:Date){
let indexPath = indexPathFor(date: date)
collectionView.scrollToItems(at: [indexPath], scrollPosition: .centeredVertically)
}
override func viewDidLayout() {
super.viewDidLayout()
if scrollToToday == true{
self.scrollToDate(date: self.today)
}
}
// MARK : CollectionView and DateHelpers
func dateForIndexPath(_ indexPath : IndexPath,completion:(_ date:Date)->Void){
concurrentQueue.sync {
let date = self.monthInSection((indexPath as NSIndexPath).section)
var yesComponents = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.day], from: date)
yesComponents.day = (indexPath as NSIndexPath).item - self.getShiftInSection((indexPath as NSIndexPath).section) + 1
let newDate = calendar.date(from: yesComponents)
completion(newDate!)
}
}
func getShiftInSection(_ section : Int )->Int{
var offset = 0
// we hang on to some of this info to speed up the load
// that's why we check this
if currentSection == nil || section != currentSection{
let sectionDate : Date = Calendar.current.date(byAdding: .month, value: section, to: endDate)!
var oldComp = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.weekday], from: sectionDate)
oldComp.day = 1
let newDay = calendar.date(from: oldComp)!
let newComponents = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.weekday], from: newDay )
offset = newComponents.weekday! - 1
currentSection = section
self.currentOffset = offset
}else{
offset = self.currentOffset
}
// adjust offset
if offset == 7{
return 0
}else if offset > 7{
return offset - 7
}else{
return offset
}
}
func getMonthForSection(_ dateIndex : Int)->Int{
var comp = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.day], from: startDate)
let monthSection = comp.month ?? 0 + dateIndex
return monthSection
}
func monthInSection(_ dateIndex: Int)->Date{
var comp = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.day], from: startDate)
comp.month = (comp.month ?? 0) + dateIndex
return calendar.date(from: comp)!
}
func daysInSection(_ section :Int)->Int{
var componets = (calendar as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month, NSCalendar.Unit.weekday], from: startDate)
componets.month = componets.month! - section
let date = calendar.date(from: componets)
return self.numberOfDaysInMonthFrom(date: date!)
}
func getMonthAsDateForSection(_ dateIndex : Int)->Date{
var comp = calendar.dateComponents([.year,.month,.day], from: startDate)
comp.month = comp.month! + dateIndex
return calendar.date(from: comp)!
}
func isToday(_ date : Date)-> Bool{
if date == self.today{
return true
}else{
return false
}
}
func doesDatesContainDate(_ date : Date)->Bool{
if dates.contains(date){
return true
}else{
return false
}
}
func numberOfMonthsToLoad(_ startDate : Date, endDate : Date)->Int{
//start and end date and we load at least 1
let time = Calendar.current.dateComponents([.month], from: startDate, to: endDate).month
if let tm = time{
return tm + 1
}else{
return 1
}
}
func numberOfDaysInMonthFrom(date:Date)->Int{
return calendar.range(of: .day, in: .month, for: date)!.count
}
func indexPathFor(date:Date)->IndexPath{
let section = calendar.dateComponents([.month], from: startDate, to: date).month ?? 0
return IndexPath(item: 0, section: section + 1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment