Put UIDatePicker inside static UITableView

In the previous article we implemented input form using UITableView. Static table views provide a great help during iOS applications development: developer can visually create nice looking UIs. One of the common tasks here is to create UI for picking a date. Apple already provides UI element for this: UIDatePicker.

In this article we will embed UIDatePicker into the UITableViewCell inside static table view. User will be able to show/hide it by tapping on a label with date. Apple do this in their Reminders application.

Let’s reproduce this behavior.

Initial Setup

Create a single view application, remove default view from the Main.storyboard and add Table View Controller instead. And don’t forget to make it Initial View Controller. Remove ViewController.swift file as well.

Now, select table view and set it’s Content to Static Cells, Style to Grouped and uncheck Show Selection on Touch:

Interface Builder: UITableView setup

Set number of Sections to 3. First section should have one row, second section - 4 rows, third section - 2 rows. Select all rows and set their Selection property to None.

Rows' Setup

First row

Select first row and set its style to Basic. Double click on a label inside the cell and enter any text. I use "Write a letter to your future self". This is a text for reminder.

Second row

Select second row and set its style to Custom. Drag and drop one label and one switch on it. Place label close to the left edge of the row. Put switch close to the right edge.

Setup Auto Layout constraints for the switch:

Interface Builder: Auto Layout constraints for UISwitch

Setup Auto Layout constraints for the label:

Interface Builder: Auto Layout constraints for UILabel

Double click on the label and set it’s text to "Remind me on a day". Set State of the switch to Off.

Third row

Select third row and set its style to the Right Detail. Double click on Title label and set it’s text to "Alarm".

Table view should have the following look right now:

Interface Builder: Table View with 3 cells

Fourth row

Select fourth row and resize it:

Resized cell

Drag and drop date picker on it and setup Auto Layout (just use Reset to Suggested Constraints on it):

Date picker inside UITableViewCell

Fifth row

Select fifth row and set it’s style to Right Detail. Set it’s Accessory to Disclosure Indicator. Double click on Title label and set its text to "Repeat". Double click on Detail label and set its text to "Never".

Sixth row

Setup it in the same way as row #2. Set label text to "Remind me at a location".

Seventh row

Set its style to Basic. Set it’s Accessory to Disclosure Indicator. Double click on Title label and set its text to "Location".

This is how your table view should look like now:

Table View with all cells
I decided not to copy the whole UI look and feel of the Reminders application for simplicity.

Listen to DatePicker’s value changes

We want to update text of the Detail label of third row each time user changes value of the date picker.

Create a new Cocoa Touch Class named TableViewController and set its base class to UITableViewController. Remove all auto generated code inside newly created class:

import UIKit

public class TableViewController: UITableViewController {
}

Open storyboard and select table view controller on it. Go to Identity Inspector and set Class to TableViewController.

Create outlets for detail label and date picker using Ctrl+Drag in Assistant Editor.

@IBOutlet weak var txtDate: UILabel!
@IBOutlet weak var datePicker: UIDatePicker!

Add method that updates label text using date value from the date picker:

@IBAction
public func didChangeDate() {
    txtDate.text = NSDateFormatter.localizedStringFromDate(datePicker.date, dateStyle: .ShortStyle, timeStyle: .ShortStyle)
}

Select date picker and go to Connections Inspector. Drag from its Value Changed event to this method. Now add viewDidLoad method override:

public override func viewDidLoad() {
    didChangeDate()
}

This will update label’s text for the first time. Now build the project and run it. If you did everything correct, then label should change its text every time you change value of the date picker.

Toggle date picker on date row tap

We want to toggle date picker when user taps on the cell with Alarm date. Let’s override tableView(_:didSelectRowAtIndexPath:) method:

public override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    switch (indexPath.section, indexPath.row) {
    case (1, 1):
        toggleDatePicker()
    default:
        ()
    }
}

This will invoke toggleDatePicker method if user tapped on the row with index 1 in the section with index 1. And this is the row we need.

How we should implement toggleDatePicker method? We can try to set row’s hidden property to true. This doesn’t work. We can try to set it’s height to zero. This also doesn’t work.

In order to achieve our goal we need to use tableView(_:heightForRowAtIndexPath:) method. This method provides heights for each table’s row. So we need a boolean property that holds information about visibility of the row and then use it inside the method to return zero height if date picker should not be visible:

private var datePickerHidden = false
public override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if datePickerHidden && indexPath.section == 1 && indexPath.row == 0 {
        return 0
    } else {
        return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
    }
}

This method returns zero height for the date picker row if it should be hidden.

And now we can implement toggleDatePicker method:

private func toggleDatePicker() {
    datePickerHidden = !datePickerHidden

    // Force table to update its contents
    tableView.beginUpdates()
    tableView.endUpdates()
}

Date picker should be hidden when we load view for the first time:

public override func viewDidLoad() {
    didChangeDate()
    toggleDatePicker()
}

Compile and run…​

Display issues with UIDateTimePicker

To fix these display issues, you should go to storyboard, select row that contains date picker and set it’s Clip Subviews property to true.

Compile and run. Now it works as expected.

Refactoring

Let’s stop for a moment and look into the code we have:

public override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if datePickerHidden && indexPath.section == 1 && indexPath.row == 0 {
        return 0
    } else {
        return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
    }
}

public override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    switch (indexPath.section, indexPath.row) {
    case (1, 1):
        toggleDatePicker()
    default:
        ()
    }
}

I don’t like "magic" numbers here. Also, I don’t like duplication of row identification logic. Let’s fix this:

private enum Row: Int {
    case Reminder
    case RemindMeOnDay
    case Alarm
    case DatePicker
    case Repeat
    case RemindMeAtLocation
    case Location

    case Unknown


    init(indexPath: NSIndexPath) {
        var row = Row.Unknown

        switch (indexPath.section, indexPath.row) {
        case (0, 0):
            row = Row.Reminder
        case (1, 0):
            row = Row.RemindMeOnDay
        case (1, 1):
            row = Row.Alarm
        case (1, 2):
            row = Row.DatePicker
        case (1, 3):
            row = Row.Repeat
        case (2, 0):
            row = Row.RemindMeAtLocation
        case (2, 1):
            row = Row.Location
        default:
            ()
        }

        assert(row != Row.Unknown)

        self = row
    }
}

This enum replaces "magic" numbers with meaningful names, encapsulates NSIndex → RowName transformation logic, protects from mistakes.

Let’s rewrite our methods using this enum:

public override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let row = Row(indexPath: indexPath)

    if row == .Alarm {
        toggleDatePicker()
    }
}

public override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    let row = Row(indexPath: indexPath)

    return datePickerHidden && row == .DatePicker ? 0 : super.tableView(tableView, heightForRowAtIndexPath: indexPath)
}

This code is much better and less error prone.

Toggle other rows

Well, I will not show how to do this. It’s your exercise.

Thanks for reading this.