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.
Related
I have an array of reminders(reminder model) in a view model and want to be able to edit existing reminders specifically through and edit swipe action and then through the reminders detail screen. I tried adding a button with a sheet to my Homeview in a list and then tried updating the edited reminder in the reminders array to a property in my view model called existingRemindData by using an update function in the reminder model. this should work but the remind var created by the foreach loop in the home view doesn't keep its value when it is called in the sheet. In the home view under the edit swipe action when I assign homevm.existingRemindData = remind.data it is equal to whatever reminder I swipe on because I did a print statement to confirm but as soon as I try to use the remind var inside of the sheet for the edit action the remind var defaults to the first item in the reminder array in the view model which is obviously not right. how would I make it so it uses the correct reminder index value when trying to update the reminder or is there another way which I could implement this functionality. any help would be great and look in the code for clarification on what I talk about.
HomeView
'''
import SwiftUI
struct HomeView: View {
#StateObject private var homeVM = HomeViewModel()
#State var percent: Int = 1
#State var showDetailEditView = false
#State var showAddView = false
#State var dropDown = false
//#State var filter = false
var body: some View {
ZStack {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
ReminderView(remind: $remind)
//.background(remind.theme.mainColor)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.swipeActions(edge: .leading) {
Button(action: {
self.showDetailEditView.toggle()
homeVM.existingRemindData = remind.data
print(homeVM.existingRemindData.title)
}) {
Label("Edit", systemImage: "pencil")
}
}
.sheet(isPresented: $showDetailEditView) {
NavigationView {
ReminderEditView(data: $homeVM.existingRemindData)
.navigationTitle(homeVM.existingRemindData.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
self.showDetailEditView.toggle()
homeVM.existingRemindData = Reminder.Data()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
self.showDetailEditView.toggle()
print("\(remind.id) \(remind.title)")
print("\(homeVM.existingRemindData.id) \(homeVM.existingRemindData.title)")
remind.update(from: homeVM.existingRemindData)
homeVM.newRemindData = Reminder.Data()
}
}
}
.background(LinearGradient(gradient: Gradient(colors: [
Color(UIColor(red: 0.376, green: 0.627, blue: 0.420, alpha: 1)),
Color(UIColor(red: 0.722, green: 0.808, blue: 0.725, alpha: 1))
]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
.swipeActions(allowsFullSwipe: true) {
Button (role: .destructive, action: {
homeVM.deleteReminder(remind: remind)
}) {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
.onAppear(
perform: {
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
})
'''
Reminder edit view
'''
import SwiftUI
extension Binding {
static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
return Binding(get: {lhs.wrappedValue ?? rhs}, set: {lhs.wrappedValue = $0})
}
}
struct ReminderEditView: View {
#ObservedObject var editVM: EditViewModel
init(data: Binding<Reminder.Data>) {
editVM = EditViewModel(data: data)
}
var body: some View {
Form {
Section {
TextField("Title", text: $editVM.data.title)
TextField("Notes", text: $editVM.data.notes ?? "")
.frame(height: 100, alignment: .top)
}
Section {
Toggle(isOn: $editVM.data.hasDueDate, label: {
if editVM.data.hasDueDate {
VStack(alignment: .leading) {
Text("Date")
Text(editVM.data.hasDueDate ? editVM.data.formatDate(date: editVM.data.date!) : "\(editVM.data.formatDate(date: Date.now))")
.font(.caption)
.foregroundColor(.red)
}
} else {
Text("Date")
}
})
if editVM.data.hasDueDate {
DatePicker("Date", selection: $editVM.data.dueDate, in: Date()..., displayedComponents: .date)
.datePickerStyle(.graphical)
}
'''
Reminder model
'''
extension Reminder {
struct Data: Identifiable {
var title: String = ""
var notes: String?
var date: Date?
var time: Date?
var theme: Theme = .poppy
var iscomplete: Bool = false
var priority: RemindPriority = .None
let id: UUID = UUID()
var dueDate: Date {
get {
return date ?? Date()
}
set {
date = newValue
}
}
var dueTime: Date {
get {
return time ?? Date()
}
set {
time = newValue
}
}
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .none
return formatter.string(from: date)
}
func formatTime(time: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter.string(from: time)
}
var hasDueDate: Bool {
get {
date != nil
}
set {
if newValue == true {
date = Date()
}
else {
date = nil
hasDueTime = false
}
}
}
var hasDueTime: Bool {
get {
time != nil
}
set {
if newValue == true {
time = Date()
hasDueDate = true
}
else {
time = nil
}
}
}
}
var data: Data {
Data(title: title, notes: notes, date: date, time: time, theme: theme, iscomplete: iscomplete, priority: priority)
}
mutating func update(from data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
}
init(data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
id = data.id
}
}
'''
HomeViewModel(View model talked about)
'''
import Foundation
import SwiftUI
import Combine
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder] = Reminder.sampleReminders
#Published var newRemindData = Reminder.Data()
#Published var existingRemindData = Reminder.Data()
#Published var selectedRemind = Reminder(data: Reminder.Data())
#Published var compReminds: [Reminder] = []
private var cancellables = Set<AnyCancellable>()
/*init(reminds: [Reminder]) {
self.reminds = reminds
}*/
func newReminder() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
func deleteReminder(remind: Reminder) {
Just(remind)
.delay(for: .seconds(0.25), scheduler: RunLoop.main)
.sink {remind in
if remind.iscomplete {
self.removeRemind(remind: remind)
}
if !remind.iscomplete {
self.removeRemind(remind: remind)
}
self.reminds.removeAll { $0.id == remind.id }
}
.store(in: &cancellables)
}
func appendRemind(complete: Reminder) {
compReminds.append(complete)
}
func removeRemind(remind: Reminder) {
compReminds.removeAll() { $0.id == remind.id }
}
func remindIndex() -> Int {
return reminds.firstIndex(where: {
$0.id == existingRemindData.id
}) ?? 1
}
We don't use view model objects in SwiftUI. Change EditViewModel class to be an EditConfig struct, declare it as #State var config: EditConfig? use it as the item in sheet(item:onDismiss:content:) instead of the bool version.
Also, your date formatting is not SwiftUI compatible, you won't benefit from the labels being updated automatically when the user changes their region settings. To fix that remove the formatDate code and instead supply the formatter to Text or simply use .date. If using a formatter object make sure you aren't initing a new one every time, e.g. store one inside an #State struct or a static var. in SwiftUI we must not init objects in a View's init and body, only value types.
Hi all I have a problem with selecting cells in a LazyHGrid using SwiftUI
In the TimePickerGridView.swift I create a horizontal list with times that the user can select. To manage cell selection I use #State private var selectedItem: Int = 0
Everything seems to work but I have some problems when I save the user selection to create a date which contains the selected times
When the user selects a cell, it immediately updates a date by setting the hours and minutes.
The date being modified is #Binding var date: Date, this date refers to a RiservationViewModel.swift which is located in the RiservationView structure
struct ReservationsView: View {
#StateObject var viewModels = ReservationViewModel()
var body: some View {
VStack {
TimePickerGridView(date: $viewModels.selectedDate)
}
}
}
Now the problem is that when the user selects the time and creates the date the LazyHGrid continuously loses the selection and has to select the same cell more than once to get the correct selection again ...
At this point the variable date is observed because it can change thanks to another view that contains a calendar where the user selects the day.
How can I solve this problem? where is my error?
private extension Date {
var hour: Int { Calendar.current.component(.hour, from: self) }
var minute: Int { Calendar.current.component(.minute, from: self) }
var nextReservationTime: TimePicker {
let nextHalfHour = self.minute < 30 ? self.hour : (self.hour + 1) % 24
let nextHalfMinute = self.minute < 30 ? 30 : 0
return TimePicker(hour: nextHalfHour, minute: nextHalfMinute)
}
}
struct TimePickerGridView: View {
#Binding var date: Date
#State private var selectedItem: Int = 0
#State private var showNoticeView: Bool = false
private var items: [TimePicker] = TimePicker.items
private func setTimeForDate(_ time: (Int, Int)) {
guard let newDate = Calendar.current.date(bySettingHour: time.0, minute: time.1, second: 0, of: date) else { return }
date = newDate
}
private var startIndex: Int {
// se trova un orario tra quelli della lista seleziona l'index appropriato
// altrimenti seleziona sempre l'index 0
items.firstIndex(of: date.nextReservationTime) ?? 0
}
init(date: Binding<Date>) {
// Date = ReservationViewModel.date
self._date = date
}
var body: some View {
VStack(spacing: 20) {
HStack {
Spacer()
TitleView("orario")
VStack {
Divider()
.frame(width: screen.width * 0.2)
}
Button(action: {}) {
Text("RESET")
.font(.subheadline.weight(.semibold))
.foregroundColor(date.isToday() ? .gray : .bronze)
.padding(.vertical, 5)
.padding(.horizontal)
.background(.thinMaterial)
.clipShape(Capsule())
}
Spacer()
}
ZStack {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { reader in
LazyHGrid(rows: Array(repeating: GridItem(.fixed(60), spacing: 0), count: 2), spacing: 0) {
ForEach(items.indices) { item in
TimePickerGridCellView(date: $date, selectedItem: $selectedItem, picker: items[item], selection: selectedItem == items.indices[item])
.frame(width: (UIScreen.main.bounds.width - horizontalDefaultPadding * 2) / 4)
.onTapGesture {
setTimeForDate((items[item].hour, items[item].minute))
selectedItem = item
}
.onChange(of: date) { _ in
selectedItem = startIndex
}
.onAppear(perform: {
selectedItem = startIndex
})
}
}
.background(Divider())
}
}
.frame(height: showNoticeView ? 70 : 130)
}
}
}
}
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:")
}
}
}
I have a simple View with a custom month selector. Each time you click chevron left or right,
Inside DateView I had two private vars: startDateOfMonth2: Date and endDateOfMonth2: Date. But they were only available inside DateView.
QUESTION
How to make those two variables available in other views?
I have tried to add them as #Published vars, but I am getting an error:
Property wrapper cannot be applied to a computed property
I have found just a few similar questions, but I cannot manage to use answers from them with my code.
import SwiftUI
class SelectedDate: ObservableObject {
#Published var selectedMonth: Date = Date()
#Published var startDateOfMonth2: Date {
let components = Calendar.current.dateComponents([.year, .month], from: selectedMonth)
let startOfMonth = Calendar.current.date(from: components)!
return startOfMonth
}
#Published var endDateOfMonth2: Date {
var components = Calendar.current.dateComponents([.year, .month], from: selectedMonth)
components.month = (components.month ?? 0) + 1
let endOfMonth = Calendar.current.date(from: components)!
return endOfMonth
}
}
struct DateView: View {
#EnvironmentObject var selectedDate: SelectedDate
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("yyyy MMMM")
return formatter
}()
var body: some View {
HStack {
Image(systemName: "chevron.left")
.frame(width: 50, height: 50)
.contentShape(Rectangle())
.onTapGesture {
self.changeMonthBy(-1)
}
Spacer()
Text("\(selectedDate.selectedMonth, formatter: Self.dateFormat)")
Spacer()
Image(systemName: "chevron.right")
.frame(width: 50, height: 50)
.contentShape(Rectangle())
.onTapGesture {
self.changeMonthBy(1)
}
}
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
.background(Color.yellow)
}
func changeMonthBy(_ months: Int) {
if let selectedMonth = Calendar.current.date(byAdding: .month, value: months, to: selectedDate.selectedMonth) {
self.selectedDate.selectedMonth = selectedMonth
}
}
}
No need to declare your computed values as Published, as they are depending on a Published value. When the published value changed, they get recalculated.
class SelectedDate: ObservableObject {
#Published var selectedMonth: Date = Date()
var startDateOfMonth2: Date {
let components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
let startOfMonth = Calendar.current.date(from: components)!
print(startOfMonth)
return startOfMonth
}
var endDateOfMonth2: Date {
var components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
components.month = (components.month ?? 0) + 1
let endOfMonth = Calendar.current.date(from: components)!
print(endOfMonth)
return endOfMonth
}
}
When you print endDateOfMonth2 on click, you will see that it is changing.
computed property is function substantially。
it can't stored value,it just computed value from other variable or property in getter and update value back to things it computed in setter。
think about your property‘s character carefully。
mostly,you can directly readwrite computed property。
just remember it is function core and property skin
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()
}