Course deadlines app re-revisited

In an earlier post, I mentioned an issue that the deadline list rows were not updated properly while looking at the deadline coming and going. And that the solution was to do this:

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

That is, to update an observed value viewUpdateNeeded to generate a change event to the @Observable so that SwiftUI view would get updated properly.

I do not know if the code above always had an issue I just didn’t see. Or did an update to Xcode and Swift/SwiftUI libraries change something. But anyways, what I did notice afterwards was this:

  1. Start the app.
  2. Hide the left side course list view (when the ContentView showed ContentUnavailableView since no course was selected).

In this use case scenario, everything works OK. But if I did this:

  1. Start the app.
  2. Select a course from the course list on the left (ContentView displaying the deadlines of the currently selected course).
  3. Hide the left side course list view.

Then the app would get stuck, freeze totally, at step 3 (not at step 2). Why this happened is that the list row view got updated repeatedly, so that the app used 100% of the CPU.

I saw this happening when testing the app in Instruments and looking at which operation was taking the most of the execution time. Then I placed a breakpoint into that piece of code and repeated the two usage scenarios. That showed me what was happening – app was not frozen but too busy updating the list rows that it became unusable.

The reason was the line viewUpdateNeeded = date <= Date.now “changed” the value of viewUpdateNeeded from false to false repeatedly and SwiftUI then repeatedly rebuild the view. While I was in the impression that this was not supposed to be a change. Only if the value would actually change from false to true or true to false would be a change that would cause view rebuilding.

Either I had misunderstood how SwiftUI works, and my original solution did not work at all, but I just didn’t see that happening until I dit the second use case scenario with the app. Or something changed in how SwiftUI works, and then this became a bug.

Whichever, I do not know. I have no idea why this happened only when hiding the list of courses, since the course deadlines were already visible at step 2 in the second scenario.

Anyways, this change got rid of the bug:

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

That is, change the value of viewUpdateNeeded only when the condition becomes true.

UI testing is important, folks. Unfortunately I did not implement any automated UI testing for this app, so finding the bug was just by a chance. Regression testing is also important, to check if the app still works after updating the tools, frameworks and libraries used by the app, to catch any changes and possible issues caused by the changes there.