Creating a Custom Calendar Control for iOS
In this calendar UI control tutorial, you’ll build an iOS control that gives users vital clarity and context when interacting with dates. By Jordan Osterberg.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating a Custom Calendar Control for iOS
25 mins
- Getting Started
- Breaking Down the UI
- Creating the Month View
- Setting up the Collection View
- Breaking Down the Data
- Using the Calendar API
- Generating a Month’s Metadata
- Looping Through the Month
- Handling the Last Week of the Month
- Creating the Collection View Cell
- Setting the Cell’s Constraints
- Configuring the Cell’s Appearance
- Preparing the Month View for Data
- Adding UICollectionViewDelegateFlowLayout Conformance
- Presenting the Calendar
- Adding the Header and Footer
- Where to Go From Here?
Providing users with a way of selecting dates is a common functionality in mobile apps. Sometimes, using the built-in UIDatePicker
is all you need, but what if you want something more customized?
Although UIDatePicker
works well for basic tasks, it lacks important context for guiding users to the date they want to select. In daily life, you use a calendar to keep track of dates. A calendar provides more context than UIDatePicker
, as it tells you what day of the week a date falls on.
In this tutorial, you’ll build a custom calendar UI control that adds that vital clarity and context when selecting dates. In the process, you’ll learn how to:
- Generate and manipulate dates using
Calendar
APIs provided by the Foundation framework. - Display the data in a
UICollectionView
. - Make the control accessible to assistive technologies such as VoiceOver.
Are you ready? Then jump right in! :]
UICollectionView
. If you’re new to iOS development, check out our UICollectionView Tutorial: Getting Started first.Getting Started
Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial.
The sample project, Checkmate, outlines a Reminders-like checklist app that allows a user to create tasks and set their due dates.
Open the starter project. Then, build and run.
The app shows a list of tasks. Tap Complete the Diffable Data Sources tutorial on raywenderlich.com.
A details screen opens, showing the task’s name and due date.
Tap Due Date. Right now nothing happens, but soon, tapping here will present your calendar control.
Breaking Down the UI
Here’s a screenshot of what the completed control will look like:
Three components make up the calendar control:
- Green, Header view: Shows the current month and year, allows the user to close the picker and displays the weekday labels.
- Blue, Month view: Displays the days of the month, along with the currently-selected date.
- Pink, Footer view: Allows the user to select different months.
You’ll start by tackling the month view.
Creating the Month View
Open CalendarPickerViewController.swift inside the CalendarPicker folder.
The file currently contains dimmedBackgroundView
, a transparent black view used to elevate the calendar picker from the background, as well as other boilerplate code.
First, create a UICollectionView
. Below the definition of dimmedBackgroundView
, enter the following code:
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.isScrollEnabled = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
In the code above, you create a collection view with no spacing between its cells and disable its scrolling. You also disable automatic translation of its auto-resizing mask into constraints, since you’ll create your own constraints for it.
Great! Next, you’ll add the collection view to the view hierarchy and set up its Auto Layout constraints.
Setting up the Collection View
Inside viewDidLoad()
and below super.viewDidLoad()
, set the background color of the collection view:
collectionView.backgroundColor = .systemGroupedBackground
Next, below view.addSubview(dimmedBackgroundView)
, add the collection view to the view hierarchy:
view.addSubview(collectionView)
Finally, you’ll give it four constraints. Add the following constraints just before the existing call to NSLayoutConstraint.activate(_:)
:
constraints.append(contentsOf: [
//1
collectionView.leadingAnchor.constraint(
equalTo: view.readableContentGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(
equalTo: view.readableContentGuide.trailingAnchor),
//2
collectionView.centerYAnchor.constraint(
equalTo: view.centerYAnchor,
constant: 10),
//3
collectionView.heightAnchor.constraint(
equalTo: view.heightAnchor,
multiplier: 0.5)
])
This looks complex, but don’t panic! Using this code, you:
- Constrain the collection view’s leading (left) and trailing (right) edges to the view’s readable content guide’s leading and trailing edges.
- Vertically center the collection view within the view controller, shifted down by 10 points.
- Set the height of the collection view to be half of the view controller’s height.
Build and run. Open the calendar picker, and…
Success! Well… kinda. There isn’t much to see right now, but you’ll solve that later, when you generate data for the calendar.
Breaking Down the Data
To display a month, you’ll need a list of days. Create a new file in the Models folder named Day.swift.
Inside this file, enter the following code:
struct Day {
// 1
let date: Date
// 2
let number: String
// 3
let isSelected: Bool
// 4
let isWithinDisplayedMonth: Bool
}
So what are these properties for? Here’s what each does:
- Date represents a given day in a month.
- The number to display on the collection view cell.
- Keeps track of whether this date is selected.
- Tracks if this date is within the currently-viewed month.
Great, you can now use your data model!
Using the Calendar API
The next step is to get a reference to a Calendar
. The Calendar API, which is part of Foundation, allows you to change and access information on Date
s.
Open CalendarPickerViewController.swift. Below selectedDateChanged
, create a new Calendar
:
private let calendar = Calendar(identifier: .gregorian)
Setting the calendar identifier as .gregorian
means the Calendar API should use the Gregorian calendar. The Gregorian is the calendar used most in the world, including by Apple’s Calendar app.
Now that you have a calendar object, it’s time to use it to generate some data.
Generating a Month’s Metadata
At the bottom of CalendarPickerViewController.swift, add this private extension:
// MARK: - Day Generation
private extension CalendarPickerViewController {
// 1
func monthMetadata(for baseDate: Date) throws -> MonthMetadata {
// 2
guard
let numberOfDaysInMonth = calendar.range(
of: .day,
in: .month,
for: baseDate)?.count,
let firstDayOfMonth = calendar.date(
from: calendar.dateComponents([.year, .month], from: baseDate))
else {
// 3
throw CalendarDataError.metadataGeneration
}
// 4
let firstDayWeekday = calendar.component(.weekday, from: firstDayOfMonth)
// 5
return MonthMetadata(
numberOfDays: numberOfDaysInMonth,
firstDay: firstDayOfMonth,
firstDayWeekday: firstDayWeekday)
}
enum CalendarDataError: Error {
case metadataGeneration
}
}
Now, to break this code down:
- First, you define a method named
monthMetadata(for:)
, which accepts aDate
and returnsMonthMetadata
.MonthMetadata
already exists in the project, so there’s no need to create it. - You ask the calendar for the number of days in
baseDate
‘s month, then you get the first day of that month. - Both of the previous calls return optional values. If either returns
nil
, the code throws an error and returns. - You get the weekday value, a number between one and seven that represents which day of the week the first day of the month falls on.
- Finally, you use these values to create an instance of
MonthMetadata
and return it.
Great! This metadata gives you all the information you need to generate the days of the month, plus a bit extra.