How to convert String to Binding<Date> in DatePicker SwiftUI? - ios

I want to set DatePicker from a String to a certain date when it appears.
#State var staff: Staff
DatePicker(selection: $staff.birthDate, in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
What I expect is something like that, but it didn't work because selection requires Binding<Date> and $staff.birthDate is a String. How do I convert it?
I tried to create a func to format it like so:
func formatStringToDate(date: String?) -> Binding<Date> {
#Binding var bindingDate: Date
let dateForm = DateFormatter()
dateForm.dateFormat = "dd-MM-YYYY"
// _bindingDate = date
return dateForm.date(from: date ?? "")
}
But it still doesn't work. Any idea on how to solve this? Or did I approach this wrong?
Thankyou in advance.

You need to use another date parameter for birth date.
So your Staff class is
class Staff: ObservableObject {
#Published var birthDate: String = "02-05-2021"
#Published var date: Date = Date()
init() {
self.date = DateFormatter.formate.date(from: birthDate) ?? Date()
}
}
Now create one date formattter in the DateFormatter extension.
extension DateFormatter {
static var formate: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
return dateFormatter
}
}
Now, in view struct use the .onChange method. It will execute each time when the date is changed and you need to convert this date to a string and store it in your birthDate string var.
struct ContentView: View {
#StateObject var staff: Staff = Staff()
var body: some View {
DatePicker(selection: $staff.date, in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
.onChange(of: staff.date) { (date) in
staff.birthDate = DateFormatter.formate.string(from: date)
}
}
}
If your project target is from iOS 13 then you can use custom binding.
struct ContentView: View {
#StateObject var staff: Staff = Staff()
var body: some View {
DatePicker(selection: Binding(get: {staff.date},
set: {
staff.date = $0;
staff.birthDate = DateFormatter.formate.string(from: $0)
}), in: ...Date(),displayedComponents: .date) {
Text("Birth Date:")
}
}
}

Related

Passing variable from view inside the class ObservableObject?

i got this struct
struct Calendar: View {
#State private var selectDate = Date()
var body: some View {
// my code
}
Class fetchmydate: ObservableObject {
func fetchmydata (){
// i want to pass selectDate to here in "dd-mm-yyyy" format as string
}
}
}
the idea i want to pass the selectdate from the view into observedobject class and not vice versa, but in this format "dd-mm-yyyy" and as a string. and if i don't pass this selected date it says it can't read the selected date inside the function.
i know it is somehow weird question but if it can't be done in logic (passing) then what do you suggest to pass the date data and thank you alot my friends.
What are you trying to achieve is not something unheard of, is just easier to pass the entire logic to the viewModel, I believe you are looking for this:
The view:
struct ViewDate: View {
#State private var date = Date()
var myviewModel = viewModel()
var body: some View {
DatePicker(selection: $date, displayedComponents: .date) {
Text("Select your date")
}
.datePickerStyle(.wheel)
.onChange(of: self.date) { newDate in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let stringDate = dateFormatter.string(from: newDate)
myviewModel.fetchmydata(selectedDate: stringDate)
}
}
}
The "View Model"
class viewModel: ObservableObject {
init() {}
func fetchmydata (selectedDate: String) {
// i want to pass selectDate to here in "dd-mm-yyyy" format as string
print(selectedDate)
}
}

Crash while dismissing a view that hasn't been edited - SwiftUI

I'm building an iOS app with SwiftUI. When I click the "done" button, and the entry property is not nil, and I have not used the DatePicker TextField or TextView, I get the following runtime error in AppDelegate:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee2a83fe8)
Here is my code:
import SwiftUI
struct EditView: View {
#State var entry: Entry?
#ObservedObject var entries: Entries
#State var newDate: Date
#State var newTitle: String
#State var newBody: String
#Environment(\.presentationMode) var presentationMode
init(entries: Entries, entry: Entry?) {
UIScrollView.appearance().keyboardDismissMode = .onDrag
_entry = .init(initialValue: entry)
_entries = .init(initialValue: entries)
_newDate = .init(initialValue: entry?.date ?? Date())
_newTitle = .init(initialValue: entry?.title ?? "")
_newBody = .init(initialValue: entry?.body ?? "")
}
var body: some View {
GeometryReader { geometry in
Form {
Section {
DatePicker("Date", selection: self.$newDate, in: ...Date(), displayedComponents: .date)
.labelsHidden()
}
Section {
TextField("Title (optional)", text: self.$newTitle)
TextView(placeholder: "Entry", text: self.$newBody)
.frame(width: geometry.size.width, height: 250, alignment: .topLeading)
}
}
}
.navigationBarItems(trailing:
Button("Done") {
if let entry = self.entry {
if let index = self.entries.list.firstIndex(of: entry) {
self.entries.list[index] = Entry(date: self.newDate, title: self.newTitle, body: self.newBody)
}
} else {
self.entries.list.append(Entry(date: self.newDate, title: self.newTitle, body: self.newBody))
}
self.presentationMode.wrappedValue.dismiss()
})
}
}
import Foundation
class Entries: ObservableObject {
#Published var list = [Entry]()
}
class Entry: ObservableObject, Identifiable, Equatable {
static func == (lhs: Entry, rhs: Entry) -> Bool {
return lhs.id == rhs.id
}
let id = UUID()
#Published var date: Date
var dateString: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter.string(from: self.date)
}
#Published var title: String
#Published var body: String
init(date: Date, title: String, body: String) {
self.date = date
self.title = title
self.body = body
}
static let example = Entry(date: Date(), title: "I wrote some swift today", body: "Today I wrote some swift for an app I'm developing. It was very fun.")
When I remove the self.presentationMode.wrappedValue.dismiss() line, the problem goes away. Though, I need that line to dismiss the view. Why would this be happening, and how can I fix it? Please forgive me if my code is a complete mess. Thank you!
It looks like it tries to update during dismissing, try to postpone dismiss a bit
DispatchQueue.main.async { // defer to next event
self.presentationMode.wrappedValue.dismiss()
}

Start and End date of month in SwiftUI

How to get startDateOfMonth and endDateOfMonth based on selected date in SwiftUI?
I have found some answers for Swift (DateComponents), but couldn't make it work with SwiftUI.
Why I need this: I am going to use dynamic filters using predicate to filter all the data in the currently selected month (using custom control to switch months). But first I need to get the start and end dates per selected month.
EXAMPLE code:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var currentDate = Date()
// How to make startDateOfMonth and endDateOfMonth dependent on selected month?
#State private var startDateOfMonth = "1st January"
#State private var endDateOfMonth = "31st January"
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DateView.swift
import SwiftUI
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
#Binding var date : Date
var body: some View {
HStack {
Image(systemName: "chevron.left")
.padding()
.onTapGesture {
print("Month -1")
self.changeDateBy(-1)
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Image(systemName: "chevron.right")
.padding()
.onTapGesture {
print("Month +1")
self.changeDateBy(1)
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.background(Color.yellow)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}
struct DateView_Previews: PreviewProvider {
struct BindingTestHolder: View {
#State var testItem: Date = Date()
var body: some View {
DateView(date: $testItem)
}
}
static var previews: some View {
BindingTestHolder()
}
}
I managed to solve it by the following implementation of ContentView
#State var currentDate = Date()
private var startDateOfMonth: String {
let components = Calendar.current.dateComponents([.year, .month], from: currentDate)
let startOfMonth = Calendar.current.date(from: components)!
return format(date: startOfMonth)
}
private var endDateOfMonth: String {
var components = Calendar.current.dateComponents([.year, .month], from: currentDate)
components.month = (components.month ?? 0) + 1
components.hour = (components.hour ?? 0) - 1
let endOfMonth = Calendar.current.date(from: components)!
return format(date: endOfMonth)
}
var body: some View {
VStack {
DateView(date: $currentDate)
Text("\(currentDate)")
Text(startDateOfMonth)
Text(endDateOfMonth)
}
}
private func format(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
return dateFormatter.string(from: date)
}
Because currentDate is changed by DateView through Binding the body computed property will be invoked thus startDateOfMonth and endDateOfMonth computed properties will return the updated values.

How to properly group a list fetched from CoreData by date?

For the sake of simplicity lets assume I want to create a simple todo app. I have an entity Todo in my xcdatamodeld with the properties id, title and date, and the following swiftui view (example pictured):
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#FetchRequest(
entity: Todo.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Todo.date, ascending: true)
]
) var todos: FetchedResults<Todo>
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
var body: some View {
VStack {
List {
ForEach(todos, id: \.self) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
}
}
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
}
}
The todos are sorted by date upon fetching, and are displayed in a list like this:
I want to group the list based on each todos respective date as such (mockup):
From my understanding this could work with Dictionaries in the init() function, however I couldn't come up with anything remotely useful. Is there an efficient way to group data?
Update for iOS 15
SwiftUI now has built-in support for Sectioned Fetch Requests in a List via the #SectionedFetchRequest property wrapper. This wrapper reduces the amount of boilerplate required to group Core Data lists.
Example code
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#SectionedFetchRequest( // Here we use SectionedFetchRequest
entity: Todo.entity(),
sectionIdentifier: \.dateString // Add this line
sortDescriptors: [
SortDescriptor(\.date, order: .forward)
]
) var todos: SectionedFetchResults<Todo>
var body: some View {
VStack {
List {
ForEach(todos) { (section: [Todo]) in
Section(section[0].dateString!))) {
ForEach(section) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatted: todo.dateFormatter)")
}
}
}
}.id(todos.count)
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
The Todo class can also be refactored to contain the logic for getting the date string. As a bonus, we can also use the .formatted beta method on Date to produce the relevant String.
struct Todo {
...
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
var dateString: String? {
formatter.string(from: date)
}
}
You may try the following, It should work in your situation.
#Environment(\.managedObjectContext) var moc
#State private var date = Date()
#FetchRequest(
entity: Todo.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Todo.date, ascending: true)
]
) var todos: FetchedResults<Todo>
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
return Dictionary(grouping: result){ (element : Todo) in
dateFormatter.string(from: element.date!)
}.values.map{$0}
}
var body: some View {
VStack {
List {
ForEach(update(todos), id: \.self) { (section: [Todo]) in
Section(header: Text( self.dateFormatter.string(from: section[0].date!))) {
ForEach(section, id: \.self) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
}
}
}
}.id(todos.count)
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
}
To divide SwiftUI List backed by Core Data into sections, you can change your data model to support grouping. In this particular case, this can be achieved by introducing TodoSection entity to your managed object model. The entity would have a date attribute for sorting sections and a unique name string attribute that would serve as a section id, as well as a section header name. The unique quality can be enforced by using Core Data unique constraints on your managed object. Todos in each section can be modeled as a to many relationship to your Todo entity.
When saving a new Todo object, you would have to use Find or Create pattern to find out whether you already have a section in store or you would have to create a new one.
let sectionName = dateFormatter.string(from: date)
let sectionFetch: NSFetchRequest<TodoSection> = TodoSection.fetchRequest()
sectionFetch.predicate = NSPredicate(format: "%K == %#", #keyPath(TodoSection.name), sectionName)
let results = try! moc.fetch(sectionFetch)
if results.isEmpty {
// Section not found, create new section.
let newSection = TodoSection(context: moc)
newSection.name = sectionName
newSection.date = date
newSection.addToTodos(newTodo)
} else {
// Section found, use it.
let existingSection = results.first!
existingSection.addToTodos(newTodo)
}
To display your sections and accompanying todos nested ForEach views can be used with Section in between. Core Data uses NSSet? for to many relationships so you would have to use an array proxy and conform Todo to Comparable for everything to work with SwiftUI.
extension TodoSection {
var todosArrayProxy: [Todo] {
(todos as? Set<Todo> ?? []).sorted()
}
}
extension Todo: Comparable {
public static func < (lhs: Todo, rhs: Todo) -> Bool {
lhs.title! < rhs.title!
}
}
If you need to delete a certain todo, bear in mind that the last removed todo in section should also delete the entire section object.
I tried using init(grouping:by:) on Dictionary, as it has been suggested here, and, in my case, it causes jaggy animations, which are probably the sign that we are going in the wrong direction. I’m guessing the whole list of items has to be recompiled when we delete a single item. Furthermore, embedding grouping into a data model would be more performant and future-proof as our data set grows.
I have provided a sample project if you need any further reference.

Change selected date format from DatePicker SwiftUI

Is there a way to format the date?(from the position indicated by the arrow in the picture) I know it is formatted based on the locale but is there a way to format it myself?
struct ContentView: View {
#State private var selectedDate = Date()
var body: some View {
Form {
DatePicker(selection: $selectedDate, in: ...Date(), displayedComponents: .date) {
Text("From*")
}
}
}
}
The only way I could figure to accomplish this is to create my own custom DatePicker view and use onAppear on the TextField to update an #State selectedDateText: String variable for displaying in the TextField. This feels like a hack and I’m almost embarrassed to post it but it works. I’m new at Swift and iOS programming in general so I’m sure someone will come along with a better answer so I’ll offer this for what it’s worth. My custom view is something like this:
struct CustomDatePicker: View {
#Binding var date: Date
#State private var showPicker: Bool = false
#State private var selectedDateText: String = "Date"
private var selectedDate: Binding<Date> {
Binding<Date>(get: { self.date}, set : {
self.date = $0
self.setDateString()
})
} // This private var I found… somewhere. I wish I could remember where
// To take the selected date and store it as a string for the text field
private func setDateString() {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM dd, yyyy"
self.selectedDateText = formatter.string(from: self.date)
}
var body: some View {
VStack {
HStack {
Text("Date:")
.frame(alignment: .leading)
TextField("", text: $selectedDateText)
.onAppear() {
self.setDateString()
}
.disabled(true)
.onTapGesture {
self.showPicker.toggle()
}
.multilineTextAlignment(.trailing)
}
if showPicker {
DatePicker("", selection: selectedDate,
displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
}
}
}
}
EDIT: I figured out where I got the private var code. It was from this post: How to detect a value change of a Datepicker using SwiftUI and Combine?

Resources