Inline UIDatePicker In Swift With UIViewController

This tutorial walks through implementing two inline UIDatePicker controls for selecting date/time values, and is written in Swift.

There are examples out there that demonstrate an inline UIDatePicker. However, I’ll be illustrating some key additional features in this tutorial, including:

  1. Use a UIViewController and a UITableView, instead of a UITableViewController.
  2. The two UIDatePicker controls represent start and end times. There is interaction between the two date fields, such that one changing can update the other. The end time must always be a minimum of 15 minutes after the start time, but defaults to 1 hour after the start time.
  3. The UIDatePicker controls are in a UITableView, so that is the “inline” aspect. However, the pickers open over the rest of the user interface, and close back to their original places. You can see the start time picker open in the featured image at the top of this post. The user interface below the pickers is an MKMapView for this demo.

Incorporating these features imposes additional design considerations that must be considered in the solution.

I implemented inline date pickers for my 1Goal app settings screen, which was written in Objective-C. I also created a demo app and tutorial for the Objective-C version here on my blog back in 2015.

Since I’ve used inline date pickers in recent Swift apps with the features as described above, I decided to write this post and provide a fully working demo app. Some of the techniques I’ve used were inspired by this valuable post and demo.

With that preamble out of the way, let’s get started building the app.

The project starts with a Single View Application template, and a project called InlineUIDatePickerSwift. Since there will be an MKMapView, the MapKit.framework needs to be specified in the project. Just go to Target > InlineUIDatePickerSwift, click on the Capabilities tab, and enable the switch for Maps.

I won’t go through every detail of the storyboard setup, but in a nutshell, I’ll put a UITableView and UIStackView below the main UIView. The table view will have Dynamic Prototypes set to 2. One prototype cell is for the date picker, and the other for the date displayed when there is no picker open. In the stack view, I’ll put one MkMapView that takes up the whole stack view. The map view could be directly under the main view, but I’m using a stack view because realistically one might have more than just a map view, so I’ll just get setup for that.

Constraints with Constant equal to 0 are defined on the table view for leading, trailing, and top relative to the main UIView. For the stack view, leading, trailing and bottom constraints relative to the main UIView are defined also with Constant values of 0. One more is added for the stack view top, also relative to the main UIView. This one is set to a Constant of 0 to start, but in viewWillAppear, it will be updated to match the height of the table view, and then it will not change again. This means that once the table view is initially sized, the stack view will start vertically at the bottom of the table view, based on the table view’s height without open date pickers.

The reason the stack view isn’t constrained relative to the table bottom is because in my scenario, I don’t want the map to shrink when the date picker is open, as I mentioned in the feature summary above.

The background for the main View should be set to Clear, and the same for the dateCell itself, and its fields.

For the first prototype, the Style is set to Right Detail, which means we don’t need constraints defined for it. The Identifier is set to dateCell. The second prototype remains Custom, and a UIDatePicker is added under the Content View. The constraints are defined for leading, trailing, top, and bottom, with Constant values of 0, not relative to the margins.

The datePickerCell needs to have its Custom checkbox checked, and a value for Row Height of 216. This one is based on inherent knowledge of the intrinsic content size of the UIDatePicker. When experimenting with not setting this value, various issues seem to occur. The UIDatePicker is one component that does not seem to lend itself well to dynamic sizing.

Finally, a Height Constraint is created for the Table View, and the value is set to 88. This is going to be updated at runtime to reflect the content size of the table, so at this point it is an estimate based on an estimated row size of 44 for the start and end time cells. There are various solutions for setting a UITableView height based on its contents, but using automatic dimensioning and a height constraint is cleaner in my opinion than adding up sizes of rows.

In order to access the date picker when we have a date picker cell, let’s give it a tag of 99.

The Content View for the dateCell should have a Black background color, and both the Title and Detail fields, white for the text Color.

For more realism, I’ll embed the View Controller in a UINavigationController, via Editor > Embed In > Navigation Controller.

The Navigation Item created under the View Controller shall have the Title set to Inline Start/End Date Picker Swift. I want the navigation bar to be black, so I’ve specified Black for the Style, and unchecked Translucent. In code later, we’ll have the status bar match.

If you run it, there isn’t much to see other than a map and the navigation bar, because we haven’t done anything with the table view yet.

It’s now time to focus on writing Swift code in the ViewController class. First, we’ll need IBOutlets for the table view, the top constraint for the stack view, and the height constraint for the table view. The IBOutlet for the stack view top constraint will facilitate setting the stack view top to be at the bottom of the table view.

The next thing is a class to represent the start and end time fields, and specifically the title and date for each field. It will become obvious soon why we need that. While in that area, we’ll also add an extension to allow us to round the end date up to the next interval, which in our case will be 15 minutes.

Since we have a UIViewController instead of a UITableViewController, we must conform to the UITableViewDataSource and UITableViewDelegate protocols, so we can specify those and some properties that will be needed.

We will have compile errors now because the implementation of the protocols isn’t there yet, but we’ll get to those soon.

Let’s define some methods for the controller. I’ll add some comments as we go to make the code easier to understand.

The next set of code includes various supporting methods needed before we define the protocol implementations.

With those methods defined, we need to create the connection between the date picker and the dateAction method to handle the user changing the value of the date picker.

When the start picker date is changed, that triggers validation and the end date/time will be changed if necessary.

Let’s now add in the required protocol implementations.

The app is now ready to run. It works in portrait and landscape, and you can observe that the table opens over the map when a date picker is open, as illustrated in the next two screenshots.

Although I’ve added lots of comments, there are a few additional concepts I will address.

I general I would rather not use a boolean as you can see in viewWillAppear, but among the other alternatives, it seemed like the easiest approach in this case for a rather unique requirement of setting an initial constraint for the stack view top to anchor to the table view bottom based on it’s starting content size.

This demo app can be modified easily so that the top of the map is constrained to be at the bottom of the table, such that opening the date picker pushes the map down and adjusts it’s size accordingly. It requires three steps, which are also described in the viewWillAppear method comments.

  1. In the storyboard, remove the top constraint for the map’s UIStackView., and remove the corresponding  IBOutlet in the ViewController.
  2. Create a vertical space constraint between the UITableView and UIStackview, with a constant of 0.
  3. Comment out this line in viewWillAppearstackTopConstraint.constant = tableView.contentSize.height, and remove all traces of the hasAppeared boolean and the if statement. viewWillAppear will then only have the lines shown below.

When you run the app now with those changes, you’ll see the map is resized so that it is all below the open table.

If you’ve experimented with inline UIDatePicker implementations, and have other techniques or suggestions, it would be much appreciated if you would leave a comment.

You can find the working demo app here in GitHub.

Leave a Comment: