I used a DatePicker inside a Form, and It looks like the following image ,
Now,I hope it display "2020-4-19 " instead of "4/19/20 ".
Someone knows how to do it?
I could not find a way with the default DatePicker, so I've taken the code from
Change selected date format from DatePicker SwiftUI
made some changes to make it work. This CustomDatePicker should do you what you asked for even within a Form.
struct CustomDatePicker: View {
#State var text: String = "Date"
#Binding var date: Date
#State var formatString: String = "yyyy-MM-dd"
#State private var disble: Bool = false
#State private var showPicker: Bool = false
#State private var selectedDateText: String = "Date"
let formatter = DateFormatter()
private func setDateString() {
formatter.dateFormat = formatString
self.selectedDateText = formatter.string(from: self.date)
}
var body: some View {
VStack {
HStack {
Text(text).frame(alignment: .leading)
Spacer()
Text(self.selectedDateText)
.onAppear() {
self.setDateString()
}
.foregroundColor(.blue)
.onTapGesture {
self.showPicker.toggle()
}.multilineTextAlignment(.trailing)
}
if showPicker {
DatePicker("", selection: Binding<Date>(
get: { self.date},
set : {
self.date = $0
self.setDateString()
}), displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
}
}
}
}
struct ContentView: View {
#State var date = Date()
var body: some View {
Form {
Section {
CustomDatePicker(text: "my date", date: $date, formatString: "yyyy-MM-dd")
Text("test")
}
}
}
Related
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
}
}
}
I have a date picker, when the date selection value changes, I would like to store it in a string array called filterSelections, how do I do that? Thanks in advance.
import SwiftUI
public var filterSelections: [String: Any]?
func setFilterSelections(name: String, selectedValue: Any) {
filterSelections[name] = selectedValue
}
struct myMainSwiftUIView: View{
var body: some View {
ScrollView {
VStack{
mySub1View()
}
}
}
}
struct mySub1View: View {
#State public var fromDate: Date = Calendar.current.date(byAdding: DateComponents(year: -40), to: Date()) ?? Date()
var body: some View {
HStack(spacing:10) {
VStack(alignment:.leading, spacing:20) {
DatePicker(selection: $fromDate, displayedComponents: .date) {
Text("From")
.font(.body)
.fixedSize()
}
}
}
}
}
}
It's hard for me to see the application of what storing all of the changes to the date picker would be, since there wouldn't be any way to cancel them out (and, in pre-iOS 14, I think the wheel would make this a particular crazy looking list when things were changing).
My suspicion is that you probably want the date along with some other filters added together. And, you specified wanting to share that state between views and subviews, which I've tried to accommodate. I also used the date format that you asked for.
I did not include the [String:Any] as your question said "array", not dictionary.
Lots of guess work here, since it's not totally clear what your goal is, but hopefully this gives you some ideas of how to share state.
class FilterViewModel : ObservableObject {
#Published var dateFilter : Date = Calendar.current.date(byAdding: DateComponents(year: -40), to: Date()) ?? Date()
#Published var myOtherFilter = "Filter1"
static var formatter = DateFormatter()
var allFilters : [String] {
Self.formatter.dateFormat = "yyyy/MM/dd"
return [myOtherFilter, Self.formatter.string(from: dateFilter)]
}
}
struct ContentView: View{
#StateObject private var filterModel = FilterViewModel()
var body: some View {
ScrollView {
VStack{
MySub1View(filterModel: filterModel)
}
ForEach(filterModel.allFilters, id: \.self) { filter in
Text(filter)
}
}
}
}
struct MySub1View: View {
#ObservedObject var filterModel : FilterViewModel
var body: some View {
HStack(spacing:10) {
VStack(alignment:.leading, spacing:20) {
DatePicker(selection: $filterModel.dateFilter, displayedComponents: .date) {
Text("From")
.font(.body)
.fixedSize()
}
}
}
}
}
It is so simple, make an array and store all of them, do not make more complex in your code, if you want export your Date array then use StateObject, there is really not a big issue. after all then start working on your stored array, for example where and how you want use it!
import SwiftUI
struct ContentView: View {
var body: some View {
mySub1View()
}
}
struct mySub1View: View {
#State private var selection: Date = Date()
#State private var selectionArray: [Date] = [Date]()
var body: some View {
if #available(iOS 14.0, *) {
DatePicker(selection.description, selection: $selection, displayedComponents: .date)
.onChange(of: selection) { newValue in
selectionArray.append(newValue)
print(selectionArray)
}
}
}
}
I created a custom alert. I want the product to be added to the basket when the Ok button on Alert is clicked on the first screen. When the Ok button is pressed on the second screen, the purchase of the product is requested. I called the same alert on 2 pages and I want it to take different actions. I couldn't do that with #Escaping.
AlertView
struct AlertView: View {
#Binding var openShowAlert: Bool
#State var closeShowAlert: Bool = false
#State var openState: CGFloat = -UIScreen.main.bounds.height
#State var closeState: CGFloat = UIScreen.main.bounds.height
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var cancelButtonText: String = ""
var body: some View {
VStack {
Text(title)
.michromaFont(size: 20)
.padding(.top)
Spacer()
Text(message)
.michromaFont(size: 18)
Spacer()
HStack {
Button(action: {
self.openShowAlert = false
openState = -UIScreen.main.bounds.height
closeState = UIScreen.main.bounds.height
}) {
Text(cancelButtonText)
.foregroundColor(.red)
}
Spacer()
Button(action: {}) {
Text(okButtonText)
}
}
.michromaFont(size: 18)
.padding([.horizontal, .bottom])
}
.neumorphisimBackground(width: 300, height: 200)
.offset(y: self.openShowAlert ? self.openState : self.closeState)
.animation(.easeInOut)
.onChange(of: self.openShowAlert, perform: { value in
if value {
self.openState = .zero
}
})
}
}
DetailView
On this screen, click Alert presentation to add the product to the cart.
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
var device = UIDevice.current.userInterfaceIdiom
#State var width: CGFloat = 300
#State var height: CGFloat = 450
#Binding var text: String
#State var showAlert: Bool = false
var body: some View {
ZStack() {
......
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
CartView Click I am providing an alert on this screen to purchase the product.
struct CartView: View {
#State var cartList = [1,2,3,4,5,6,7,8,9]
#Environment(\.presentationMode) var presentationMode
#State var showAlert: Bool = false
var body: some View {
ZStack(alignment: .top) {
.....
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
How can I send two different actions in the same alert.
Hmm, I don't see why it shouldn't work with a closure. Have you tried passing over a closure like so?
struct AlertView: View {
...
var okButtonAction: () -> ()
var body: some View {
...
Button(action: okButtonAction) {
Text(okButtonText)
}
}
}
Usage
AlertView(openShowAlert: self.$showAlert) {
// Your custom code
}
Alternative Idea
You could work with Combine and create a publisher with a specific key to identify the sender screen. Then you can put your custom code inside .onReceive().
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:
I'm trying to do something, and search, but I found nothing. I have a function DatePicker in a file. I call this function and send to it a binding var date. I want to keep only date and remove time from it. I have this function :
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}
But I use it like this : Text("(date, formatter: dateFormatter)")
How I can return the binding value with this formatter ?
thank you
edit my file :
struct WIDatePicker: View {
#Binding var date: Date
#State private var sheetDate = false
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}
var body: some View {
Button(action: {self.sheetDate.toggle()}) {
VStack {
HStack {
Text("\(date, formatter: dateFormatter)")
Print("Variables : \(dateFormatter.string(from: date))")
Spacer()
if (sheetDate == true) {
Divider()
DatePicker("", selection: $date, displayedComponents: .date)
.labelsHidden()
}
}
}
}
}
I guess you are asking how to use the formatter. Use it like this:
Text("date \(date, formatter: dateFormatter)")
maybe you could do something like this, to get the string date without hours and minutes.
struct WIDatePicker: View {
#State var date: Date
#State private var sheetDate = false
#State var myDateString = ""
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}
var body: some View {
let bindingDate = Binding<Date>(
get: { self.date },
set: {
self.myDateString = self.dateFormatter.string(from: $0)
self.date = $0
})
return Button(action: {
self.sheetDate.toggle()
}) {
VStack {
HStack {
Text("\(self.myDateString)")
Spacer()
if (sheetDate == true) {
Divider()
DatePicker("", selection: bindingDate, displayedComponents: .date).labelsHidden()
}
}
}
}.onAppear(perform: loadData)
}
func loadData() {
myDateString = dateFormatter.string(from: date)
}
}