Skip to content

Inline Date Picker Disclosure

A date row that opens in place into a calendar and closes again once a day is chosen.

Inline Date Picker Disclosure, playing
Download .swift
Origin
Original work
Found by
date picker · calendar · disclosure · inline · expand · collapse · date · selector de fecha · calendario · desplegable · en línea · expandir · colapsar · fecha

A date row that opens in place into a calendar and closes again once a day is chosen.

With Reduce Motion on, the calendar appears and disappears without animating.

struct InlineDatePickerDisclosureDemo: View {
@State private var departure = Date.now
@State private var comeback = Date.now.addingTimeInterval(4 * 86_400)
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Your trip")
.font(.system(.largeTitle, design: .rounded, weight: .bold))
VStack(spacing: 14) {
InlineDatePickerDisclosure(title: "Departure", date: $departure)
.background(.background.secondary, in: .rect(cornerRadius: 20))
InlineDatePickerDisclosure(title: "Return", date: $comeback, range: departure...Date.distantFuture)
.background(.background.secondary, in: .rect(cornerRadius: 20))
}
.tint(.indigo)
// The return never comes before the departure.
.onChange(of: departure) { _, newValue in
if comeback < newValue { comeback = newValue }
}
}
.padding(20)
}
}
}
ParameterTypeDefault
titleLocalizedStringResourceRequired
dateBinding<Date>Required
rangeClosedRange<Date>Date.distantPast...Date.distantFuture

Copy this code into your project, or download the file.

InlineDatePickerDisclosure.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A date row that opens in place into a calendar and closes again once a day is chosen.
///
/// With Reduce Motion on, the calendar appears and disappears without animating.
struct InlineDatePickerDisclosure: View {
let title: LocalizedStringResource
@Binding var date: Date
var range: ClosedRange<Date> = Date.distantPast...Date.distantFuture
@State private var isExpanded = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var animation: Animation? {
reduceMotion ? nil : .smooth(duration: 0.35)
}
var body: some View {
VStack(spacing: 0) {
Button {
withAnimation(animation) { isExpanded.toggle() }
} label: {
row
}
.buttonStyle(.plain)
.accessibilityLabel(title)
.accessibilityValue(date.formatted(date: .long, time: .omitted))
.accessibilityHint(isExpanded ? "Hides the calendar" : "Shows the calendar")
if isExpanded {
Divider()
DatePicker(selection: $date, in: range, displayedComponents: .date) { Text(title) }
.datePickerStyle(.graphical)
.labelsHidden()
.padding(.horizontal, 8)
.transition(.opacity)
}
}
.clipped()
.onChange(of: date) { oldValue, newValue in
if isExpanded, InlineDatePickerDisclosureRule.closes(from: oldValue, to: newValue, in: .current) {
withAnimation(animation) { isExpanded = false }
}
}
}
private var row: some View {
HStack(spacing: 12) {
Image(systemName: "calendar")
.foregroundStyle(.tint)
.accessibilityHidden(true)
// At the largest text sizes the date moves under the title.
ViewThatFits(in: .horizontal) {
HStack {
labels
}
VStack(alignment: .leading, spacing: 4) {
labels
}
}
Image(systemName: "chevron.down")
.font(.footnote.weight(.bold))
.foregroundStyle(.secondary)
.rotationEffect(.degrees(isExpanded ? 180 : 0))
.accessibilityHidden(true)
}
.padding()
.contentShape(.rect)
}
@ViewBuilder
private var labels: some View {
Text(title)
.font(.body.weight(.medium))
.lineLimit(1)
.fixedSize()
Spacer(minLength: 8)
Text(date.formatted(date: .abbreviated, time: .omitted))
.foregroundStyle(.secondary)
.monospacedDigit()
.lineLimit(1)
.fixedSize()
}
}
enum InlineDatePickerDisclosureRule {
/// Paging through months does not change the date, so only a different day closes the calendar.
static func closes(from oldValue: Date, to newValue: Date, in calendar: Calendar) -> Bool {
!calendar.isDate(oldValue, inSameDayAs: newValue)
}
}
// MARK: - Demo
struct InlineDatePickerDisclosureDemo: View {
@State private var departure = Date.now
@State private var comeback = Date.now.addingTimeInterval(4 * 86_400)
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Your trip")
.font(.system(.largeTitle, design: .rounded, weight: .bold))
VStack(spacing: 14) {
InlineDatePickerDisclosure(title: "Departure", date: $departure)
.background(.background.secondary, in: .rect(cornerRadius: 20))
InlineDatePickerDisclosure(title: "Return", date: $comeback, range: departure...Date.distantFuture)
.background(.background.secondary, in: .rect(cornerRadius: 20))
}
.tint(.indigo)
// The return never comes before the departure.
.onChange(of: departure) { _, newValue in
if comeback < newValue { comeback = newValue }
}
}
.padding(20)
}
}
}
#Preview {
InlineDatePickerDisclosureDemo()
}