How to set the time to a standard value when using a DatePicker in SwiftUI - swift

I have a date-picker in my app, which only selects the date, not the time. Looks like this:
DatePicker("Birthday",
selection: $date,
displayedComponents: [.date])
A date picker gives you a date object to work with. So, when the user selects a date, it of course also comes with a time, timezone, etc. My problem is, that the time is set to the current time by default. However, I need it to be set to midnight always. How can I provide a default value?
A possible solution that came to my mind was using a formatter and storing the date as a String (instead of a Date), but since I need to do some work with it later (e.g. calculate days between two dates), this doesn't seem like the best approach to me.

you could try this:
struct ContentView: View {
#State var date = Date()
var body: some View {
VStack {
Text("\(date)")
DatePicker("Birthday", selection: $date, displayedComponents: [.date])
.onChange(of: date) { newDate in
if let midnight = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: newDate) {
date = midnight
}
}
}
}
}
or this:
struct ContentView: View {
#State var date = Date()
var body: some View {
VStack {
Text("\(date)")
DatePicker("Birthday", selection: $date, displayedComponents: [.date])
}
.onAppear {
if let midnight = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()) {
date = midnight
}
}
}
}
or this:
struct ContentView: View {
#State var date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())!
var body: some View {
VStack {
Text("\(date)")
DatePicker("Birthday", selection: $date, displayedComponents: [.date])
}
}
}

Related

How to make the date change simultaneously in all views?

Date not change simultaneously in all views.
I want to link two calendars. Standard and custom. But they don't connect.
When I change the date in one, it doesn't change in the other.
I made Published:
import Combine
import Foundation
class CustomCalendar: ObservableObject {
#Published var currentDate = Date()
var currentThreeWeek: [Date] = []
init() {
fetchCurrentThreeWeek()
}
func fetchCurrentThreeWeek() {
let calendar = Calendar.current
var todayDay = DateInterval(start: Date(), duration: 1814400).start
let lastDay = DateInterval(start: Date(), duration: 1814400).end
currentThreeWeek.append(todayDay)
while todayDay < lastDay {
todayDay = calendar.date(byAdding: .day, value: 1, to: todayDay)!
currentThreeWeek.append(todayDay)
}
}
func extractDate(date: Date, format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
dateFormatter.locale = Locale(identifier: "rus")
return dateFormatter.string(from: date)
}
func isToday(date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDate(currentDate, inSameDayAs: date)
}
}
When I select a date it doesn't change in other views.
import SwiftUI
struct FilterView: View {
#StateObject private var calendar = CustomCalendar()
#Binding var filterViewIsPresented: Bool
let todayDay = DateInterval(start: Date(), duration: 1814400).start
let lastDay = DateInterval(start: Date(), duration: 1814400).end
var body: some View {
VStack {
DatePicker("", selection: $calendar.currentDate, in: todayDay...lastDay, displayedComponents: .date)
.labelsHidden()
.environment(\.locale, Locale.init(identifier: "ru"))
HorizontalCalendarView()
HorizontalCalendarView()
}
}
}
struct FilterView_Previews: PreviewProvider {
static var previews: some View {
FilterView(filterViewIsPresented: .constant(false))
}
}
Custom calendar. On tap Gesture I change currentDate
import SwiftUI
struct HorizontalCalendarView: View {
#StateObject private var calendar = CustomCalendar()
var body: some View {
ScrollViewReader { value in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(calendar.currentThreeWeek, id: \.self) { day in
VStack(spacing: 0) {
Text(calendar.extractDate(date: day, format: "dd"))
.font(.title3)
.fontWeight(.bold)
Text(calendar.extractDate(date: day, format: "EEE"))
RoundedRectangle(cornerRadius: 10, style: .continuous)
.frame(width: calendar.isToday(date: day) ? 40 : 0, height: 5)
.opacity(calendar.isToday(date: day) ? 1 : 0)
.padding(4)
}
.frame(width: 45, height: 45)
.foregroundStyle(calendar.isToday(date: day) ? .primary : .secondary )
.foregroundColor(calendar.isToday(date: day) ? .white : .black)
.padding(8)
.background(
ZStack {
if calendar.isToday(date: day) {
RoundedRectangle(cornerRadius: 10, style: .continuous)
}
}
)
.onTapGesture {
withAnimation(.easeIn(duration: 0.2)) {
calendar.currentDate = day
value.scrollTo(calendar.currentDate, anchor: .leading)
}
}
}
.padding(9)
}
}
Text(calendar.currentDate.formatted())
}
}
}
struct HorizontalCalendarView_Previews: PreviewProvider {
static var previews: some View {
HorizontalCalendarView()
}
}
How can I do this?
You have two options:
Pass calendar in HorizontalCalendarView constructor and use #ObservedObject property wrapper instead of #StateObject:
struct HorizontalCalendarView: View {
#ObservedObject private var calendar: CustomCalendar
init(_ calendar: CustomCalendar) {
self.calendar = calendar
}
...
and just pass it in FilterView
HorizontalCalendarView(calendar)
Another option is to use #EnvironmentObject (it's preferred method for deeply nested views in your example option 1 is better):
struct HorizontalCalendarView: View {
#EnvironmentObject private var calendar: CustomCalendar = CustomCalendar()
...
then you have to pass calendar with environmentObject modifier:
HorizontalCalendarView().environmentObject(calendar)
Note: in order to #EnvironmentObject works as expected it is not necessary to use environmentObject modifier on actual view, you can use this modifier in any of parent views. That makes it perfect for deep nested views

Combine Date in swiftui to obtain only one Date

Hello I have two date picker one only for date and another for hour and minute
if(showStartDatePicker) {
DatePicker("Seleziona data", selection: $selectedStartDate,/* in: startingDate...endingDate,*/ displayedComponents: [.date])
.datePickerStyle(.graphical)
} else if(showStartTimePicker) {
DatePicker("Select a date", selection: $selectedStartTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
}
I need to obtain a single date that is the merge of the two date picker
So how can I do?
Just share the variable instead of having 2
struct DoubleDatePickerView: View {
#State var showStartDatePicker: Bool = false
#State var selectedStartDate: Date = Date()
var body: some View {
VStack{
Text(selectedStartDate.description)
if(showStartDatePicker) {
DatePicker("Seleziona data", selection: $selectedStartDate,/* in: startingDate...endingDate,*/ displayedComponents: [.date])
.datePickerStyle(.graphical)
Button("show time picker", action: {
showStartDatePicker.toggle()
})
} else {
DatePicker("Select a date", selection: $selectedStartDate, displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
Button("show date picker", action: {
showStartDatePicker.toggle()
})
}
}
}
}
The other option is to combine the desired components from each variable
var combined: Date{
let timeComponents: DateComponents = Calendar.current.dateComponents([.hour,.minute,.second,.timeZone], from: selectedStartTime)
let dateComponents: DateComponents = Calendar.current.dateComponents([.year,.month,.day], from: selectedStartDate)
let combined: DateComponents = .init(calendar: .current, timeZone: timeComponents.timeZone, year: dateComponents.year, month: dateComponents.month, day: dateComponents.day, hour: timeComponents.hour, minute: timeComponents.minute, second: timeComponents.second)
return Calendar.current.date(from: combined) ?? Date()
}

SwiftUI - Date handling

maybe you can help me with following question:
I am programming a UI with two date pickers (startDatePicker and endDatePicker).
The startDatePicker should be updated itself to the date of endDatePicker in UI when the date of startEndPicker is smaller than the date of endDatePicker.
Do you habe any idea how I can realise that?
content.swift
import SwiftUI
struct ContentView: View {
#State var startDate = Date()
#State var endDate = Date()
#ObservedObject var dateModel = Period.shared
var body: some View {
VStack{
startDatePicker
endDatePicker
}
.onAppear {
// print("VStack")
// dateModel.startDate = startDate
// dateModel.endDate = endDate
}
}
var startDatePicker: some View{
DatePicker("Start", selection: $startDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
.onAppear(perform: {
print("StartDate.onAppear")
dateModel.startDate = dateModel.toLocalTime(date: startDate, type: true)
print(dateModel.startDate)
})
.onChange(of: startDate, perform: { startDate in
print("StartDate.onChange")
dateModel.startDate = dateModel.toLocalTime(date: startDate, type: true)
print(dateModel.startDate)
})
}
var endDatePicker: some View{
DatePicker("End", selection: $endDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
.onAppear(perform: {
print("EndDate.onAppear")
dateModel.endDate = dateModel.toLocalTime(date: endDate, type: false)
print(dateModel.endDate)
})
.onChange(of: endDate, perform: { endDate in
print("EndDate.onChange")
dateModel.endDate = dateModel.toLocalTime(date: endDate, type: false)
if dateModel.endDate < dateModel.startDate{
print("Error")
dateModel.startDate = dateModel.endDate
}
print(dateModel.endDate)
})
}
}
Datahandler.swift
class Period : ObservableObject{
static let shared = Period()
#Published var startDate: Date = Date()
#Published var endDate: Date = Date()
func toLocalTime(date : Date, type: Bool) -> Date {
var startDate : Date?
var endDate : Date?
var dateLocalTimezone : Date?
//Auswahl der aktuellen Kalender
let calendar = Calendar.current
//Auswahl der Zeitzone
let timezone = TimeZone.current
//Bestimmen Anzahl Sekunden zwischen Zeitzone und GMT
let seconds = TimeInterval(timezone.secondsFromGMT(for: date))
//Anpassen des eingelesenen Werts
if type == true {
startDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: startDate!)
}else{
endDate = calendar.date(bySettingHour: 23, minute: 59, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: endDate!)
}
return dateLocalTimezone!
}
}
Is there a better way for the code? The idea is to separate the part for dates from the UI.
You're almost there. You can move logic from onAppeat to init(), and from .onChange to didSet, like this:
struct ContentView: View {
#ObservedObject var dateModel = Period.shared
var body: some View {
VStack{
startDatePicker
endDatePicker
}
}
var startDatePicker: some View{
DatePicker("Start", selection: $dateModel.startDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
}
var endDatePicker: some View{
DatePicker("End", selection: $dateModel.endDate, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.frame(width: 250, height: 50, alignment: .center)
}
}
class Period : ObservableObject{
static let shared = Period()
#Published var startDate: Date {
didSet {
let localTime = Self.toLocalTime(date: startDate, type: true)
if startDate != localTime {
startDate = localTime
}
}
}
#Published var endDate: Date {
didSet {
let localTime = Self.toLocalTime(date: endDate, type: false)
if endDate != localTime {
endDate = localTime
}
if endDate < startDate{
print("Error")
startDate = endDate
}
print(endDate)
}
}
init() {
startDate = Self.toLocalTime(date: Date(), type: true)
endDate = Self.toLocalTime(date: Date(), type: false)
}
static private func toLocalTime(date : Date, type: Bool) -> Date {
var startDate : Date?
var endDate : Date?
var dateLocalTimezone : Date?
//Auswahl der aktuellen Kalender
let calendar = Calendar.current
//Auswahl der Zeitzone
let timezone = TimeZone.current
//Bestimmen Anzahl Sekunden zwischen Zeitzone und GMT
let seconds = TimeInterval(timezone.secondsFromGMT(for: date))
//Anpassen des eingelesenen Werts
if type == true {
startDate = calendar.date(bySettingHour: 00, minute: 00, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: startDate!)
}else{
endDate = calendar.date(bySettingHour: 23, minute: 59, second: 00, of: date)
dateLocalTimezone = Date(timeInterval: seconds, since: endDate!)
}
return dateLocalTimezone!
}
}
Your toLocalTime returns wrong value for end date, like I pass 2021-08-18 23:59:00 +0000 and the result is 2021-08-19 23:59:00 +0000 which is the next day. This proceeds to recursion of didSet, but I'll leave fixing this logic to your
It's not a good fit for SO to ask for "a better way", since every solution has its pros and cons and thus there are a lot of opinions. But the latter I guess, is pretty much consolidated (which is in itself highly opinionated ;) )
I would like to add some refactoring suggestions:
You can make the DatePickerView a reusable component:
struct DatePickerView: View {
let title: String
let date: Date
let setDateAction: (Date) -> Void
var body: some View {
let binding: Binding<Date> = .init {
return self.date
} set: { newValue in
self.setDateAction(newValue)
}
DatePicker(title, selection: binding, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
}
}
Then, it can be used in the ContentView as follows:
struct ContentView: View {
let state: TwoDatePickers.ViewModel.ViewState
let setStartDate: (Date) -> ()
let setEndDate: (Date) -> ()
var body: some View {
VStack{
Text("Start: \(state.startDate)")
.padding()
Text("End: \(state.endDate)")
.padding()
DatePickerView(
title: "Start",
date: state.startDate,
setDateAction: self.setStartDate)
.padding()
DatePickerView(
title: "End",
date: state.endDate,
setDateAction: self.setEndDate)
.padding()
}
}
}
You might notice that ContentView has no coupling to any certain kind of view model or model, and no logic what so ever. That's by intention to make it reusable as well and let the logic be done elsewhere.
Now, you can implement your view model plus logic as a separated component as well. I am cheating here a bit, since it internally uses a reusable "store" component ;)
extension TwoDatePickers {
static let store = Oak.Store(state: .init(),
update: update,
scheduler: DispatchQueue.main)
final class ViewModel: ObservableObject {
typealias ViewState = TwoDatePickers.State
init() {
cancellableState = store.sink { state in
self.viewState = self.view(state)
}
}
#Published
private(set) var viewState: ViewState = .init()
private var cancellableState: AnyCancellable!
private func view(_ state: State) -> ViewState { state }
func setStartDate(_ date: Date) {
store.input.send(.setStartDate(date))
}
func setEndDate(_ date: Date) {
store.input.send(.setEndDate(date))
}
}
}
Note, that the ViewModel is just a thin wrapper which connects the store and provides a view function which returns a ViewState from the store's state. The ViewState is the thing that completely and unambiguously defines what the view should render, while the store's State is there to perform the logic and may contain additional data. That is, ViewState is a function of State.
The "store" is implemented much like in Redux or Elm, which can be done in a few lines of code.
As mentioned, this is also a reusable component. It is event driven, unidirectional and uses internally a Finite State Machine to change state and generate outputs.
So, you have to implement an update function, which is basically the heart of your logic:
extension TwoDatePickers {
struct State {
var startDate: Date = Date()
var endDate: Date = Date()
}
enum Event {
case setStartDate(Date)
case setEndDate(Date)
}
typealias Command = Void
static func update(state: State, setter: (State) -> Void, event: Event) -> Void {
switch (state, event) {
case (_, .setStartDate(let date)):
let adjustedEndDate = Date(
timeIntervalSinceReferenceDate: max(
date.timeIntervalSinceReferenceDate,
state.endDate.timeIntervalSinceReferenceDate))
setter(State(startDate: date, endDate: adjustedEndDate))
case (_, .setEndDate(let date)):
let adjustedStartDate = Date(
timeIntervalSinceReferenceDate: min(
date.timeIntervalSinceReferenceDate,
state.startDate.timeIntervalSinceReferenceDate))
setter(State(startDate: adjustedStartDate, endDate: date))
default:
print("not handled: \(state), \(event)")
break
}
}
}
To wire all the things together, we may use a "root view". Here you can see how eventually the view model gets connected to the views:
struct TwoDatePickersSceneView: View {
#StateObject var viewModel = TwoDatePickers.ViewModel()
var body: some View {
TwoDatePickers.ContentView(
state: viewModel.viewState,
setStartDate: viewModel.setStartDate,
setEndDate: viewModel.setEndDate
)
}
}
What's left is the reusable implementation of the store. I post it here as a bonus:
https://gist.github.com/couchdeveloper/4100f1ec8470980c5c49adc119240de1
Final note:
This is an example how we can decompose a typical SwiftUI feature and separate it into several reusable components. You don't have to do this extra effort when the feature is small and tidy. However, this solution, especially using a Finite State Machine to solve UI problems scales much better for more complex problems.

SwiftUI: how to update the range of a ForEach loop based on the value of a Picker

So I'm trying to have a ForEach loop update the amount of times a View is looped based on what Month (value in a picker) is selected. In my case, they will be looping based on the number of days in the month of the selected month for the given year. I already have a function that gives me the number of days in each month, however, when I plug that into the ForEach Loop, it only gets run based on the first month selected and stays iterating the number of days of that month for the rest. Here is my code for the ForEach Loop:
ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
NavigationLink(destination: ContentView(day: yearData[yearIndex].months[monthIndex].dayInfo[i])) {
DayRow(day: yearData[yearIndex].months[monthIndex].dayInfo[i])
}
}
and here is the getRange() Function:
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
}
The yearIndex variable is linked to the picker value of three years, (2020, 2021, 2022). Here is the code for it:
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { i in
Text(String(self.year[i])).tag(i)
}
}
.pickerStyle(SegmentedPickerStyle())
The monthIndex variable is linked to the picker with the months in the year (Jan-Dec). Here is the code for it:
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { i in
Text(self.monthArray[i]).tag(i)
}
}
.padding(.bottom, 2)
I'm not sure what I'm doing wrong, and I'm not sure how to do this, so any help would be greatly appreciated! I'm still quite new to Swift/SwiftUI, so any advice to better code this would also be appreciated!
EDIT: Here is a minimal reproducible example as requested:
struct ContentView: View {
#State var year = [2020, 2021, 2022]
//monthSymbols gets an array of all the months
#State var monthArray = DateFormatter().monthSymbols!
#State var yearIndex = 0
#State var monthIndex = 0
#State var indexCheck = 0
#State var indexTest = 0
var body: some View {
NavigationView {
List {
Section {
VStack {
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { i in
Text(String(self.year[i])).tag(i)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { i in
Text(self.monthArray[i]).tag(i)
}
}
.padding(.bottom, 2)
}
}
Section(header: Text("What I love about you")) {
ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
NavigationLink(destination: DetailsView()) {
Text("Row \(i)")
}
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle(Text("\(monthArray[monthIndex + indexCheck]) \(String(year[yearIndex]))"))
}
}
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
}
}
struct YearView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I cleaned a little bit your code so it's more readable.
Here is the ContentView:
struct ContentView: View {
let yearArray = [2020, 2021, 2022]
let monthArray = DateFormatter().monthSymbols!
// you don't need to operate on indices, you can use real values
#State var selectedYear = 2020
#State var selectedMonth = 1
// improved readability
var combinedYearMonth: String {
"\(monthArray[selectedMonth - 1]) \(selectedYear)"
}
var body: some View {
NavigationView {
List {
pickerSection
daySelectionSection
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle(combinedYearMonth)
}
}
}
The part responsible for displaying list sections:
// sections extracted to a private extension (you can still have everything in one `ContentView` struct if you want)
private extension ContentView {
var pickerSection: some View {
Section {
yearPicker
monthPicker
}
}
var daySelectionSection: some View {
Section(header: Text("What I love about you")) {
ForEach(dayRange, id: \.self) { day in
NavigationLink(destination: DetailsView()) {
Text("Day \(day)")
}
}
}
}
// create a range of days in the `selectedMonth` for the `selectedYear`
var dayRange: Range<Int> {
let dateComponents = DateComponents(year: selectedYear, month: selectedMonth)
let calendar = Calendar.current
let date = calendar.date(from: dateComponents)!
return calendar.range(of: .day, in: .month, for: date)!
}
}
And the part with pickers:
private extension ContentView {
var yearPicker: some View {
Picker("Years", selection: $selectedYear) {
ForEach(yearArray, id: \.self) { year in
Text(String(year)) // <- no need for `.tag()` if the `id` parameter in `ForEach` is specified
}
}
.pickerStyle(SegmentedPickerStyle())
}
var monthPicker: some View {
Picker("Month", selection: $selectedMonth) {
ForEach(1...12, id: \.self) { month in
Text(self.monthArray[month - 1])
}
}
.padding(.bottom, 2)
}
}

How to give DatePicker a minute interval in SwiftUI

does anyone know how to give the minutes DatePicker an interval? I am trying to only display multiples of 15 (0, 15, 30, 45). Is this only yet possible by interfacing with UIKit?
I believe it's not using the in: ...Date() part, at least I couldn't think of a solution. Here is my snippet:
DatePicker(
"",
selection: $selectedDate,
/*in: ...Date(),*/
displayedComponents: .hourAndMinute
)
Thanks so much!
The solution is to configure using UIDatePicker.appearance before SwiftUI DatePicker is created.
Tested with Xcode 13.3 / iOS 15.4
Main part is:
UIDatePicker.appearance().minuteInterval = 15
Complete code in here
can be tried this way:
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
}
}
}
}
What about using two pickers, one for hour and one for minute, and then converting to Date() afterwards? It's not pretty but this gets part of the way there:
struct ContentView: View {
#State private var hour = 1
#State private var minute = 0
let minutes = [0, 15, 30, 45]
var body: some View {
GeometryReader { geometry in
HStack {
Picker("Hour", selection: self.$hour) {
ForEach(1...12, id: \.self) {
Text("\($0)")
}
}
.labelIsHidden()
.frame(maxWidth: geometry.size.width / 4)
.clipped()
Picker("Minutes", selection: self.$minute) {
ForEach(self.minutes, id: \.self) { minute in
Text(minute == 0 ? " : 0\(minute)" : " : \(minute)")
}
}
.labelIsHidden()
.frame(maxWidth: geometry.size.width / 4)
.clipped()
}
}
}
}