A Multi-Date Picker for SwiftUI

The information presented here is now obsolete with the introduction of iOS 16 and SwiftUI 4.0.

SwiftUI MultidatePicker


Many applications have the need to pick a date. But what if you have an app that requires more than one date to be selected by the user? Perhaps you are writing a hotel reservation system or an employee scheduler and need the user to select starting and ending dates or just all the days they will be working this month.

MultiDatePicker

To help facilitate that task, I’ve written the MultiDatePicker, a handy component that can deliver a single date, a collection of dates, or a date range, to your application. You can see screen shots below.

The MultiDatePicker is pretty easy to use. You first need to decide which selection mode you need: a single day, a collection of days, or a range. You create a @State var to hold the value and pass that to one of the MultiDatePicker’s constructors. When the user makes their selection your binding will be updated.

Source for MultiDatePicker can be found in my GitHub Library

Here are some examples:

Single Date

@State var selectedDate = Date()
MultiDatePicker(singleDay: self.$selectedDate)

In singleDay selection mode, the MultiDatePicker acts similarly to the SwiftUI DatePicker component. Unlike the SwiftUI component, MultiDatePicker can only select dates, not times.

The calendar in the control shows the month/year of the singleDay given.

Screen Shot 2020-11-09 at 8.23.08 AM.png

Collection of Dates

@State var anyDays = [Date]()
MultiDatePicker(anyDays: self.$anyDays)

With the anyDays selection mode, the binding contains the user’s selected day sorted in ascending order. If the selection order matters, you should write a custom binding to intercept the changes or use the onChanged modifier.

Tapping a day will select it and add it to the collection; tapping the day again will de-select it and remove it from the collection.

The calendar in the control will show the month/year of the first date in the anyDays array. If the array is empty the calendar will show the current month and year.

Screen Shot 2020-11-09 at 8.23.33 AM.png

Date Range

@State var dateRange: ClosedRange<Date>? = nil
MultiDatePicker(dateRange: self.$dateRange)

With the dateRange selection mode, the binding is not changed until the user selects two dates (the order does not matter, the MultiDatePicker will sort them). For example, if the user picks two dates, the binding will have that range. If the user then taps a third date, the binding reverts to nil until the user taps a fourth date.

The calendar in the control shows the month/year of the first date in the range given. If the dateRange is nil the control shows the current month and year.

Screen Shot 2020-11-09 at 8.23.54 AM.png

The Model

Making all of this work is the model: the MDPModel. As with most SwiftUI components, the views are data driven with a model containing the data. Often you’ll have a service (a connection to a remove system or to something outside the app) that deposits information into a model which then causes the view to react to the change in the data and refresh itself with the changes.

In the case of the MultiDatePicker, there is no service, instead the model creates the data. And the data for the model are the days of the month in the calendar displayed. My example of MultiDatePicker shows how preparing the data up front can make writing the UI easier.

The model works from the control date - any Date will do, as only its month and year are important. Once the control date is set, the model constructs a calendar - an array of MDPDayOfMonth instances that represent each day. Days that are not displayed are giving a day number of zero (these are days before the beginning of the month and after the last day of the month). As the MDPDayOfMonth items are created, the model determines if each is selectable (see below), if one represents “today”, and what its actual Date is so that comparisons at runtime can be efficient.

Once the days have been created they are set into a @Published var which triggers the UI to refresh itself, displaying the calendar. The UI can then quickly examine each MDPDayOfMonth to see how it should appear (eg, draw a circle around “today” or highlight a selected day).

More Options

In addition to the selection mode, MultiDatePicker has other parameters, all of which have defaults.

Selectable Days

If you look at the Any Dates screen shot you can see that the weekend days are gray. These days are not selectable. The MultiDatePicker can be told which days are eligible for selection by using the includeDays parameter with a value of allDays (the default), weekendsOnly, or weekdaysOnly (shown in the screen shot).

Excluding Days

Other options you can use with MultiDatePicker are minDate and maxDate (both default to nil). If used, any dates before minDate or after maxDate are not eligible for selection and shown in gray.

Jumping Ahead (or Back)

The increment (>) and decrement (<) controls move the calendar forward or backward one month. If the user wants to skip to a specific month and year, they can tap on the month and year (which is a button) to bring up the month/year picker, as shown in the last screen shot.

The month/year picker replaces the calendar with two wheels to select a month and a year. Once the user has done that, they tap the month/year button again and the calendar returns, showing the newly selected month/year combination.

Screen Shot 2020-11-09 at 8.24.13 AM.png
Previous
Previous

A SwiftUI Sidebar

Next
Next

Weather Demo for SwiftUI