I want to fetch and set date from DatePicker, but my date is not updating. SwiftUI is new to me and I am confused with what type of property wrapper to use. Please help in this and advice when and where to use #State, #Binding, #Published I read some articles but still concept is not clear to me.
Here I used MVVM and SwiftUI and my code as follows.
class MyViewModel:ObservableObject {
#Published var selectedDate : Date = Date()
#Published var selectedDateStr : String = Date().convertDateToString(date: Date())
}
struct DatePickerView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var viewModel : MyViewModel
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
#State private var selectedDate = Date()
var body: some View {
VStack {
//Title
HStack{
Text("SELECT A DATE")
.foregroundColor(.white)
.font(.system(size: 20))
}
.frame(width:UIScreen.main.bounds.width,height: 60)
.background(Color.red)
//Date Picker
DatePicker(selection: $selectedDate, in: Date()-15...Date(), displayedComponents: .date) {
Text("")
}.padding(30)
Text("Date is \(selectedDate, formatter: dateFormatter)")
Spacer()
//Bottom buttons
Text("DONE")
.fontWeight(.semibold)
.frame(width:UIScreen.main.bounds.width/2,height: 60)
.onTapGesture {
self.viewModel.selectedDate = self.selectedDate
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
//calling:
DatePickerView(viewModel: self.viewModel)
Reply against your second question about wrapper properties used in SwiftUI i.e #State, #Binding, #Published.
The most common #Things used in SwiftUI are:
• #State - Binding<Value>
• #Binding - Binding<Value>
• #ObservedObject - Binding<Value> (*)
• #EnvironmentObject - Binding<Value> (*)
• #Published - Publisher<Value, Never>
(*) technically, we get an intermediary value of type Wrapper, which turns a Binding once we specify the keyPath to the actual value inside the object.
So, as you can see, the majority of the property wrappers in SwiftUI, namely responsible for the view’s state, are being “projected” as Binding, which is used for passing the state between the views.
The only wrapper that diverges from the common course is #Published, but:
1. It’s declared in Combine framework, not in SwiftUI
2. It serves a different purpose: making the value observable
3. It is never used for a view’s variable declaration, only inside ObservableObject
Consider this pretty common scenario in SwiftUI, where we declare an ObservableObject and use it with #ObservedObject attribute in a view:
class ViewModel: ObservableObject {
#Published var value: Int = 0
}
struct MyView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View { ... }
}
MyView can refer to $viewModel.value and viewModel.$value - both expressions are correct. Quite confusing, isn’t it?
These two expressions ultimately represent values of different types: Binding and Publisher, respectively.
Both have a practical use:
var body: some View {
OtherView(binding: $viewModel.value) // Binding
.onReceive(viewModel.$value) { value // Publisher
// do something that does not
// require the view update
}
}
Hope it may help you.
You can calculate the current date - 15 days using this:
let previousDate = Calendar.current.date(byAdding: .day, value: -15, to: Date())!
Then use the previousDate in DatePicker`s range:
DatePicker(selection: $selectedDate, in: previousDate...Date(), displayedComponents: .date) { ...
Summing up, your code can look like this:
struct DatePickerView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel: MyViewModel
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
#State private var selectedDate = Date()
let previousDate = Calendar.current.date(byAdding: .day, value: -15, to: Date())!
var body: some View {
VStack {
//Title
HStack{
Text("SELECT A DATE")
.foregroundColor(.white)
.font(.system(size: 20))
}
.frame(width:UIScreen.main.bounds.width,height: 60)
.background(Color.red)
//Date Picker
DatePicker(selection: $selectedDate, in: previousDate...Date(), displayedComponents: .date) {
Text("")
}.padding(30)
Text("Date is \(selectedDate, formatter: dateFormatter)")
Spacer()
//Bottom buttons
Button(action: {
self.viewModel.selectedDate = self.selectedDate
self.presentationMode.wrappedValue.dismiss()
}) {
Text("DONE")
.fontWeight(.semibold)
}
}
}
}
Tested in Xcode 11.5, Swift 5.2.4.
Related
I have a CoreData entity called PokSession which contains multiple attributes (date, currency, period, nbheure).
I want to design a view which display those informations (and allow me to modify them) for a given entity.
So basically from a previous view, I call an other view like this:
#FetchRequest(entity: PokSession.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \PokSession.date, ascending: false)
]) var poksessions: FetchedResults<PokSession>
ForEach(poksessions, id: \.date) { session in
DetailSessionPokUIView (session: session)
}
.onDelete(perform: deleteSessions)
which leads to the following view:
struct DetailSessionPokUIView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var session: PokSession
#State private var date: Date
#State private var currency: String
#State private var periode: String
#State private var nbheure: Double
let liste_currency = ["CAD", "EUR", "USD", "GBP"]
let liste_periode = ["matinée", "après-midi", "soirée"]
init(session: PokSession) {
date = session.date!
currency = session.currency!
periode = session.periode!
nbheure = session.nbheure
}
var body: some View {
NavigationView {
Form {
Section {
DatePicker(selection: $date , displayedComponents: .date){
Text("Date")
}
HStack{
Picker("Devise", selection: $currency) {
ForEach(liste_currency, id: \.self) { currency in
Text(currency)
}
}
}
HStack{
Picker("Période de jeu", selection: $periode) {
ForEach(liste_periode, id: \.self) { periode in
Text(periode)
}
}
}
HStack{
Text("Temps de jeu (h)")
.multilineTextAlignment(.leading)
Slider(value: $nbheure, in: 0...24, step: 1)
Text("\(nbheure, specifier: "%.0f")")
Image(systemName: "clock")
}
}
} // Form
} // NavigationView
}
}
But I am having error message inside my init(), saying " Variable 'self.session' used before being initialized".
I dont really understand why as "session" is an input in my init().
How can I use attributes of the selected PokSection entity to populate my DatePicker and my other Pickers default value
It shouldn't be difficult I guess but I am struggling...
Basically, I just want to have my Pickers set with the value coming from the selected PokSession.
And I want to see it in a Picker because I want to be able to modify it.
Thanks for your help
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 am at the beginning phases of understanding Combine and to my surprise I actually got this simple model to work. In the model I simply pass data the .receive publisher in the init using .receive. What I'm wondering is:
Is there a better way to implement this functionality
None of it worked until I added .store() and passed in a Set = []. which to my understanding is a way to cancel the stream of data. But although it is just to cancel to stream, the stream also will not work without it. Wondering if my understanding of that is correct and if there is a better to way to implement the cancellable.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewModel = SimpleViewModel()
#State private var changer = ""
var body: some View {
VStack {
TextField("ENTER TEXT TO PASS", text: $changer)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
HStack(alignment: .top) {
Button(action: {
viewModel.changer = self.changer
}){
Text("Change")
.fontWeight(.bold)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
VStack {
Button(action: {}){
Text("Receive")
.fontWeight(.bold)
.padding()
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(10)
}
Group {
Text(viewModel.firstValue).bold()
Text(viewModel.secondValue).bold()
Text(viewModel.thirdValue).bold()
}.padding(.vertical)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
final class SimpleViewModel: ObservableObject {
#Published var changer = ""
#Published var firstValue = "Value #1"
#Published var secondValue = "Value #2"
#Published var thirdValue = "Value #3"
private var cancellableSet: Set<AnyCancellable> = []
init() {
$changer
.receive(on: RunLoop.main)
.assign(to: \.firstValue, on: self)
.store(in: &cancellableSet)
}
}
I assume in this case you can use just didSet, like
final class SimpleViewModel: ObservableObject {
#Published var changer = "" {
didSet { firstValue = changer }
}
// ... other code
}
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: