A Multi-Date Picker for SwiftUI
The information presented here is now obsolete with the introduction of iOS 16 and SwiftUI 4.0.
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.
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.
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.
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.