Crafting a Data-Driven Date Picker for SwiftUI

The Swift Package for this component is available on GitHub.

The selector’s calendar view, with the kind of visual feedback I was looking for.

One of the nice things about being an app developer is that if you can’t find an app you want in the App Store, you can build it.

I’ve been wanting something specific on my phone (also accessible from my watch and Mac) to help me stay focused on my day-to-day tasks. With the release of SwiftUI 5 this year, it seemed like a great time to finally start working on it while I learned more about SwiftUI’s new features—especially the new SwiftData framework, which I’m really excited about.

Including Dark Mode support.

Since the app is highly date and time focused, a key part of the user experience will be giving users a way to select those dates and times. SwiftUI’s built-in DatePicker is nice. It provides a fair amount of flexibility over how you can configure it, including what form it takes, and what the user can “pick” (date vs. date and time, etc.). However, I was really hoping it could provide some visual feedback for what days already had activities, and could show it in a way that might indicate how “busy” each day was. The built-in picker doesn’t provide any of this, which meant I would have to build my own, if I wanted that functionality.

Fortunately, that happens to be one of my favorite things in the world to do.

New Picker Requirements

Apple’s built-in DatePicker is a good example to model the requirements for a new picker from. I particularly like that you have the choice of either embedding the picker as an expanded calendar view into your own view, or having it be a small button that opens a pop-up with a calendar to choose from. This gives you a lot of freedom over how it fits within your UI. I wanted to modeled my custom picker after some of that functionality as much as I could.

Based on that, the requirements for my new selector were as follows…

The new DateAndTimeSelector shown in various configurations.

  1. It should initially be available for iOS, iPadOS, and macOS. Support for watchOS and tvOS (and now visionOS!?) to come in later iterations.

  2. The selector should provide the option of displaying it as either a button, or as an embedded calendar view.

  3. The selector should allow for both date and/or time selection.

  4. In button form, it should open a popover for selecting dates and times making it easier to implement for iOS, iPadOS, and macOS.

  5. The calendar view should provide a way to easily navigate between months in the calendar view.

  6. There should be a way to quickly navigate back to “today” in the calendar view.

  7. The calendar view should display visual indicators for days that have activities.

  8. The visual indicator should also help indicate how busy each day is.

  9. The selector should provide a convenient data source interface to make it easy for developers to provide their own data to be visualized as described in 7 and 8.

  10. It should support both Light and Dark modes.

  11. Finally, it should be coded, organized, and packaged in a way that other developers can easily configure and use the selector in their own apps.


Building a Reusable Component

Building a software component that can be reused in other apps, by other developers, takes some special thought and care up front. I might argue all software can benefit from that same kind of thinking, but for an actual reusable software component, in particular, you want to make sure that it:

  • Is easy to install. Adding the component to your app should have as little impact on your app’s infrastructure and project configuration as possible. Plug-and-play, as they say. I never warmed up to CocoaPods for this very reason, and will usually try to avoid components that rely on it if I can. Its intentions were certainly good, but its impact on your project configuration is way too burdensome. Luckily we have the Swift Package Manager now.

  • Is easy to code at its “point-of-use”. Or, in other words, the code you write in your app to use the component should be simple to write and easy to read, with its intended behavior clear and predictable.

  • Is flexible and easy to configure. The component should be flexible to use for all of its intended use-cases, and the app using it should be able to easily configure it to do so.

  • Can be execute entirely on its own, without dependencies. Separating concerns and reducing dependencies is always an important coding goal when writing software, but it’s especially important for designing reusable software component libraries. Any dependencies a component may require should be provided through dependency injection, not coded directly into the component, and the component should be able to be dropped into an app and run as is, with default, “factory installed” behaviors and properties that can then be customized and configured by the app as needed.

  • Feels like it fits within the environment. A UI component should look and behave like it fits within the rest of the UI ecosystem, following platform design guidelines and adhering to any custom UI changes the user may have configured for their device. This includes supporting things like what the user has chosen for their system tint colors, dynamic type settings, various appearance modes (e.g. light and dark modes), and device orientation and screen size changes.

  • Is tested, stable, and nicely polished. “Polish” is a bit of a subjective term, I know, but a component should have a nice “out-of-box experience“. Or, in other words, try and aim for a few “oohs” and “ahhs” whenever developers add it to their apps.


Rendering the Calendar View

A relatively complex piece of this custom date and time selector was figuring out how to render the calendar view with SwiftUI.

Calendars are not rocket science, as they say. But the approach you take with a declarative SwiftUI view is quite different than the traditional UIView approach. You rely entirely on declaring the layout with subviews, not actually drawing or composing it at runtime. And since SwiftUI views are structs (structs can’t be subclassed in Swift), there was nothing I could extend and re-use from Apple’s existing DatePicker in my own code. I would have to write it all from scratch.

There are also some date-specific calculations you need to get right. Figuring out what date, for example, falls in the first cell of the calendar grid (i.e. the upper-left corner). Is it the first date in the current month being rendered? Most likely not. So what cell (i.e. day of the week) does the first date of the month actually fall on?

Also, how many rows (weeks) of cells are there in the month’s grid? In part, this will depend on the month and the year, but it also depends on the day of the week that the first day of the month falls on too. It could be 5 or 6 weeks (or rows) that need to be rendered for any given month, and the view needs to account for that by either always having room for 6 weeks (rows) and leaving the last row empty (not ideal) or adjusting the view’s height to 5 or 6 rows, which is what Apple’s built-in picker does, and what I wanted for mine.

There are other formatting and design considerations to think about as well. Should dates outside of the current month be rendered, for example? Or should they leave a blank space, like Apple’s built-in DatePicker displays them.

The approach I took was to use a SwiftUI ZStack and creating a cell view for each day of the month. Then I positioned each day’s cell view in the calendar grid using its .offset modifier. I used the width and height of each cell view to calculate its offset based on the number of weeks (rows) in the month grid.

The code inside buildCalendarCellView() function (being called in the snippet below) would build the cell view, deciding if the cell should be blank (outside of the current month) or contain actual visuals for the that day of the month…


ZStack {
    ForEach(Array(calendarCells.enumerated()), id: \.offset) { index, cell in
        buildCalendarCellView(cell: cell, cellWidth: cellWidth, cellHeight: cellHeight)
            .offset(offsetForIndex(index, cellWidth: cellWidth, cellHeight: cellHeight))
    }
}


Making it Data Driven

To provide data to the selector (and therefore give it the ability to show visual indicators for days that have activities) the selector allows for a DateAndTimeSelectorDataSource protocol that developers can supply it. A data source has only one function you must implement:


func getDateAndTimeData(
  starting: Date,
  ending: Date
  ) -> DateAndTimeSelectorActivities

As you can see, this function should be implemented by your app to return a DateAndTimeSelectorActivities struct containing the activity data you want to be visualized by the selector.



Working Through Visual Indicator Ideas

An early iteration using dots. The dots were clean and simple to implement, but they didn’t convey how busy a day was.

Circles that grew, based on the number of activities in a day, provided better information.

I tried a few visual ideas for how to show days that had activities. At first I tried simple dots beneath the date number. This was clean and simple to implement, but it didn’t let the user know how busy a day might be.

As I iterated through more attempts, it occurred to me that a circle that grew, based on the number of activities in the day, could also be a quick way to convey that idea of how full a day was.

The circle would have to grow from the center to have space, and it should not grow beyond the date’s cell size. The 7th of October, shown here for example, is a day with a lot of activities that could cause the circle to grow too large if was allowed to.

To control this, I established a pre-determined number of activities that would indicated a “full” day. Days with at least 10 activities would show as a full, 100% circle. I settled on a default of 10, and then made that number configurable through a view modifier (.fullDayCount(…)) for apps to change or configure as needed.

The finally iteration (so far). Circles are a light shade of the date text color, and start from a small size. Days that have a number of activities beyond the configured “full day” amount get marked in red or orange, depending on how over-the-top they are.

At first I was rendering the black circles at a minimum size to make sure the date’s number on top of the circle was legible. October 14th, for example, only has one activity. But this ended up restricting the variance in the size of the circle, which made it harder to discern. So I landed on making the circle a lighter shade of the date’s text color, which allowed it to start out smaller, behind the date. See the 14th in the final iteration, which still has only one activity. For comparison, the 7th has eleven, the 20th has five, the 25th has seven, and the 27th has sixteen.

Since the number of activities beyond the configured “full” number will not make the circle grow any larger, the user wouldn’t know which days were extra busy. To help with this a bit more, I made one final refinement that makes the color of the circle turn red, and then orange, depending on how many activities went beyond the “full day” amount.

That extra bit of color feedback is secondary information, and not crucial to the use of the app, so using color is probably okay. But it’s not ideal. For those who are color blind, the distinctions might be too subtle to see. A future enhancement should be to design a way to make the “really full” days more friendly to those who can’t see those color distinctions.

Notice, too, that the days before “today” still show their circle, but they get dimmed back to indicate they are in the past. I may eventually dim the date numbers on past dates as well as another iteration.

Avoiding Visual Overload

A calendar full of various sized circles could start to become overwhelming. For now, I’m relying on the idea that the app can control this, if it becomes too much, by figuring out what activities are actually counted in the visual—perhaps based the context of where the selector is being used in the app, or maybe on filters that the user has set. This filtering could easily be done with logic in the getDateAndTimeData(…) function that you provide in the data source. There, you can decide which activities are included for visual representation, and which ones are left out.

Time will tell, as I implement and use the component more in my app, if that’s enough or if I’ll have to try out some other visualization ideas down the road.

Large Screen Support

The calendar view opening up from the selector’s button in macOS.

The selector is using SwiftUI’s built-in popover view to display the calendar when you tap on the selector in its button form, which means the calendar will open up in a bubble, pointing to the button itself, when accessing it on a larger screen like the iPad or Mac. On the phone’s small screen, it slides up from the bottom in a sheet. This behavior, based on screen size, is some of the nice functionality provided by SwiftUI’s popover view.

The Mac version in Dark Mode.

Thanks to SwiftUI’s cross-platform support, the code and usage of the custom date selector, itself, is virtually the same for iPhone, iPad, and macOS. It’s only the presentation that might change, depending on the device.



Making it a Swift Package

The Swift Package for this component is available on GitHub.

Date calculation and selection is often a requirement for many mobile apps. I don’t know how many projects I’ve worked on that didn’t involve using and manipulating dates in one form or another, plus needing the UI for users to select their own dates and times along with it. My experience has also been that date logic is more complicated and bug-ridden than you expect it to be.

The demo app included with the DurableDateAndTime Swift Package, running in Xcode’s SwiftUI preview.

Because of that, and because the selector itself needed a good amount of common date logic to go along with it, I decide to bundle the selector in a larger, more general “DateAndTime” Swift Package that includes both the selector UI and a suite of common date calculation and manipulation routines useful for both Swift and SwiftUI projects. It made sense, for my own work at least, to finally package all of that date logic, along with the new sector, into one central, shared library that can be updated, maintained, and tested on its own.

The package is still in its early pre-release state, but I’ve made it available here if you would like to start checking it out. I would appreciate any suggestions or feedback.

Just be warned it is very much a work in progress. There is still a lot of work needed to be done. I’ll be iterating and improving it as I work on the app I’m building that uses it.

Also note that time selection in the DateAndTimeSelector is only available for iOS as of the date of this post. It currently uses a picker wheel to select the time, which is not available on macOS. A macOS implementation for time selection will be added later.

Keep checking the repo for the latest updates.

Previous
Previous

Holiday at the Old Arcade

Next
Next

Some of My All-Time Favorite Computer Books