Let’s create a Calendar supporting EventKit using a UICollectionView!

(Code on GitHub – Built with XCode 7.0)

It amazes me how Apple has not provided a standard and customisable Calendar component for its UI but then again it is an opportunity to create one. While coding I also took Swift for a spin and explored lazy loading, generics and closures.

cal

Like always there are many ways of doing it but I chose the most compact which is to use a single collection view and a custom layout.

My rationale followed that of the native UICollectionView, where the view is responsible for rendering the data taken from another object that conforms to two protocols, one for data provision the other for interactivity.

Internally the calendar will cache the data taken from the delegate so that it does not make unnecessary function calls every time a section or even cell is rendered. As the calendar is multi-date selectable we need to store the indexPaths and dates that have been selected at any given point. We need to be able to access the selected data externally but set it only internally hence the private(set) directive. Note: I initialised all these variables so as not to make them optionals having to wrap them in if-let statements.

The calendar will consist of two views: a calendarView which will render the days and a headerView which will display the month and year string as well as a series of labels to indicate the days of the week. Both are created through the use of lazy loading, a feature of Swift that was previously implemented through custom getters in Objective-C.

Note: the allowsMultipleSelection variable changes the behaviour of the selection/deselection of cells massively. You can look up at Apple’s documentation for details.

The header and collection view’s frames need to be laid out with that of the calendar component’s, so I will override the frame using Swift’s didSet block. This is a good place to calculate the size of each (day) cell since it is a division of the overall frame.

A few words about dates

Dates can be a pain. This is mainly because while time is an objective measure and a scalar value, a month is a human construct that is variable in size. Think of the concept of increment a date by a month. What does this mean exactly? If we are on the 1rst of January we would expect to get the 1rst of February and if we repeat from there, the 1rst of March. In the first case however we advanced by more days that in the second since February has less than 30 days. Furthermore, if we are on the 30th of January how would we calculate a month in the future? Would that be the 2nd of March (where we would miss my birthday) of the 28th of February? Turns out that whichever way you do it is as correct and as wrong as the other and Apple has opted out of a best guess strategy where the 30th of January becomes the 28th or 29th of February.

To add to the complication we have time zones and daylight savings which can even change through political decisions! So we need a strategy to deal with them.

The first step is to realise that NSDate is essentially a wrapper around a timestamp that is always UTC (or GMT, the two being equal for what we are concerned). It does not carry any timezone information so if we ever try to increment its value to match a different area it will be impossible to know it. So, what I recommend is to consider the dates as always being in GTM and transforming them at the last moment into the local time as part of a “view.”

We need therefore a gregorian calendar in GTM

Getting the Start and End of the Calendar

And now for the fun part. We want every month to be a section and every day a cell. We call the delegate objects startDate and endDate methods and cache them in the class’s local variables, we then calculate the first of the month to use as an offset where we will increment it by a month to create the calendar.

We do all that in the numberOfSectionsInCollectionView. To do that This is a restriction that we must accept with the approach explained above (where instead of a view controller I created a view).

To render each month we basically need to know 2 things. The weekday the month starts and the number of days it has.

Using the power of Swift we use a typed dictionary called monthInfo witch will hold these 2 values.

Calendar Overview

We make the calculations on the UICollectionViewDelegate’s method which gets called for each section (month). One thing we need to notice is that the default NSCalendar has weekday 0 being a Sunday rather than a Monday. Although we could keep it like that we are going to shift the days to the more familiar Monday-indexed week.

The magic number 42 is the maximum numbers of cells that a month can need to display. It is the 7 days of the week times the 6 rows. Finally we render each cell. Since all 42 cells are going to be rendered we only need to check if the index path to be between the start and the last day of the month.

The only point of shame in the code above is the last conditional which calls the scrollViewDidEndDecelerating on the first cell. This is to give an initial notification to the delegate and set the month label inside the calendar view. If we where to use a UIViewController instead of a UIView as the KDCalendar’s superclass we might be able to do that on viewWillAppear but I stand by my choice nonetheless 😉

There is a point we need to watch for. When we created our layout above we defined our scroll direction to be:

This has a side effect that it will render cells upside down (which makes sense). In other words the second cell will be under the first rather than to the left, as in a calendar.

Direction

To fix that we need to create a custom layout and override its methods. When subclassing a UICollectionViewFlowLayout we need to account for 2 methods that get called to do the same things in different occasions. In contrast with Obj-C we can use the power of closures to write the loop in one line

The applyLayoutAttributes method will use the section and index to calculate the page and the position of the cell respectively. The base offset multiplying the section number by the size of the collection view bounds

CalendarLayout

That should give you the basic calendar.

Lastly we need a header to show an indication of the dates as well as the current month we are viewing. What is interesting is that the NSDateFormatter object has a list of the weekday and months localised so it is wise to use that. Inside the header I created a container view to place the UILabels. The format is “MO”, “TU” etc. which is created by taking the first 2 letters of the day string and capitalising it. Once again the list of days starts from Sunday so we need to loop from 1 to 7 rather than 0 to 6 and pass the value from a mod 7 so that 7 will become 0.

We need to update the month label in the header when the user scrolls to a new month as well as at the beginning (as shown in the cellForIndexPath method). This is done in the standard UIScrollViewDelegate’s callback method.

Selecting Dates

When a user presses on a cell we want to add it to a list together with the date displayed on that cell. First we check with the calendar’s delegate as to whether we can select the date.

Here I used an anti-pattern which is to save a variable (dateBeingSelectedByUser) which I just know will be used in the next function. This is because I just do not want to re-calculate the date via the indexPath offset so I trust the OS to call its methods in the proper order. If the function returns true we move on to the select/deselect methods.

The selected variable of the cell will be set automatically by the system so we only need to override it in our custom cell. In this case we only change the border width of the layer

Adding EventKit Functionality

The title said that we are going to add EventKit and that is what we are going to do. First of course we need to import it in the project

eventkit

We need to call the OS to give us the events stored in the system but first we need to ask for permission. If the user agrees we will run our function as below.

We pass the events array to the calendar where the setter will take care of storing and displaying the events. We store an array of events for every indexPath that has events

We then modify the cellForItemAtIndexPath function to check our newly create dictionary and add a dot for every event (up to 3).

 

For more tutorials on iOS

[catlist name=”iOS”]

 

88 thoughts on “Let’s create a Calendar supporting EventKit using a UICollectionView!

  1. Hi Michael,

    Thanks for posting this excellent tutorial. I am learning Swift and EventKit by creating a simple app to display a month calendar and allow the user to select a date. Your tutorial is exactly aligned with what I am trying to do. I downloaded the sample and tried to run it in Xcode 7 beta with Swift 2; but it is throwing some errors due to the modified Swift syntax. The errors are in the KDCalendarFlowLayout.swift and KDCalendarTests areas. Do you by chance have an updated version that compiles with Swift 2?

    Thanks again for this excellent tutorial,

    Donovan

      1. Hi Michael,

        Thank you so much for the tutorial.

        I’m using this calendar in my project, But i want to display the previous month and next month dates in the current month.I’m trying modify your code but unable to display prev,next dates.

        Kindly help me with my requirements.

  2. Hello,
    really nice tutorial, but I would like to make a vertical calendar and I cannot change the code to get it. Do you have any tips?

    1. I refactored the code so that you can pass a ‘direction’ property on the calendarView and have it either vertical or horizontal. It is a small difference in the calculations. So, pull from the master branch and enjoy!

  3. Hi Michael,

    That is an amazing tutorial and piece of code ! Congrats !

    I changed your code a bit to reflect the usage in my app.
    Now I am trying to change the KDCalendarFlowLayout to add a header to each section.

    The problem is that I cannot find a proper way to do that. Would you have some tips ?

    Thanks you again !

  4. Your calendar is awesome, but I’m having some problems with the beginning and the end of which month. Exemple: one month ends on Sunday and the next month begin on Friday. And when it shows today, shows a day from last month like today is 23 but it is mark august 23. Do you know what could be wrong in both cases?

    1. Thanks for the comments. There was indeed a bug as I was getting the names of the months wrong. This was because I was counting from the first of the year rather than the start date set by the delegate! I did not notice it before due to the particular date I developed the tutorial: It was January and both first of year AND first of start date where the same… Amazing! hahahaha 😉

      I pushed an update which should be fixing it. I had to convert everything to XCode 7.0 since it is on a stable release now so beware…

      1. I’m sorry but I’m still having this problem, I really don’t know if is some config of my phone that do this. What do you think might be causing this? Thank you very much!

      2. I managed to get the day started on the right weekday and follow the calendar week but the day is still displaying wrong but the message on the console is correct. Could you tell me how you informs to the dayCell today? Thank you and sorry for my bad English.

  5. Hi Michael,

    first of all thank you very much for this tutorial. I have a question for the Objective C version that I found here: https://github.com/mmick66/KDCalendarView

    When I try to implement it like this:

    KDCalendarView* calendarView = [[KDCalendarView alloc] initWithFrame:CGRectMake(0, 0, 150, 200)];
    calendarView.delegate = self;
    calendarView.dataSource = self;
    [self.view addSubview:calendarView];

    With the appropriate source and delegate methods, I get a EXC_BAD_ACCESS message with no error at all. Just the usual (lldb).

    Do you have any idea how to resolve this?

    Thanks a lot!

  6. Thanks so much for posting this, Michael! BTW I completely agree with you on this “It amazes me how Apple has not provided a standard and customisable Calendar component for its UI …” But in the meantime, your tutorial is incredibly helpful, as I am new to iOS.

    I’m also curious to get your take on this: I am working on an app that allows a user to schedule future events and also mark events due as Done. Events may be delegated to others, so I’m envisioning some sort of shared calendar. However, I want to support both iOS and Android devices. What do you think is the best strategy for managing such a calendar? I would rather not create and manage my own calendar just for this app, so hopefully that’s not the only solution…

  7. Just starting to use KDCalendar and have noticed that the events showing in the calendar seem to be offset depending on what the start day of the month is therefore not appearing in the correct spot.

    So for example, I have event for 18th Jan 2016 and it’s showing in the calendar on 13th Jan, I have another event for 19th Jan 2016 and it’s showing in the calendar on 14th Jan.

    Any ideas? Has anyone else seen this as well?

  8. Micheal! I found a bug when I tried to add another event to the calendar – the calendar was displaying the dates wrong.

    Let say I tried to add an event for today (17th of Jan 2016) – I did it but it displayed the event dot 4 days earlier (so exactly the number of the offect (We do not have the first 4 cell for January 2016 as it starts on Friday)).

    I added:
    let numberToAdd = self.monthInfo[distanceFromStartComponent.month]![0]
    in var events didSet in KDCalendarView and then changed this:

    let indexPath = NSIndexPath(forItem: distanceFromStartComponent.day, inSection: distanceFromStartComponent.month)

    to that:

    let indexPath = NSIndexPath(forItem: distanceFromStartComponent.day + numberToAdd, inSection: distanceFromStartComponent.month)

    Seems to resolve the problem!

  9. Hi Michael,
    Great post, can u tell me, how can i set the calendar, show me today and not the last 3 months.
    I tried to change this line.
    “…
    dateComponents.month = -3
    ..”
    to


    dateComponents.month = 0


    but the problem is … i can scroll the last month … only after this month …
    Ty in advance.

  10. With help from your code tutorial I have created a highly configurable Calendar control for my project. Couldn’t have done this without your clear documentation on this link. Will add a link to your blog and recognize your help in my documentation on next patch. I decided to post my code since it was so useful. If you use Cocoapods you can find it here -> https://cocoapods.org/pods/JTAppleCalendar

    Currently it has no events, yet. I’m thinking of the best way to add it from the framework side. Currently working on other features for it as well. Thank a mil.

    1. Thank you for your kind words! I am sure you can create something better as I have stopped updating the repository due to lack of time. I think that currently I have at least a basic implementation of events in my code base, so you might as well have a look there.

      1. Your mention was added at the top of the documentation. Thanks again.

        There is one last error i am trying to resolve with KDCalendar btw. In your flowLayout, you have some code in there which makes the cells on the calendarView render horizontally instead of vertically. This works perfectly fine. But for some odd reason, whenever the calendar view is placed into a UIStackView, the cellForItemAtIndexPath doesnt get called at the right time only for some cells (not all). I thought this was a bug that was introduced when i messed with the code, but I see it is also present in your original code. I have placed a question about it here –> https://stackoverflow.com/questions/36412116/bug-with-uicollectionview-cellforitematindexpath-is-not-called-at-the-correct-t

        I am currently looking into why this situation could happen because every thing “seems” fine.

  11. Great tutorial! Its happy to learn this tutorial. I’m trying to make the calendar scrolled to the month of the current date. May i know how to achieve it? Thanks a lot Michael!

    1. I have just updated the code with your request. It does not go to today’s date but I have added the ability to animate into any date, so it should require a line of code to implement what you want.

      The method is on the CalendarView and is called:
      func setDisplayDate(date : NSDate, animated: Bool) -> Void

  12. Dear Michael,
    Thank you for this great tutorial.

    I am trying to implement in your code a UICollectionReusableView as a header that repeats in every section.
    Unfortunately, the collectionView does not appear and the debugger says that “negative or zero item sizes are not supported in the flow layout”. (In the FlowLayout I just changed the func from layoutAttributesForElementsInRect to layoutAttributesForSupplementaryViewOfKind and implemented a header).

    Do you have an idea what my mistake could be?
    Thank you in advance for your help!!

    1. Unfortunately I tried exactly the same (makes sense right?) back when I started the project and bumped into the exact same problem as you. I do not know what is happening there. If you manage to solve it I would love to have it as part of the code as I consider it more elegant.

  13. Thanks for this excellent tutorial. It’s very light and works like a charm. I have one question: If I want to select some dates programmatically, how do you suggest to approach? for example when they launch the calendar I want to preselect some dates. Right now it’s only possible if user taps on the date, but how can I programmatically do it?

  14. I’ve been looking for a light and easy calendar for a long time and I should say your calendar is the best one on internet because it is designed the way that customization is very easy and at the same time it’s less than 700 lines of code. You are genius and I appreciate your work. I hope best things happen in your life.

  15. Since you did add deselect functionality. I’ll try to add a logic for selecting dates to accept array of dates instead of accepting only one date. Same thing for deselect. I guess it should be easy, I just have to put the logic in a loop.

  16. Excuse my comments, I’m totally excited about the calendar I put inside of my app and that’s why I’m sending multiple comments here 😀

  17. One small, tiny, minor issue that I need to fix is that when the view that includes calendar (Like ViewController.swift in your example) gets navigation bar, month label will go under the navigation bar. I put different kinds of constraints to show it bellow the navigation bar but it falls under it. For now I used modal view so it won’t need navigation bar on top but eventually I should fix it.

  18. Please could you make it possible so that you can set the calendar start on a given month, say current. I would still like to page back through history and also forward but it would be good if you could select the start month.

  19. I wanted to add a delegate method similar to canSelectDate but for deSelecting, something like canDeSelectDate. I think it would be very easy to add and straight forward.

  20. Hi Michael,

    Thanks for this great tutorial !

    I started to play with the ideas that you have presented here with Objective-C.
    It made me learn a bit about UICollectionViewFlowLayout – a class which i have never heard about 🙂

    I have found that there is a known bug with this class – http://openradar.appspot.com/12433891
    Apparently it looks like this class doesn’t work well with minimumInteritemSpacing
    So maybe, it is good to mention this here in the replies.

    My question to you is how does your code takes care with this unique scrolling ?
    I could not understand from the code how it stops on every section automatically ?
    I will be thankful if you can refer me to the lines responsible for this behavior, or to another reference.

    Thanks Again !

  21. Hi Michael, I downloaded your sample code, and ran it on xcode 8 after their auto-converting. However, there are issues that makes the app clashes, it says: The app’s Info.plist must contain an NSCalendarsUsageDescription key with a string value explaining to the user how the app uses this data. Is there any way to fix this? Thanks!

    Best Regards

  22. Awesome Calendar Michael!
    Would you please tell me how to enable scrolling to past months, as i can only scroll to left “to future dates” .. but never to old dates :/

      1. Thanks for your reply
        Yes i implemented it, but when i change the start date, the initial view change also, like it shows 2015 instead of the current year

  23. Hi Michael,,
    I am working with app like user can select the multiples dates(example from today’s date to 20th of this month)…so i implemented my calander where users can select multiple dates but now i want o disable some particular dates where that dates am getting from API side…so can you help me to do perform this one…
    Thanks in advance..

  24. Micheal….
    Actually what i want means am getting some particular dates from webservice…now i want to disable that dates means user should not select that dates…(currently am woring with Yacht related application & dis stuff am implementing while displaying the calender for customers when they want to book the yacht..)
    Can you help me to complete this …

  25. Im trying to be able to reorder the dates in the calendar, so trying to make it so you can move the dates out of order but still keep them in the calendar view. I want to be able to do the reordering like, http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/ (the first example on this site). I also am wanting to replace the numbers with pictures and link each picture to a different part of the app. Do you think any of this would be possible? Got calendar view working perfectly just cant figure out the reordering yet.

  26. Hi Michael, thank you for this great tutorial. I have a problem, when change the orientation of the movil to landscape and after return to portrait some cells of collectionview don´t show correctly, the cells change the size (same size of landscape).
    Do you have any idea how to solve it.
    Thanks.

  27. how to add the need next and previous button in Calendar. if next click move to next month and IN previous click move to previous month. How it possible help me.

  28. Thank you very much for the project.
    But there is some problem when rotating device, the cell size got wrong.
    I tried to get it right, but can not figure it out.
    Do you have any suggestion?

  29. Good work !

    Just a little change to be Swift4 ok :

    //if let newDate = (self.displayDate?.applyOffSetOfMonth(calendar: self.calendar, offset: offet)){

    if let newDate = self.calendar.date(byAdding: .month, value: offet, to: self.displayDate!) {

Leave a Reply

Your email address will not be published. Required fields are marked *