How to add or minus 1 month from current date? - swiftui

I need to be able to add or minus 1 month from the current date.
So far I have this code:
import SwiftUI
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
var date = Date()
var body: some View {
HStack {
Button(action: {
print("Button Pushed")
}) {
Image(systemName: "chevron.left")
.padding()
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Button(action: {
print("Button Pushed")
}) {
Image(systemName: "chevron.right")
.padding()
}
}
.padding()
.background(Color.yellow)
}
}
struct DateView_Previews: PreviewProvider {
static var previews: some View {
DateView()
}
}
I would like to change the date displayed to be +1 month or -1 month depending on which chevron I will tap.
I am new to swift and swiftui and don't know what action I should use. I think it's related to DateComponents, but what should I do about it now? I am stuck. Please help me.
To better visualise what I have and want to do, here is an image of my current result:

Swift 5
Function to add or subtract month from current date.
func addOrSubtractMonth(month: Int) -> Date {
Calendar.current.date(byAdding: .month, value: month, to: Date())!
}
Now calling the function
// Subtracting
var monthSubtractedDate = addOrSubtractMonth(-7)
// Adding
var monthAddedDate = addOrSubtractMonth(7)
To Add date pass prospective value
To Subtract pass negative value

You can use the Calendar to add or subtract months/days/hours etc to your Date. Apple's documentation on the Calendar can be found here.
Below is a working example, showing how to increase/decrease the month by 1.
struct ContentView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
#State var date = Date()
var body: some View {
HStack {
Button(action: {
print("Button Pushed")
self.changeDateBy(-1)
}) {
Image(systemName: "chevron.left")
.padding()
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Button(action: {
print("Button Pushed")
self.changeDateBy(1)
}) {
Image(systemName: "chevron.right")
.padding()
}
}
.padding()
.background(Color.yellow)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}

Related

SwiftUI passing selected date from modal to parent variables

Need help with this please.
I have a view with 2 date variables and I want to show a modal which have the datepicker and let user pick different dates for these variables.
Currently I have two buttons that show the same sheet but pass different variable to the modal.
The problem the variable don’t update after dismissing the modal.
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerPresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerPresented)
{
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}
import SwiftUI
struct DatePickView: View {
#Environment(\.dismiss) private var dismiss
#Binding var selectDate: Date
var body: some View {
VStack(alignment: .center, spacing: 20) {
HStack {
Text("\(selectDate)")
.padding()
Spacer()
Button {
dismiss()
} label: {
Image(systemName: "delete.backward.fill")
.foregroundColor(.indigo)
}
}.padding()
DatePicker("", selection: $selectDate)
.datePickerStyle(.graphical)
}
}
}
First of all, thank you for your minimal, reproducible example: it is clear and can be immediately used for debugging. Answering to your question:
The problem with your code is that you have only one variable that opens the sheet for both dates. Even though you are correctly passing the two different #Bindings, when you toggle isDatepickerPresented you are asking SwiftUI to show both sheets, but this will never happen. Without knowing, you are always triggering the first of the sheet presentations - the one that binds secOneDate. The sheet that binds secTwoDate is never shown because you can't have two sheets simultaneously.
With that understanding, the solution is simple: use two different trigger variables. Here's the code corrected (DatePickView doesn't change):
struct Example: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerOnePresented = false
#State private var isDatepickerTwoPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerOnePresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerOnePresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerTwoPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerTwoPresented) {
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}

Initializing the Date Picker with Date Other Than Today

I need help with dates. How do you initialize the DatePicker with a stored date? Say, for example, that the user entered an entry with a date. He then determines the date is wrong and would like to change the date. But currently the DatePicker in this code will always default to today's date instead of the stored date.
The state parameter startDate can't be initialized with a stored date above the body. It appears that I need to set startDate in the body.
import SwiftUI
import Combine
struct GetDate: View {
#ObservedObject var userData : UserData = UserData()
#State private var startDate = Date()
var body: some View {
//let startDate = userData.startDate
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
// enter start date
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $startDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
userData.saveDates(startDate: startDate)
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
}
}
class UserData: ObservableObject {
#Published var startDate: Date = Date() {
didSet {
UserDefaults.standard.set(startDate, forKey: "startDate") // save
}
}
init() {
// save / retrieve trip dates
if let sdate = UserDefaults.standard.object(forKey: "startDate") as? Date {
startDate = sdate
print("startDate 2 = \(startDate)")
} else {
UserDefaults.standard.set(Date(), forKey: "startDate")
}
}
func saveDates(startDate: Date) -> () {
self.startDate = startDate
UserDefaults.standard.set(self.startDate, forKey: "startDate")
print("startDate 3 = \(self.startDate)")
}
}
Use #AppStorage property wrapper:
struct GetDate: View {
#AppStorage("startDate") var startDate = Date()
#State private var selectedDate = Date()
var body: some View {
VStack (alignment: .leading) {
Form {
Text("startdate 1 = \(startDate)")
Section(header: Text("Enter Start Date")) {
HStack {
Image(systemName: "calendar.badge.clock")
.resizable()
.foregroundColor(Color(.systemRed))
.frame(width: 35, height: 35)
Spacer()
DatePicker("", selection: $selectedDate, in: ...Date(), displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
}
Button ( action: {
startDate = selectedDate // save
}) {
HStack {
Spacer()
Text("Save Dates?")
Spacer()
}
}
}
.font(.body)
.navigationBarTitle("Get Date", displayMode: .inline)
}
.onAppear {
selectedDate = startDate
}
}
}
// Support #AppStorage for `Date` type.
extension Date: RawRepresentable {
private static let formatter = ISO8601DateFormatter()
public var rawValue: String {
Date.formatter.string(from: self)
}
public init?(rawValue: String) {
self = Date.formatter.date(from: rawValue) ?? Date()
}
}
Just reset startDate to userData.startDate in .onAppear().
.onAppear {
startDate = userData.startDate
}

Is it possible to dismiss the DatePicker's calendar when you tap a date?

I have a basic SwiftUI date picker that shows a calendar widget when tapped:
DatePicker(
"Date",
selection: $date,
in: ...Date(),
displayedComponents: [.date]
)
When you select a date (8th October in the example above), the calendar remains on screen and in order to collapse it, you need to tap outside of it.
Is it possible to automatically collapse it when a date is selected?
I ended up with a rather hacky solution that seems to do the job:
Add a #State variable that holds the calendar ID:
#State private var calendarId: Int = 0
Chain the DatePicker call with .id, .onChange and .onTapGesture actions:
DatePicker(
"Date", selection: $date, in: ...Date(), displayedComponents: [.date]
)
.id(calendarId)
.onChange(of: date, perform: { _ in
calendarId += 1
})
.onTapGesture {
calendarId += 1
}
#chris.kobrzak provided a good direction, and I ended up solving this with:
struct ContentView: View {
#State var calendarId: UUID = UUID()
#State var someday: Date = Date()
var body: some View {
VStack {
DatePicker("Day", selection: $someday, displayedComponents: [.date])
.labelsHidden()
.id(calendarId)
.onChange(of: whatday) { _ in
calendarId = UUID()
}
AnotherView(someday)
}
}
}
This is just an updated answer following #Chris Kobrzak as above.
I am using XCode 14.1 and iOS 15+ and 16+ (iPad and iPhone) and it seems to work without error today in Nov 2022.
I have seen some folk using the same .id() method complain that it doesn’t work.
I haven’t tested this but note that I am using the CompactDatePickerStyle(), maybe it doesn’t work the same on other styles.
The reason this hack works is the .id() is for the ‘view’ (DatePicker being a view). When you change the id of a view you basically reset it (in this case closing the DatePicker).
There is a good explanation about .id() here: https://swiftui-lab.com/swiftui-id/
Why this isn’t built into the control seems rather a joke but hey…
Note I have ripped the following out of a real App. I've edited it in a dumb text editor to post on here so there may be some silly syntax errors and odd remnants of the original code.
import SwiftUI
struct FooView: View {
#Published var dateOfBirth: Date = Date()
#State private var datePickerId: Int = 0
private var dateOfBirthRange: ClosedRange<Date> {
let dateFrom = Calendar.current.date(byAdding: .year, value: -160, to: Date())!
let dateTo: Date = Date()
return dateFrom...dateTo
}
var body: some View {
Form {
ZStack(alignment: .leading) {
Text("Date of Birth")
.offset(y: -36)
.foregroundColor(Color.accentColor)
.scaleEffect(0.9, anchor: .leading)
DatePicker(
"",
selection: $dateOfBirth,
in: dateOfBirthRange,
displayedComponents: .date
)
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
.id(datePickerId)
.onChange(of: dateOfBirth) { _ in
datePickerId += 1
}
}
.padding(.top, 24)
.animation(.default, value: "")
}
}
}
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 but that's ok.
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
}
}
}

How to make a Dynamic PageViewController in SwiftUI?

Anyone know how to make a dynamic pageView controller in SwiftUI, iOS 14? Something that displays pages that are a function of their date so that one can scroll left or right to look at data from the past, present and future.
struct DatePg: View
{
let date: Date
var body: some View {
Text(date.description)
}
}
There is a new API that allows one to make a PageViewController with the TabView and a viewModifier. But the only examples I've seen are static. Here's an example of a static PageView.
import SwiftUI
struct SwiftUIPageView: View
{
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("Hello")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.tag(0)
Text("World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(1)
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
Already have something working using UIHostingController but passing NSManageObjectContext through the UIKit objects is cuasing problems.
Here's where I'm at so far. Still not working.
import SwiftUI
#main struct PagerApp: App
{
var body: some Scene {
WindowGroup { DatePageView() }
}
}
struct DatePageView: View
{
#StateObject var dateData = DateData(present: Date())
#State var index: Int = 1
var body: some View {
TabView(selection: $index) {
ForEach(dateData.dates, id: \.self) { date in
Text(date.description)
.onAppear { dateData.current(date: date) }
.tag(dateData.tag(date: date))
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
class DateData: ObservableObject
{
#Published var dates: [Date]
init(present: Date) {
let past = present.previousDay()
let future = present.nextDay()
self.dates = [past, present, future]
}
func current(date: Date) {
//center around
guard let i = dates.firstIndex(of: date) else { fatalError() }
self.dates = [ dates[i].previousDay(), dates[i], dates[i].nextDay() ]
print("make item at \(i) present")
}
func tag(date: Date) -> Int {
guard let i = dates.firstIndex(of: date) else { fatalError() }
return i
}
}
You can create view dynamically using an array and ForEach.
Here is an example using an array of strings:
// See edited section
You could pass the items you want in the View initializer
Edit:
Here is an example for adding a new page each time I reach the last one:
struct SwiftUIPageView: View
{
#State private var selection = "0"
#State var items: [String] = ["0", "1", "2", "3"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(item)
}
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if Int(value) == (self.items.count - 1) {
self.items.append("\(self.items.count)")
}
})
.id(items)
}
}
The last id(items) is important because it forces the View to reload when the array changes.

In SwiftUI how can I use #State and #Binding to deal with different types?

As the title might not very clear, below is an example:
I have a View which is just DatePicker, name is "MIMIRxDatePicker"
struct MIMIRxDatePicker: View {
#Binding var dobStr: String
#Binding var screenShouldGrayOut: Bool
var body: some View {
VStack {
Text("Select Date of Birth: \(dateFormatter.string(from: self.dobStr))")
DatePicker(selection: self.dobStr , in: ...Date(), displayedComponents: .date) {
Text("")
}
Button(action: {
withAnimation {
self.screenShouldGrayOut.toggle()
}
}) {
Text("Choose").foregroundColor(.white).padding()
}.background(Color(Constants.ThemeColor)).cornerRadius(20)
}.cornerRadius(10).frame(width: 270).padding(20).background(Color(.white))
}
}
Heres is MIMIRxDatePicker s parent view: SignupContentView
struct SignupContentView: View {
#State var email: String = ""
#State var firstName: String = ""
#State var lastName: String = ""
#State var dobStr: String = ""
#State var screenShouldGrayOut: Bool = false
var body: some View {
ZStack {
Color(Constants.ThemeColor)
ScrollView(.vertical) {
Spacer().frame(height: 50)
VStack (alignment: .center, spacing: 2) {
Spacer()
Group {
Group {
HStack {
Text("Email:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "envelope").foregroundColor(.white)
TextField("Email", text: $email).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
}
Group {
HStack {
Text("First Name:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "person").foregroundColor(.white)
TextField("First Name", text: $firstName).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
}
Group {
HStack {
Text("Last Name:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "person").foregroundColor(.white)
TextField("Last Name", text: $lastName).foregroundColor(.white)
}.modifier(AuthTextFieldContainerModifier())
Spacer()
HStack { GenderSelector() }
}
Group {
HStack {
Text("Date of Birth:").modifier(AuthTextLabelModifier())
Spacer()
}.padding(2)
HStack {
Image(systemName: "calendar").foregroundColor(.white)
TextField("", text: $dobStr).foregroundColor(.white).onTapGesture {
withAnimation {
self.screenShouldGrayOut.toggle()
}
}
}.modifier(AuthTextFieldContainerModifier())
}
}
}.padding()
}.background(screenShouldGrayOut ? Color(.black) : Color(.clear)).navigationBarTitle("Sign Up", displayMode: .inline)
if screenShouldGrayOut {
MIMIRxDatePicker(dobStr: $dobStr, screenShouldGrayOut: $screenShouldGrayOut).animation(.spring())
}
}.edgesIgnoringSafeArea(.all)
}
}
As you can see the #State var dobStr in "SignupContentView" is a String which is Date of birth's TextField needed, but in the MIMIRxDatePicker the DatePicker's selection param needs a type of Date, not a string, even if I declared a #Binding var dobStr: String in MIMIRxDatePicker.
I also tried:
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
and do:
DatePicker(selection: dateFormatter.date(from: self.dobStr) , in: ...Date()....
But it didn't work.
I know that assume if the DatePicker is a TextField then everything will work and good to go because TextField accept String only also, but that's not the case, I need the DatePicker.
So in SwiftUI how can I use #State and #Binding to deal with different types?
What you can do is to use Date for date manipulation and String for displaying the date.
Which means you can use Date variable in your Picker and String in the Text views.
struct MIMIRxDatePicker: View {
#State var dob: Date
...
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
var dobStr: String {
dateFormatter.string(from: self.dob)
}
var body: some View {
VStack {
// `String` for displaying...
Text("Select Date of Birth: \(self.dobStr)")
// and `Date` for date manipulation...
DatePicker(selection: self.$dob, in: ...Date(), displayedComponents: .date) {
Text("")
}
...
}
...
}
}
Then follow the same pattern in your SignupContentView. Generally try to use Date objects for date manipulation as they are less prone to errors and malformed data.
You need to first identify the types involved.
You are passing a Binding<String> to selection: which expects a Binding<Date>.
#Binding var dobStr: String is still Binding<String>
dateFormatter.date(from: self.dobStr) is Date, not Binding<Date>
What you need is to create a custom Binding<Date>, with its getter/setter interacting with Binding<String>
E.g.;
Binding(
get: { // return a Date from dobStr binding },
set: { // set dobStr binding from a Date}
)
Then use this Binding as argument to selection: