Course deadlines app revisited

Three days ago I wrote about the Course deadlines app:

I list some future improvement ideas in the GitHub readme, but don’t actually know if I am going to implement those. The app already fulfills the intended purpose, so anything else would be just something fun to implement, if I have the time or motivation to do them. 

And now, after three days, I’ve implemented:

  • alerts; the app now alerts about the incoming deadline when it becomes “hot” using UserNotifications framework,
  • pick a symbol; the app now has preselected SF Symbols to choose from when adding or editing a deadline,
  • icon; app now has an icon,
  • app checks that the course name and deadline goal are not empty,
  • app checks that the course name is unique when creating a new course, and
  • several GUI enhancements and bug fixes.

As if it looks like I am using the app to avoid some other (not so fun) responsibilities and instead spend time on this hobby project… Though this is related to teaching work, really!! Valid work done here!!

One of the important bug fixes was related to setting the date and time for a deadline. I use the SwiftUI DatePicker for this, specifying that I want to select both date and time, using displayedComponents: [.date, .hourAndMinute] for the picker.

I was falsely assuming that the Date object would then contain the exact date/time selected by the user. Like if the user selects, using the picker, 2025-05-15 12.00, the date object then would have that exact time. Well, it does contain that date and that time, but since the user is not selecting seconds, the date object may then contain something in the seconds, like 2025-05-15 12.00.46, for example. Not good, since now the communicated deadline is actually later than the intended deadline.

I noticed this issue when I was testing the alerts feature and saw that the actual date and time shown in the alert was not the one specified in the deadline, by the seconds.

What I needed to do was to take the user selected Date object, then set the seconds part of the date to zero and then use that modified datetime as the actual deadline date:

extension Date {	
   func secondsRoundedToZero() -> Date {
      var components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: self)
      components.second = 0
      return Calendar.current.date(from: components)!
   }
}

Oooh, those pesky dates and times can really be hard sometimes…

There was another interesting issue with SwiftUI and the deadline formatting. The app shows a hot deadline colored red, as in the example in this image:

Deadline details like how much time there is to the deadline, what is the actual deadline, and when it is. In this picture deadline is near so it is highlighted with red color
A deadline coming after one day and five hours.

And when the deadline has passed, it should be grayed out, like this:

Deadline details like how much time there is to the deadline, what is the actual deadline, and when it is. In this picture deadline already passed so it is highlighted with gray color
A deadline gone nine days and 17 hours ago.

This worked, when you open the app and view the deadlines just loaded from files. But if you open the app and actually watch a deadline date/time coming and passing, the formatting did not change. WTF?!

As you can see, the time is shown as relative to current time. That worked fine, but as the deadline came and passed, the relative time changed to past but coloring did not.

That was because SwiftUI updates the view when the @Observable object (Deadline in this case) changes. But the deadline object actually did not change at all. No member variables changed values when the deadline date/time comes and goes. The deadline’s date and time was still the same, of course.

The way the time is shown as relative time by SwiftUI Text element, gave me an illusion that something keeps changing. Since the view shows a countdown including seconds (when the deadline is very near) when .relative style is used:

Text(deadline.date, style: .relative)

From the SwiftUI viewpoint, nothing changed so view update was not needed, thus the formatting did not change.

I solved this by adding a property viewUpdateNeeded to the Deadline. That property was then updated in the isReached computed property…:

@Observable
class Deadline: Codable {
// ...
   var viewUpdateNeeded: Bool = false
//...
   var isReached: Bool {
      viewUpdateNeeded = date <= Date.now
      return viewUpdateNeeded
   }

…used by the list item row view:

HStack {
   if deadline.isReached {
      // Show deadline as passed
      Text("Deadline passed")
         .padding(.trailing, 0)
      // ...
   } else {
      // Show deadline as forthcoming
      Text(deadline.date, style: .relative)
         .padding(.trailing, 0)
      Text("until deadline")
         .padding(.leading, 0)
// ...
}
.foregroundStyle(deadlineColor)

Where the last line, .foregroundStyle(deadlineColor determines the color used (where the deadlineColor is a property in the deadline row view):

var deadlineColor: Color {
   if deadline.isReached {
      return .gray
   } else if deadline.isHot {
      return .red
   } else if deadline.isDealBreaker {
      return .orange
   } else {
      return .accentColor
   }
}

OK, so the initial value of Deadline‘s viewUpdateNeeded is false. When the view accesses the isReached property to decide the formatting, and the deadline is still in the future, the value of viewUpdateNeeded is set.

As the value was originally false, and the new value is still false (since deadline date is not less than current date; in the past), so from the viewpoint of SwiftUI, nothing changed and view does not need updating.

When the deadline finally comes and goes, the value of viewUpdateNeeded now changes from false to true (deadline date is smaller than or equal to current datetime) and then SwiftUI sees a change in the state of the deadline object, and now the view is updated and the also formatting changes.

Of course I knew this already. Maybe I just got confused seeing the UI changing and thought, when looking at the deadline closing: “nice, things change and view is updated” and then was astonished why the formatting did not change. After a while, looking and thinking, I realized what I already knew and then added that member variable to handle the issue.

Often we stumble with the basics, and that is OK. I remember reading from somewhere that experienced programmers actually do more mistakes than novices since they typically work faster. The difference is that with experience, you are able to spot your mistakes much earlier and are usually able to solve the issues much faster. So go gather some more experience!