Created
November 22, 2024 20:08
-
-
Save lifeutilityapps/cb4a83baeb4c6d2dbff9380a89207ce8 to your computer and use it in GitHub Desktop.
A simple date range picker built with SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Cool!