Skip to content

Instantly share code, notes, and snippets.

@lifeutilityapps
Created November 22, 2024 20:08
Show Gist options
  • Save lifeutilityapps/cb4a83baeb4c6d2dbff9380a89207ce8 to your computer and use it in GitHub Desktop.
Save lifeutilityapps/cb4a83baeb4c6d2dbff9380a89207ce8 to your computer and use it in GitHub Desktop.
A simple date range picker built with SwiftUI
//
// StandardDateRangePicker.swift
// Downpayment Tracker
//
// Created by Life Utility Apps on 6/5/24.
//
import SwiftUI
struct StandardDateRangePickerSheet: View {
@Binding var startDate: Date
@Binding var endDate: Date
var sheetTitle = "Date Range"
var sheetSubtitle = "Select a Date Range"
var labelStartDate = "Start Date"
var labelEndDate = "End Date"
var minDate: Date = Date().setYear(1950)
var maxDate: Date = Date()
var onClose: () -> Void
var onSave: (Date, Date) -> Void
// On Appear Methods
func syncState() {
localStartDate = startDate
localEndDate = endDate
}
func handleAppear(){
isLoading = true
syncState()
SCL.util.asyncAfter {
isLoading = false
}
}
func handleSave() {
withAnimation {
startDate = localStartDate
endDate = localEndDate
}
onSave(localStartDate, localEndDate)
onClose()
}
// State
@State private var isLoading: Bool = true
// Date Range
@State private var localStartDate: Date = Date()
@State private var localEndDate: Date = Date()
// UI
@ScaledMetric var heightSheetWithSuggestions = 320.0
@ScaledMetric var heightSheet = 260
var showSuggestions: Bool {
return EStandardDateRangePickerSuggestionItem.isSuggestionsVisible(minDate: minDate, maxDate: maxDate)
}
var displayLocalStartDate: String {
if(localStartDate.isToday) {
return "Today"
}
return StandardDateText.getDateString(.monthDayOrFullDate, localStartDate)
}
var displayLocalEndDate: String {
if(localEndDate.isToday) {
return "Today"
}
return StandardDateText.getDateString(.monthDayOrFullDate, localEndDate)
}
var displayLocalLabel: String {
let result = "\(displayLocalStartDate) to \(displayLocalEndDate)"
if(displayLocalStartDate == displayLocalEndDate) {
return displayLocalStartDate
}
return result
}
func handleSetSuggestion(suggestion: EStandardDateRangePickerSuggestionItem) {
SCL.util.vibrateDevice(.rigid)
withAnimation {
localStartDate = suggestion.startDate
localEndDate = suggestion.endDate
}
}
func scrollToSuggestion(scrollValue: ScrollViewProxy) {
var activeId: UUID? = nil
let visibleSuggestions = EStandardDateRangePickerSuggestionItem.allCases.filter({ $0.isVisible(minDate: minDate, maxDate: maxDate) })
activeId = (visibleSuggestions.first { suggestion in
suggestion.isActive(start: localStartDate, end: localEndDate)
})?.id
if let uuid = activeId {
// Check if it's the first
if let first = visibleSuggestions.first {
// Only scroll if its not first one
if(first.id != uuid){
scrollValue.scrollTo(uuid, anchor: .leading)
}
}
}
}
var body: some View {
VStack(spacing: 0) {
StandardSheetTitle(title: sheetTitle, subtitle: sheetSubtitle, onClose: onClose)
if(showSuggestions) {
HStack {
Text("Suggestions")
.font(.caption)
.foregroundStyle(SCL.colors.labelSecondaryColor)
Spacer()
}
.padding(.top)
.padding(.leading)
.padding(.bottom, SCL.ui.spacing / 2)
ScrollViewReader { scrollValue in
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(EStandardDateRangePickerSuggestionItem.allCases, id: \.self) { suggestion in
let isActive = suggestion.isActive(start: localStartDate, end: localEndDate)
let id = suggestion.id
HStack {
if(suggestion.isVisible(minDate: minDate, maxDate: maxDate)) {
Button {
handleSetSuggestion(suggestion: suggestion)
withAnimation {
scrollValue.scrollTo(id)
}
} label: {
Label(suggestion.displayName, systemImage: SCL.icons.clock)
}
.buttonStyle(.bordered)
.tint(isActive ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor)
}
}
.padding(.trailing, SCL.ui.spacing)
.id(id)
}
Spacer()
}
.padding(.horizontal)
.padding(.trailing)
}
.scrollIndicators(.never)
.onAppear {
SCL.util.asyncAfter {
if(SCL.isPhone){
scrollToSuggestion(scrollValue: scrollValue)
}
}
}
.overlay(alignment: .trailing) {
Group {
LinearGradient(colors: [.clear, SCL.colors.backgroundColor.opacity(0.5), SCL.colors.backgroundColor.opacity(0.9)], startPoint: .leading, endPoint: .trailing)
.frame(width: SCL.ui.screenWidth * 0.2)
}.allowsHitTesting(false)
}
}
}
VStack(spacing: SCL.ui.spacing) {
Spacer()
if(showSuggestions){
Divider()
}
HStack {
Image(systemName: SCL.icons.calendar)
.font(.title2)
Text(labelStartDate)
if(isLoading){
Spacer()
ProgressView()
} else {
if(minDate <= localEndDate) {
DatePicker("", selection: $localStartDate, in: minDate...localEndDate, displayedComponents: .date)
} else {
Spacer()
Text("--")
.foregroundStyle(SCL.colors.labelSecondaryColor)
}
}
}.frame(height: 40)
Divider()
HStack {
Image(systemName: SCL.icons.calendar)
.font(.title2)
Text(labelEndDate)
if(isLoading){
Spacer()
ProgressView()
} else {
if(localStartDate <= maxDate) {
DatePicker("", selection: $localEndDate, in: localStartDate...maxDate, displayedComponents: .date)
} else {
Spacer()
Text("--")
.foregroundStyle(SCL.colors.labelSecondaryColor)
}
}
}.frame(height: 40)
Spacer()
}
.padding(.horizontal)
.onAppear {
handleAppear()
}
StandardSheetControls(saveLabel: "Select \(displayLocalLabel)", onSave: handleSave)
}
.accentColor(SCL.colors.accentColor)
.presentationDetents(SCL.isPhone ? [.height(showSuggestions ? heightSheetWithSuggestions : heightSheet)] : [.medium])
}
}
@rismay
Copy link

rismay commented Nov 24, 2024

Cool!

@graygillman
Copy link

Great Job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment