How to disable the future date selection from DatePicker in SwiftUI [duplicate] - ios

This question already has an answer here:
Is it possible to set SwiftUI DatePicker minimumDate using bindings?
(1 answer)
Closed 7 months ago.
I am working on a Custom DatePicker in SwiftUI, for that I have written below code which is woking great -
struct DatePickerView: View {
#State var searchDate = Date()
let title = "Select a date"
let dateChanged: (_ date: Date) -> Void
var body: some View {
VStack {
DatePicker(selection: Binding(get: {
self.searchDate
}, set: { newVal in
self.searchDate = newVal
dateChanged(searchDate)
}), displayedComponents: .date) {
Text(title)
.font(.headline)
}
.background(.bar)
}
}
}
Now I want to disable the future date selection, can someone help me to do that?

It can be done with allowed range, like
DatePicker(selection: Binding(get: {
self.searchDate
}, set: { newVal in
self.searchDate = newVal
dateChanged(searchDate)
}), in: ...Date(), // << here !!
displayedComponents: .date) {
Text(title)
.font(.headline)
}

Related

SwiftUI: Scrolling Sticking when using LazyVGrid inside a Scrollview:

The below code seems to work fine on an iPhone 13 mini but stutters or sticks when scrolling on an iPhone 13, not every time but often. The code for the Calendar is here. What could be causing this? 🤔
struct MasterCalendarWithDay: View {
#Environment(\.calendar) var calendar
#State private var selectedDate: Date?
#State private var selectedTabIndex = 0
private var month: DateInterval {
//calendar.dateInterval(of: .year, for: Date())!
DateInterval(start: Calendar.current.date(byAdding: .month, value: show6Months ? -6 : -1, to: Date())!, end: Date())
}
var body: some View {
LoadingView(isShowing: $isLoadingRecoveryData, text: show6Months ? "Loading past 6 months of data. This may take a few seconds." : "Loading...") {
NavigationView {
ScrollView {
ScrollViewReader { value in
VStack {
ZStack {
CalendarView(interval: month) { date in
if date <= Date() {
Button(action: { self.selectedDate = date }) {
//Omitted
}.buttonStyle(PlainButtonStyle())
}
} // end of calender
.onAppear {
value.scrollTo(Date().startOfDay())
}
} //end of Zstack
}
}
} //end of scroll view
.coordinateSpace(name: "pullToRefreshInRecoveryCalendar")
.navigate(using: $selectedDate, destination: makeDestination)
}
.navigationViewStyle(.stack) //This asks SwiftUI to only show one view at a time, regardless of what device or orientation is being used, and prevents constraint warnings in console log. From: https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui
.onChange(of: selectedTabIndex, perform: { value in
})
}
}
I think your ScrollView should be nested inside the ScrollViewReader.
var body: some View {
ScrollViewReader { value in
ScrollView {
...
See
https://developer.apple.com/documentation/swiftui/scrollviewreader

Detect when user taps out of or saves on DatePicker

I'm trying to fetch some data on a network call when a user selects a time from DatePicker.
I originally tested using .onChange but this fires every time that changes, and not on the final tap off.
DatePicker("Title", selection: $currentDate, displayedComponents: .hourAndMinute)
.onChange(of: $currentDate) { value in
print(value)
}
I also tried using the didset{} but that also fired on every change too.
#Published var currentDate: Date = Date() {
didSet { print(currentDate) }
}
What I want to do is once the user selects a time, I fire off some functions. I wanted to not do it every cycle in the wheel.
Is there a way to do this in SwiftUI or UIKit importing into SwiftUI?
Please see attached of what I'm looking at:
You could try rolling your own DatePickerView, such as this approach:
struct ContentView: View {
#State private var birthdate = Date()
var body: some View {
DatePickerView(date: $birthdate)
}
}
struct DatePickerView: View {
#Binding var date: Date
#State var hasChanged = false
var body: some View {
ZStack {
Color.white.opacity(0.01).ignoresSafeArea()
.onTapGesture {
if hasChanged {
print("---> do a network call now with: \(date)")
hasChanged = false
}
}
DatePicker("Birth Date", selection: $date, displayedComponents: .hourAndMinute)
.pickerStyle(.wheel)
.onChange(of: date) { val in
hasChanged = true
}
}
}
}

Dismiss SwiftUI DatePicker when user taps on already selected date

I want the user to select a date in the past by presenting a SwiftUI DatePicker. The date binding is set to today's date.
Now, I want the user to be able to select today's date. But when the user taps on the already selected date, the DatePicker is not dismissed. It only is dismissed when the user taps on a date that was not already selected. So in my case I want to dismiss the DatePicker when the user taps on 13.
Is there a way to dismiss the DatePicker when the user taps on the preselected date?
Here is how I use the DatePicker:
struct DatePickerView: View {
#Binding var date: Date
var body: some View {
DatePicker(
"Select Date",
selection: $date,
in: ...Date(),
displayedComponents: [.date]
)
.datePickerStyle(.graphical)
.background(Color.white)
.cornerRadius(8)
.padding(.horizontal, 40)
}
}
I had a similar problem and put a .graphical DatePicker in my own popover. The only downside is on iPhone popovers currently show as sheets which is an annoying inconsistency. Now you can control the popover presentation however you like and even have cancel/done actions.
struct DatePickerPopover: View {
#State var showingPicker = false
#State var oldDate = Date()
#Binding var date: Date
let doneAction: () -> ()
var body: some View {
Text(date, format:.dateTime.year())
.foregroundColor(.accentColor)
.onTapGesture {
showingPicker.toggle()
}
.popover(isPresented: $showingPicker, attachmentAnchor: .point(.center)) {
NavigationStack {
DatePicker(selection: $date
, displayedComponents: [.date]){
}
.datePickerStyle(.graphical)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
date = oldDate
showingPicker = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
doneAction()
showingPicker = false
}
}
}
}
}
.onAppear {
oldDate = date
}
}
}

SwiftUI DatePicker Binding optional Date, valid nil

I'm experimenting code from https://alanquatermain.me/programming/swiftui/2019-11-15-CoreData-and-bindings/
my goal is to have DatePicker bind to Binding< Date? > which allow for nil value instead of initiate to Date(); this is useful, if you have Date attribute in your core data model entity which accept nil as valid value.
Here is my swift playground code:
extension Binding {
init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
self.init(get: { source.wrappedValue != nil },
set: { source.wrappedValue = $0 ? defaultValue : nil})
}
}
struct LiveView: View {
#State private var testDate: Date? = nil
var body: some View {
VStack {
Text("abc")
Toggle("Has Due Date",
isOn: Binding(isNotNil: $testDate, defaultValue: Date()))
if testDate != nil {
DatePicker(
"Due Date",
selection: Binding($testDate)!,
displayedComponents: .date
)
}
}
}
}
let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)
I can't find solution to fix this code. It works when the toggle first toggled to on, but crash when the toggle turned back off.
The code seems to behave properly when I removed the DatePicker, and change the code to following:
extension Binding {
init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
self.init(get: { source.wrappedValue != nil },
set: { source.wrappedValue = $0 ? defaultValue : nil})
}
}
struct LiveView: View {
#State private var testDate: Date? = nil
var body: some View {
VStack {
Text("abc")
Toggle("Has Due Date",
isOn: Binding(isNotNil: $testDate, defaultValue: Date()))
if testDate != nil {
Text("\(testDate!)")
}
}
}
}
let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)
I suspect it's something to do with this part of the code
DatePicker("Due Date", selection: Binding($testDate)!, displayedComponents: .date )
or
problem when the source.wrappedValue set back to nil (refer to Binding extension)
The problem is that DatePicker grabs binding and is not so fast to release it even when you remove it from view, due to Toggle action, so it crashes on force unwrap optional, which becomes nil ...
The solution for this crash is
DatePicker(
"Due Date",
selection: Binding<Date>(get: {self.testDate ?? Date()}, set: {self.testDate = $0}),
displayedComponents: .date
)
An alternative solution that I use in all my SwiftUI pickers...
I learned almost all I know about SwiftUI Bindings (with Core Data) by reading that blog by Jim Dovey. The remainder is a combination of some research and quite a few hours of making mistakes.
So when I use Jim's technique to create Extensions on SwiftUI Binding then we end up with something like this for a deselection to nil...
public extension Binding where Value: Equatable {
init(_ source: Binding<Value>, deselectTo value: Value) {
self.init(get: { source.wrappedValue },
set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
)
}
}
Which can then be used throughout your code like this...
Picker("Due Date",
selection: Binding($testDate, deselectTo: nil),
displayedComponents: .date
)
OR when using .pickerStyle(.segmented)
Picker("Date Format Options", // for example
selection: Binding($selection, deselectTo: -1)) { ... }
.pickerStyle(.segmented)
... which sets the index of the segmented style picker to -1 as per the documentation for UISegmentedControl and selectedSegmentIndex.
The default value is noSegment (no segment selected) until the user
touches a segment. Set this property to -1 to turn off the current
selection.
Here is my solution, I added a button to remove the date and add a default date. All it's wrapped in a new component
https://gist.github.com/Fiser12/62ef54ba0048e5b62cf2f2a61f279492
import SwiftUI
struct NullableBindedValue<T>: View {
var value: Binding<T?>
var defaultView: (Binding<T>, #escaping (T?) -> Void) -> AnyView
var nullView: ( #escaping (T?) -> Void) -> AnyView
init(
_ value: Binding<T?>,
defaultView: #escaping (Binding<T>, #escaping (T?) -> Void) -> AnyView,
nullView: #escaping ( #escaping (T?) -> Void) -> AnyView
) {
self.value = value
self.defaultView = defaultView
self.nullView = nullView
}
func setValue(newValue: T?) {
self.value.wrappedValue = newValue
}
var body: some View {
HStack(spacing: 0) {
if value.unwrap() != nil {
defaultView(value.unwrap()!, setValue)
} else {
nullView(setValue)
}
}
}
}
struct DatePickerNullable: View {
var title: String
var selected: Binding<Date?>
#State var defaultToday: Bool = false
var body: some View {
NullableBindedValue(
selected,
defaultView: { date, setDate in
let setDateNil = {
setDate(nil)
self.defaultToday = false
}
return AnyView(
HStack {
DatePicker(
"",
selection: date,
displayedComponents: [.date, .hourAndMinute]
).font(.title2)
Button(action: setDateNil) {
Image(systemName: "xmark.circle")
.foregroundColor(Color.defaultColor)
.font(.title2)
}
.buttonStyle(PlainButtonStyle())
.background(Color.clear)
.cornerRadius(10)
}
)
},
nullView: { setDate in
let setDateNow = {
setDate(Date())
}
return AnyView(
HStack {
TextField(
title,
text: .constant("Is empty")
).font(.title2).disabled(true).textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: setDateNow) {
Image(systemName: "plus.circle")
.foregroundColor(Color.defaultColor)
.font(.title2)
}
.buttonStyle(PlainButtonStyle())
.background(Color.clear)
.cornerRadius(10)
}.onAppear(perform: {
if self.defaultToday {
setDateNow()
}
})
)
}
)
}
}
Most optional binding problems can be solved with this:
public func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
Here's how I use it with DatePicker:
DatePicker(
"",
selection: $testDate ?? Date(),
displayedComponents: [.date]
)

Change selected date format from DatePicker SwiftUI

Is there a way to format the date?(from the position indicated by the arrow in the picture) I know it is formatted based on the locale but is there a way to format it myself?
struct ContentView: View {
#State private var selectedDate = Date()
var body: some View {
Form {
DatePicker(selection: $selectedDate, in: ...Date(), displayedComponents: .date) {
Text("From*")
}
}
}
}
The only way I could figure to accomplish this is to create my own custom DatePicker view and use onAppear on the TextField to update an #State selectedDateText: String variable for displaying in the TextField. This feels like a hack and I’m almost embarrassed to post it but it works. I’m new at Swift and iOS programming in general so I’m sure someone will come along with a better answer so I’ll offer this for what it’s worth. My custom view is something like this:
struct CustomDatePicker: View {
#Binding var date: Date
#State private var showPicker: Bool = false
#State private var selectedDateText: String = "Date"
private var selectedDate: Binding<Date> {
Binding<Date>(get: { self.date}, set : {
self.date = $0
self.setDateString()
})
} // This private var I found… somewhere. I wish I could remember where
// To take the selected date and store it as a string for the text field
private func setDateString() {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM dd, yyyy"
self.selectedDateText = formatter.string(from: self.date)
}
var body: some View {
VStack {
HStack {
Text("Date:")
.frame(alignment: .leading)
TextField("", text: $selectedDateText)
.onAppear() {
self.setDateString()
}
.disabled(true)
.onTapGesture {
self.showPicker.toggle()
}
.multilineTextAlignment(.trailing)
}
if showPicker {
DatePicker("", selection: selectedDate,
displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
}
}
}
}
EDIT: I figured out where I got the private var code. It was from this post: How to detect a value change of a Datepicker using SwiftUI and Combine?

Resources