I have a .sheet element, that acts as a form when a user is adding to a calendar. Everything works as expected, but as soon as they submit, and upload the Date, Start Time, and end time into firebase where it's held, I noticed a problem. The date and time were formated weird.
My question is, how can I change the var scheduledate to a mm/dd/yy format when stored, and how can I change the var starttime and var endtime to a h:mm format when stored.
Here's my code:
struct SheetView: View {
var userEmail = Auth.auth().currentUser?.email
#Environment(\.dismiss) var dismiss
#State private var scheduledate = Date() //Store this as mm/dd/yy
#State private var startTime = Date() //Store this as h:mm
#State private var endTime = Date() //Store this as h:mm
#State private var scheduleairplane = ""
#State private var scheduleinstructor = ""
var body: some View {
VStack {
HStack {
Text("New Flight")
.font(.title)
.fontWeight(.bold)
Spacer()
Button("Cancel") {
dismiss()
}
}
.padding()
VStack {
DatePicker("Date", selection: $scheduledate, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.frame(maxHeight: 400)
DatePicker("Start Time", selection: $startTime, displayedComponents: .hourAndMinute)
.datePickerStyle(GraphicalDatePickerStyle())
.padding(.leading)
DatePicker("End Time", selection: $endTime, displayedComponents: .hourAndMinute)
.datePickerStyle(GraphicalDatePickerStyle())
.padding(.leading)
}
VStack {
TextField("Airplane...", text: self.$scheduleairplane)
.padding([.leading, .trailing])
.offset(x: 10)
.frame(height: 30)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 1)
.padding([.leading, .trailing])
)
TextField("Instructor...", text: self.$scheduleinstructor)
.padding([.leading, .trailing])
.offset(x: 10)
.frame(height: 30)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 1)
.padding([.leading, .trailing])
)
}
Spacer(minLength: 0)
Button(action: {
let doc = db.collection("Scheduled").document("\(userEmail ?? "Error")")
// Atomically add a new region to the "regions" array field.
doc.updateData([
"Airplanes": FieldValue.arrayUnion(["\(scheduleairplane)"])
])
doc.updateData([
"Date": FieldValue.arrayUnion(["\(scheduledate)"])
])
doc.updateData([
"Time": FieldValue.arrayUnion(["\(ScheduledTime)"])
])
doc.updateData([
"Instructor": FieldValue.arrayUnion(["\(scheduleinstructor)"])
])
}) {
Text("Submit")
}
.foregroundColor(.white)
.frame(width: 120, height: 45)
.background(Color("Blue"))
.cornerRadius(30)
.padding(.bottom)
}
}
}
Thanks for all the help but I found an answer
here that works very well and is easy.
I used 3 different functions that I ran when I click the button. Then I send them off to firebase with the new values.
func updateDate() {
let selecteddate = scheduleddate //Here it gets the date and saves as another variable
let formatter1 = DateFormatter()
formatter1.dateStyle = .short
//I then made a saved string called newscheduledate to use this new variable anywhere in my project.
newscheduledate = formatter1.string(from: selecteddate) //Here is when it sets the conversion to a string which I used when I sent to firebase.
}
I then used this piece of code below twice, for the start time, and the end time.
func updateTime() {
let selectedtime = startTime //Here it gets the date
let formatter2 = DateFormatter()
formatter2.timeStyle = .medium
//I have a saved string called newstarttime to use this new variable anywhere in my project.
newstarttime = formatter2.string(from: selectedtime)
}
class GeneralExtension{
static func DateToStr(date:Date,format:String = "yyyy/MM/dd HH:mm:ss")->String{
let timeFormatter = DateFormatter()
timeFormatter.locale = Locale.current
timeFormatter.timeZone = TimeZone.current
timeFormatter.dateFormat = format
let nowTimestr = timeFormatter.string(from: date)
return nowTimestr
}
static func StrToDate(dateStr:String,format:String = "yyyy/MM/dd HH:mm:ss")->Date{
var cbDate:Date = Date()
if(!dateStr.isEmpty){
let dateFormatter=DateFormatter()
dateFormatter.dateFormat=format
let tmpDate = dateFormatter.date(from: dateStr)
if let tmpDate = tmpDate{
cbDate = tmpDate
}
}
return cbDate
}
}
example
GeneralExtension.DateToStr(date: dateSetting,format: "MM")
or
extension String{
func StrToDate(format:String = "yyyy/MM/dd HH:mm:ss")->Date{
var cbDate:Date = Date()
if(!self.isEmpty){
let dateFormatter=DateFormatter()
dateFormatter.dateFormat=format
let tmpDate = dateFormatter.date(from: self)
if let tmpDate = tmpDate{
cbDate = tmpDate
}
}
return cbDate
}
}
extension Date{
func DateToStr(format:String = "yyyy/MM/dd HH:mm:ss")->String{
let timeFormatter = DateFormatter()
timeFormatter.locale = Locale.current
timeFormatter.timeZone = TimeZone.current
timeFormatter.dateFormat = format
let nowTimestr = timeFormatter.string(from: self)
return nowTimestr
}
}
example
let date = Date()
let dateStr = date.DateToStr("MM")
let convertDate = dateStr.StrToDate()
I have an Date Picker which I want to go from one date to another date. My dates are fetched from a backend, and they look something like this in the backend
"2022-01-01T10:30:00".
After the call from the function that i wrote, the date looks like this :
2022-01-01 10:30:00 +0000
2023-01-01 20:00:00 +0000
The problem that i have is that something isn't right, the time in my Date Picker is always started from the current time and is end on current time too
This is the coded for what I have wrote :
struct TestView: View {
#EnvironmentObject var syncViewModel : SyncViewModel
#State var dateFrom = Date()
#State var dateStart : Date = Date.now
#State var dateEnd : Date = Date.now
var body: some View {
VStack {
let minMaxRange = dateStart...dateEnd
DatePickerUIKit(selection: $dateFrom,
in: minMaxRange, minuteInterval: 30)
.frame(width: 100, height: 100, alignment: .center)
}
.onAppear {
dateStart = dateFormatTime(date: syncViewModel.schedule[0].start)
dateEnd = dateFormatTime(date: syncViewModel.schedule[0].end)
print(dateStart)
print(dateEnd)
}
}
func dateFormatTime(date : String) -> Date {
let isoDate = date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter.date(from: isoDate) ?? Date.now
}
}
What I want to accomplish :
to go from this date "2022-01-01 10:30:00 +0000" to "2023-01-01 20:00:00 +0000"
I tried something like this and it worked fine, but with my dates is not working.
let minMaxRange = Date.now...Date.distantFuture
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.
using SwiftUI on my project I'm try to display some data in a list.
I would like to insert on the left side of my cell a rectangle(), but I can't find the way to fix the height.
(i don't want to manual input the height otherwise it doesn't look good on different device)
I'm try using geometry render but not working.
I highlight on the attached picture my idea...
here my code:
import SwiftUI
struct MatchViewUser: View {
var match : MatchModel
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}
var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm a"
formatter.amSymbol = "AM"
formatter.pmSymbol = "PM"
return formatter
}
var body: some View {
GeometryReader { geometry in
HStack {
Rectangle().frame(width: 30,height: geometry.size.height ,alignment: .leading)
VStack(alignment:.leading){
HStack{
Text("\(self.match.dateMatch, formatter: self.dateFormatter)")
Spacer()
Text("Time:\(self.match.dateMatch, formatter: self.timeFormatter)")
}.font(.headline).foregroundColor(.blue)
Divider().padding(.horizontal)
HStack{
VStack(alignment:.leading){
Text("Match Name:").bold()
Text("\(self.match.matchName)").font(.body)
}
Spacer()
VStack(alignment:.leading){
Text("Pitch Name").bold()
Text("\(self.match.pitchName)").font(.body)
}
Spacer()
VStack(alignment:.trailing){
Text("Player").bold()
Text("\(self.match.maxPlayer)").font(.body)
}
}.font(.headline)
}
}
}
}
}
Here is a demo of possible approach (simplified your code w/o dependent model). Prepared & tested with Xcode 11.4 / iOS 13.4
struct DemoView: View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}
let dateMatch = Date()
var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm a"
formatter.amSymbol = "AM"
formatter.pmSymbol = "PM"
return formatter
}
var body: some View {
HStack {
VStack(alignment:.leading){
HStack{
Text("\(self.dateMatch, formatter: self.dateFormatter)")
Spacer()
Text("Time:\(self.dateMatch, formatter: self.timeFormatter)")
}.font(.headline).foregroundColor(.blue)
Divider().padding(.horizontal)
HStack{
VStack(alignment:.leading){
Text("Match Name:").bold()
Text("Demo1").font(.body)
}
Spacer()
VStack(alignment:.leading){
Text("Pitch Name").bold()
Text("Demo2").font(.body)
}
Spacer()
VStack(alignment:.trailing){
Text("Player").bold()
Text("10").font(.body)
}
}.font(.headline)
}.padding(.leading, 38)
}.overlay(
Rectangle().frame(width: 30)
, alignment: .leading)
}
}
Having trouble finding a DatePicker in SwiftUI that displays "mm:ss". displayComponents only has .date and .HourAndMinute.
How do I change the DatePicker to display the minutes and seconds without AM / PM?
struct ContentView: View {
#State private var altEventTime = Date()
public var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss"
return formatter
}
var body: some View {
Form {
DatePicker(selection: self.$altEventTime, displayedComponents: .hourAndMinute) {
Text("Finish time: \(altEventTime, formatter: timeFormatter)")
}
}
}
}