1

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:

The views described on an iPhone 11 preview

2
  • You would need to add a setter to createdProject.targetDate. DatePicker needs to be able to change the property, and can't when there isn't a setter. Commented Sep 7, 2022 at 22:42
  • Oh right, that makes sense. Commented Sep 8, 2022 at 17:50

2 Answers 2

1

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 didSets (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!

Sign up to request clarification or add additional context in comments.

5 Comments

This works great if I only need to update the deadline once, but in my case I need to update the property whenever the user selects a date in the DatePicker. If I use a computed property, do I make the setter change the other variables, in order to change the deadline? For example, changing daily count: set { let daysNeeded = // newValue - startDate; dailyWordCount = targetWordCount / daysNeeded }, and then the targetDate should automatically also change.
Is targetDate the deadline, or do you have a separate property for that? I don't think I understand quite what you're trying to do.
Sorry, targetDate is the same as the deadline. Right now I'm trying to do 2 things. First, based off the related factors like daily count, target count, start date, calculate the targetDate and show that in the DatePicker. Second, if the user changes the date in the DatePicker, related factors like daily count dailyWordCount should be recalculated and the targetDate has this new date.
I updated my answer — does this work for you? (I just didn't implement logic for updateDailyWordCount().)
I think my comment was deleted for some reason, but it works.
0

Using the answer by @ProgrammerG, this is what I wrote:

import SwiftUI

class ProjectItem: Identifiable, ObservableObject {
    
    @Published var id: UUID = UUID()

    @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() {
        calculateTargetDate()
    }
    
    func calculateTargetDate() {
        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)
        
        let diffInDays = Calendar.current.dateComponents([.day], from: Calendar.current.startOfDay(for: startDate), to: Calendar.current.startOfDay(for: targetDate)).day ?? 0
        if diffInDays != 0 {
            print("Now have \(diffInDays) days to reach the target word count")
            let nextDailyCount = (targetWordCount - startWordCount) / diffInDays
            dailyWordCount = nextDailyCount
            print("Daily count of \(dailyWordCount) words to reach the target word count")
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.