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.

4.7 (26) · 3 Reviews

Download materials
Save for later
Share

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! :]

Note: This tutorial assumes you know the basics of 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 task list inside of the Checkmate app. One task shown: Complete the Diffable Data Sources tutorial on raywenderlich.com

The app shows a list of tasks. Tap Complete the Diffable Data Sources tutorial on raywenderlich.com.

Task screen in the Checkmate app

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:

Sections of the calendar picker with the header view at the top, the month view in the middle and the footer view at the bottom.

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:

  1. Constrain the collection view’s leading (left) and trailing (right) edges to the view’s readable content guide’s leading and trailing edges.
  2. Vertically center the collection view within the view controller, shifted down by 10 points.
  3. Set the height of the collection view to be half of the view controller’s height.

Build and run. Open the calendar picker, and…

Square collection view with a white background overlaid on a dimmed item detail controller

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:

  1. Date represents a given day in a month.
  2. The number to display on the collection view cell.
  3. Keeps track of whether this date is selected.
  4. 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 Dates.

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.

Note: The only countries that do not use the Gregorian calendar are Ethiopia, Nepal, Iran and Afghanistan. Therefore, if your app targets users in one of these countries, you might need to adapt this tutorial for their calendar system.

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:

  1. First, you define a method named monthMetadata(for:), which accepts a Date and returns MonthMetadata. MonthMetadata already exists in the project, so there’s no need to create it.
  2. You ask the calendar for the number of days in baseDate‘s month, then you get the first day of that month.
  3. Both of the previous calls return optional values. If either returns nil, the code throws an error and returns.
  4. 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.
  5. 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.