Introduction
The inline date picker pattern — where a UIDatePicker expands below a cell when the user taps a date row — was introduced by Apple in the iOS 7 Calendar app. The technique works by inserting or removing a hidden table view row that contains the date picker, and animating the row height change. This approach still works in modern iOS with both UIKit table views and the compact/inline date picker styles introduced in iOS 14+.
The Approach
The date picker lives in its own table view cell, directly below the cell that displays the selected date. Tapping the date cell toggles the picker cell's visibility by inserting or deleting the row with animation.
// Key concept:
// Row 0: Date display cell (shows "March 2, 2025")
// Row 1: Date picker cell (hidden by default, inserted on tap)
Implementation
Data Model
1import UIKit
2
3class DatePickerTableViewController: UITableViewController {
4
5 var selectedDate = Date()
6 var isDatePickerVisible = false
7
8 // Section 0: other fields
9 // Section 1: date field + inline picker
10 let dateDisplayRowIndex = 0
11 let datePickerRowIndex = 1
12}
Number of Rows
1override func numberOfSections(in tableView: UITableView) -> Int {
2 return 2
3}
4
5override func tableView(_ tableView: UITableView,
6 numberOfRowsInSection section: Int) -> Int {
7 if section == 1 {
8 return isDatePickerVisible ? 2 : 1 // 1 row for label, +1 for picker
9 }
10 return 3 // other fields
11}
Cell Configuration
1override func tableView(_ tableView: UITableView,
2 cellForRowAt indexPath: IndexPath) -> UITableViewCell {
3 if indexPath.section == 1 {
4 if indexPath.row == dateDisplayRowIndex {
5 let cell = tableView.dequeueReusableCell(
6 withIdentifier: "DateDisplayCell", for: indexPath)
7 cell.textLabel?.text = "Date"
8 cell.detailTextLabel?.text = formatDate(selectedDate)
9 return cell
10 } else {
11 // Date picker cell
12 let cell = tableView.dequeueReusableCell(
13 withIdentifier: "DatePickerCell", for: indexPath)
14 if let picker = cell.viewWithTag(100) as? UIDatePicker {
15 picker.date = selectedDate
16 picker.addTarget(self, action: #selector(dateChanged(_:)),
17 for: .valueChanged)
18 }
19 return cell
20 }
21 }
22 // Other section cells...
23 return UITableViewCell()
24}
25
26func formatDate(_ date: Date) -> String {
27 let formatter = DateFormatter()
28 formatter.dateStyle = .medium
29 formatter.timeStyle = .short
30 return formatter.string(from: date)
31}
Toggle Picker Visibility
1override func tableView(_ tableView: UITableView,
2 didSelectRowAt indexPath: IndexPath) {
3 tableView.deselectRow(at: indexPath, animated: true)
4
5 if indexPath.section == 1 && indexPath.row == dateDisplayRowIndex {
6 isDatePickerVisible.toggle()
7
8 tableView.beginUpdates()
9 let pickerIndexPath = IndexPath(row: datePickerRowIndex, section: 1)
10
11 if isDatePickerVisible {
12 tableView.insertRows(at: [pickerIndexPath], with: .fade)
13 } else {
14 tableView.deleteRows(at: [pickerIndexPath], with: .fade)
15 }
16 tableView.endUpdates()
17 }
18}
Row Height
1override func tableView(_ tableView: UITableView,
2 heightForRowAt indexPath: IndexPath) -> CGFloat {
3 if indexPath.section == 1 && indexPath.row == datePickerRowIndex {
4 return 216 // Standard date picker height
5 }
6 return 44 // Standard cell height
7}
Handle Date Changes
1@objc func dateChanged(_ picker: UIDatePicker) {
2 selectedDate = picker.date
3
4 // Update the display cell
5 let displayIndexPath = IndexPath(row: dateDisplayRowIndex, section: 1)
6 tableView.reloadRows(at: [displayIndexPath], with: .none)
7}
Storyboard Setup
Add a prototype cell with identifier "DateDisplayCell" using the Right Detail style
Add a prototype cell with identifier "DatePickerCell" — drag a UIDatePicker into it
Set the date picker's tag to 100 in the Attributes Inspector
Set the DatePickerCell row height to 216 in the Size Inspector
Programmatic Setup (No Storyboard)
1override func viewDidLoad() {
2 super.viewDidLoad()
3
4 tableView.register(UITableViewCell.self, forCellReuseIdentifier: "DateDisplayCell")
5 tableView.register(DatePickerCell.self, forCellReuseIdentifier: "DatePickerCell")
6}
7
8class DatePickerCell: UITableViewCell {
9 let datePicker = UIDatePicker()
10
11 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
12 super.init(style: style, reuseIdentifier: reuseIdentifier)
13
14 datePicker.datePickerMode = .dateAndTime
15 datePicker.translatesAutoresizingMaskIntoConstraints = false
16 contentView.addSubview(datePicker)
17
18 NSLayoutConstraint.activate([
19 datePicker.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
20 datePicker.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
21 datePicker.topAnchor.constraint(equalTo: contentView.topAnchor),
22 datePicker.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
23 ])
24 }
25
26 required init?(coder: NSCoder) { fatalError() }
27}
iOS 14+ Compact and Inline Styles
iOS 14 introduced built-in picker styles that simplify the inline pattern:
1let datePicker = UIDatePicker()
2datePicker.preferredDatePickerStyle = .inline // Calendar grid
3datePicker.preferredDatePickerStyle = .compact // Minimal, shows popover on tap
4datePicker.preferredDatePickerStyle = .wheels // Classic spinning wheels
5
6// .inline style renders a full calendar view inside the cell
7// .compact shows a small label that opens a popover — no cell expansion needed
For iOS 14+, the .compact style often eliminates the need for the expand/collapse pattern entirely — the picker appears as a popover.
Common Pitfalls
Index path mismatch when picker is hidden: When the picker row is not visible, row indices shift. Always check isDatePickerVisible before interpreting index paths in cellForRowAt and didSelectRowAt. Accessing row 1 when only row 0 exists causes a crash.
Forgetting beginUpdates/endUpdates: Insert and delete row calls must be wrapped in beginUpdates()/endUpdates() (or performBatchUpdates in iOS 11+) to animate correctly. Without them, the table view may crash or display incorrectly.
Date picker height mismatch: The standard UIDatePicker with wheels needs approximately 216 points of height. Returning a smaller height clips the picker. With .inline style (iOS 14+), the picker needs approximately 350 points.
Not updating the display cell after date change: When the user scrolls the picker, the date display cell above it must update. Add a .valueChanged target to the picker and reload the display cell in the handler.
Reusing cells incorrectly: If the picker cell is dequeued and reused elsewhere, the picker's target-action can fire on the wrong controller. Always configure the target-action in cellForRowAt, not in a cell subclass initializer, and remove old targets first.
Summary
Toggle an inline date picker by inserting/deleting a table view row with animation
Track picker visibility with a boolean flag and adjust numberOfRowsInSection accordingly
Use beginUpdates()/endUpdates() to animate the row insertion/deletion
Set the picker cell height to 216 for wheels style, ~350 for inline calendar style
iOS 14+ offers .compact and .inline picker styles that can replace the manual expand/collapse pattern
Handle date changes via .valueChanged target and reload the display cell to show the new date