calling an API when date changed in DatePicker SwiftUI - datepicker

I saw this link but I want a straight callback from Datepicker so I can call a method. In this link I should add a Slider and in slider's listener call the method.
Is there any better solution to call a method when the DatePicker's date changed?

how about something simple like this:
#State var date = Date()
var body: some View {
DatePicker(selection: Binding(get: {
self.date
}, set: { newVal in
self.date = newVal
self.doSomething(with: newVal)
})) {
Text("")
}
}
func doSomething(with: Date) {
print("-----> in doSomething")
}

You could try this:
DatePicker("Date", selection: $date)
.onChange(of: date) {
//API goes here
}
print($0)
}

Related

View parameter only passed to view when application first loads - SwiftUI

I have a navigation view (SettingsView) as shown below. When I go to PayScheduleForm the first time, the date from UserDefaults is passed properly. If I change the payDate value in PayScheduleForm, I can see that it updates the UserDefaults key properly. However, if I go back to SettingsView, and then go to PayScheduleForm again, the original value is still shown in the picker.
It's kind of an odd scenario so maybe it's better explained step by step:
Start App
Go to Settings -> Pay Schedule
Last UserDefaults payDate value is in DatePicker (10/08/2020)
Change value to 10/14/2020 - console shows that string of UserDefaults.standard.object(forKey: "payDate") = 10/14/2020
Go back to settings (using back button)
Go back to Pay Schedule and see that DatePicker has its original value (10/08/2020)
Of course if I restart the app again, I see 10/14/2020 in the DatePicker
struct SettingsView: View {
var body: some View {
NavigationView{
if #available(iOS 14.0, *) {
List{
NavigationLink(destination: AccountsList()) {
Text("Accounts")
}
NavigationLink(destination: CategoriesList()) {
Text("Categories")
}
NavigationLink(destination: PayScheduleForm(date: getPayDate()).onAppear(){
getPayDate()
}) {
Text("Pay Schedule")
}
}.navigationTitle("Settings")
} else {
// Fallback on earlier versions
}
}.onAppear(perform: {
getUserDefaults()
})
}
func getPayDate() -> Date{
var date = Date()
if UserDefaults.standard.object(forKey: "payDate") != nil {
date = UserDefaults.standard.object(forKey: "payDate") as! Date
}
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
return date
}
struct PayScheduleForm: View {
var frequencies = ["Bi-Weekly"]
#Environment(\.managedObjectContext) var context
#State var payFrequency: String?
#State var date: Date
var nextPayDay: String{
let nextPaydate = (Calendar.current.date(byAdding: .day, value: 14, to: date ))
return Utils.dateFormatterMed.string(from: nextPaydate ?? Date() )
}
var body: some View {
Form{
Picker(selection: $payFrequency, label: Text("Pay Frequency")) {
if #available(iOS 14.0, *) {
ForEach(0 ..< frequencies.count) {
Text(self.frequencies[$0]).tag(payFrequency)
}.onChange(of: payFrequency, perform: { value in
UserDefaults.standard.set(payFrequency, forKey:"payFrequency")
print(UserDefaults.standard.string(forKey:"payFrequency")!)
})
} else {
// Fallback on earlier versions
}
}
if #available(iOS 14.0, *) {
DatePicker(selection: $date, displayedComponents: .date) {
Text("Last Payday")
}
.onChange(of: date, perform: { value in
UserDefaults.standard.set(date, forKey: "payDate")
let date = UserDefaults.standard.object(forKey: "payDate") as! Date
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
print(df.string(from: date))
})
} else {
// Fallback on earlier versions
}
}
```
Fixed this. Posting so others with the same issue can find the answer.
iOS 14 allows the #AppStorage property wrapper which easily allows access to UserDefaults, BUT the Date type is not allowed with #AppStorage as of now.
Instead you can use an #Observable object
First create a swift file called UserSettings.swift and create a class in there:
class UserSettings: ObservableObject {
#Published var date: Date {
didSet {
UserDefaults.standard.set(date, forKey: "payDate")
}
}
init() {
self.date = UserDefaults.standard.object(forKey: "payDate") as? Date ?? Date()
}
}
Then add an #ObservableObject to your view
#ObservedObject var userSettings = UserSettings()
...
DatePicker(selection: $userSettings.date, displayedComponents: .date) {
Text("Last Payday")
}

SwiftUI - how to update a item in a struct using a textfield and .onchange

I have the following code
struct ContentView: View {
#ObservedObject var list = ModelList.shared
var body: some View {
NavigationView {
List(list.sorted()) {object in
NavigationLink(destination: ModelView(object: object)) {
Text(object.title)
}
}
}
}
}
struct ModelView: View {
#State var object: ModelObject
var body: some View {
VStack {
Text(object.title)
TextField("Label", text: self.$object.text) // xxxxx Error on this line
.onChange(of: self.$object.text) { newValue in
print("Text changed to \(self.$object.text)!")
}
Button("Use") {
self.object.updateDate = Date()
print("title: \(object.title) - text: \(object.text) - date: \(object.updateDate)")
ModelList.shared.objectWillChange.send()
}
}
}
}
class ModelObject: ObservableObject {
#Published var updateDate: Date = Date()
let title: String
var text: String
init(title: String) {
self.title = title
self.text = ""
print(self)
}
}
I do get the error - Instance method 'onChange(of:perform:)' requires that 'Binding' conform to 'Equatable' on line XXXXX
However if I remove the textfield on change line then it compiles and have the code working. But I want to have some action be done when the Textfield get changed and the data to be saved in the struct in the array?
What am I missing here?
Thank you.
.onChange(of:perform:) doesn't take a Binding. Just pass the value. The same is true in the print statement:
TextField("Label", text: self.$object.text)
.onChange(of: self.object.text) { newValue in // removed $
print("Text changed to \(self.object.text)!") // removed $
}
Here is a minimal testable example that demonstrates the problem:
struct ContentView: View {
#State private var string = "hello"
var body: some View {
TextField("Label", text: self.$string)
.onChange(of: self.$string) { newValue in // remove $ here
print("Text changed to \(self.$string)") // remove $ here
}
}
}

How can I convert a Binding<Date?> to a Binding<Date>? [duplicate]

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]
)

How do I change the appearance of the DatePicker in the SwiftUI framework to only months and years?

I want to change the DatePicker's date view. I just want to get a month and year selection. I want to assign ObservedObject to a variable at each selection.
My Code:
#State private var date = Date()
var body: some View {
DatePicker("", selection: $date, in: Date()..., displayedComponents: .date)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
.onDisappear(){
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yyyy"
self.cardData.validThru = dateFormatter.string(from: self.date)
}
}
As others have already commented You would need to implement an HStack with two Pickers:
struct ContentView: View {
#State var monthIndex: Int = 0
#State var yearIndex: Int = 0
let monthSymbols = Calendar.current.monthSymbols
let years = Array(Date().year..<Date().year+10)
var body: some View {
GeometryReader{ geometry in
HStack(spacing: 0) {
Picker(selection: self.$monthIndex.onChange(self.monthChanged), label: Text("")) {
ForEach(0..<self.monthSymbols.count) { index in
Text(self.monthSymbols[index])
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
Picker(selection: self.$yearIndex.onChange(self.yearChanged), label: Text("")) {
ForEach(0..<self.years.count) { index in
Text(String(self.years[index]))
}
}.frame(maxWidth: geometry.size.width / 2).clipped()
}
}
}
func monthChanged(_ index: Int) {
print("\(years[yearIndex]), \(index+1)")
print("Month: \(monthSymbols[index])")
}
func yearChanged(_ index: Int) {
print("\(years[index]), \(monthIndex+1)")
print("Month: \(monthSymbols[monthIndex])")
}
}
You would need this helper from this post to monitor the Picker changes
extension Binding {
func onChange(_ completion: #escaping (Value) -> Void) -> Binding<Value> {
.init(get:{ self.wrappedValue }, set:{ self.wrappedValue = $0; completion($0) })
}
}
And this calendar helper
extension Date {
var year: Int { Calendar.current.component(.year, from: self) }
}

DatePicker using Time-Interval in SwiftUI

I want to use a DatePicker in SwiftUI, it is working fine and as expected. I want to add an Time-interval, like explained: UIDatePicker 15 Minute Increments Swift
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
Is there a Modifier for that in SwifUI?
I don't believe there is a modifier for this. However it's possible to "do it yourself" by using UIViewRepresentable to wrap a UIDatePicker:
The basic structure for this code is based on the Interfacing with UIKit tutorial.
struct MyDatePicker: UIViewRepresentable {
#Binding var selection: Date
let minuteInterval: Int
let displayedComponents: DatePickerComponents
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MyDatePicker>) -> UIDatePicker {
let picker = UIDatePicker()
// listen to changes coming from the date picker, and use them to update the state variable
picker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged), for: .valueChanged)
return picker
}
func updateUIView(_ picker: UIDatePicker, context: UIViewRepresentableContext<MyDatePicker>) {
picker.minuteInterval = minuteInterval
picker.date = selection
switch displayedComponents {
case .hourAndMinute:
picker.datePickerMode = .time
case .date:
picker.datePickerMode = .date
case [.hourAndMinute, .date]:
picker.datePickerMode = .dateAndTime
default:
break
}
}
class Coordinator {
let datePicker: MyDatePicker
init(_ datePicker: MyDatePicker) {
self.datePicker = datePicker
}
#objc func dateChanged(_ sender: UIDatePicker) {
datePicker.selection = sender.date
}
}
}
struct DatePickerDemo: View {
#State var wakeUp: Date = Date()
#State var minterval: Int = 1
var body: some View {
VStack {
Stepper(value: $minterval) {
Text("Minute interval: \(minterval)")
}
MyDatePicker(selection: $wakeUp, minuteInterval: minterval, displayedComponents: .hourAndMinute)
Text("\(wakeUp)")
}
}
}
struct DatePickerDemo_Previews: PreviewProvider {
static var previews: some View {
DatePickerDemo()
}
}
Not fully a SwiftUI solution, but much simpler than the existing answer. Attach this to your main body View that contains the picker
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
The one downside is this will apply to all pickers in the view and can affect pickers in other views in the app, but you can always do the same thing in those views (set minute interval back to 1).
I think, the below will help..
struct DatePickerView: View {
#Binding var dateSelected: Date
var type: DatePickerComponents? = .date
var body: some View {
DatePicker("", selection: $dateSelected, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.onAppear {
if type == .hourAndMinute {
UIDatePicker.appearance().minuteInterval = 15
} else {
UIDatePicker.appearance().minimumDate = dateSelected
}
}
}
}
It is also possible with Introspect lib
DatePicker(...)
.introspectDatePicker {
$0.minuteInterval = 30
$0.roundsToMinuteInterval = true
}