I have a form which lets the user create a project. In this form, I want to automatically change the project's deadline DatePicker based on the user's input such as start date, daily count, and target count. If the DatePicker's date is changed, then the other views should update also.
For example if the user has a start date of Sep 6, 2022, a target of 1000, and a daily count of 500, then the deadline is 1000 / 500 = 2 days and should show Sep 8, 2022.
Looking at solutions like this one and this one, I thought maybe I should use a computed property to calculate the deadline. I can display the deadline in a Text view, but how do I do this in a DatePicker? If I directly put the deadline targetDate
in the picker:
DatePicker("Deadline", selection: $createdProject.targetDate)
Then I get a build error:
Cannot assign to property: 'targetDate' is a get-only property
Maybe something I don't understand, or there's a simple solution but I can't think of it.
ContentView.swift:
import SwiftUI
struct ContentView: View {
@StateObject var createdProject:ProjectItem = ProjectItem()
var body: some View {
Form {
Section {
DatePicker("Start", selection: $createdProject.startDate)
}
Section {
VStack {
HStack {
Text("Starting word count:").fixedSize(horizontal: false, vertical: true)
TextField("0", value: $createdProject.startWordCount, formatter: NumberFormatter()).textFieldStyle(.roundedBorder)
}.padding(.bottom, 10)
HStack {
Text("Target word count").fixedSize(horizontal: false, vertical: true)
TextField("85000", value: $createdProject.targetWordCount, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
}
}
Section {
VStack {
HStack {
Text("Daily word count").fixedSize(horizontal: false, vertical: true)
TextField("0", value: $createdProject.dailyWordCount, formatter: NumberFormatter()).textFieldStyle(.roundedBorder)
}
}
// This changes to show the updated deadline
// Text("Deadline is \(createdProject.targetDate)")
// But DatePicker has build error
DatePicker("Deadline", selection: $createdProject.targetDate)
}
} // end Form
}
}
ProjectItem.swift
import SwiftUI
class ProjectItem: Identifiable, ObservableObject {
@Published var id: UUID = UUID()
@Published var startDate:Date = Date()
var targetDate:Date {
if (dailyWordCount == 0) {
return Date()
} else {
// Given start date, starting word count,
// daily word count, and target word count,
// calculate the new target date
let daysNeeded = (targetWordCount - startWordCount) / dailyWordCount
print("Need \(daysNeeded) days to reach the target")
var dateComponent = DateComponents()
dateComponent.day = daysNeeded
let nextDate = Calendar.current.date(byAdding: dateComponent, to: startDate) ?? startDate
print("The target date will be \(nextDate)")
return nextDate
}
}
@Published var startWordCount:Int = 0
@Published var targetWordCount:Int = 85000
@Published var dailyWordCount:Int = 0
}
Example image:
DatePicker
needs to be able to set the property, so you are required to have a setter,
You can make targetDate
a normal property, and then add an initializer to set it to the correct value.
Then add didSet
s (property observers) to all the properties, and then re-calculate the target date when they change. To recalculate the other properties when the target date changes, just add a didSet
to targetDate
.
class ProjectItem: Identifiable, ObservableObject {
@Published var id: UUID = UUID() {
didSet {
calculateTargetDate()
}
}
@Published var startDate:Date = Date() {
didSet {
calculateTargetDate()
}
}
@Published var targetDate:Date! = nil {
didSet {
updateDailyWordCount()
}
}
@Published var startWordCount:Int = 0 {
didSet {
calculateTargetDate()
}
}
@Published var targetWordCount:Int = 85000 {
didSet {
calculateTargetDate()
}
}
@Published var dailyWordCount:Int = 0 {
didSet {
calculateTargetDate()
}
}
init() {
targetDate = calculateTargetDate()
}
func calculateTargetDate() -> Date {
if (dailyWordCount == 0) {
targetDate = Date()
} else {
// Given start date, starting word count,
// daily word count, and target word count,
// calculate the new target date
let daysNeeded = (targetWordCount - startWordCount) / dailyWordCount
print("Need \(daysNeeded) days to reach the target")
var dateComponent = DateComponents()
dateComponent.day = daysNeeded
let nextDate = Calendar.current.date(byAdding: dateComponent, to: startDate) ?? startDate
print("The target date will be \(nextDate)")
if targetDate == nextDate {
// Exit to stop recursion
return
}
targetDate = nextDate
}
}
func updateDailyWordCount() {
// Do some calculations with the new target date
// Then update the daily word count
// (or some other property)
}
}
I hope this helps!