{"id":1187,"date":"2025-05-15T10:52:05","date_gmt":"2025-05-15T07:52:05","guid":{"rendered":"https:\/\/www.juustila.com\/antti\/?p=1187"},"modified":"2025-05-20T15:56:23","modified_gmt":"2025-05-20T12:56:23","slug":"course-deadlines-app-revisited","status":"publish","type":"post","link":"https:\/\/www.juustila.com\/antti\/2025\/05\/15\/course-deadlines-app-revisited\/","title":{"rendered":"Course deadlines app revisited"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Three days ago I wrote about the Course deadlines app:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">I list some future improvement ideas in the GitHub readme, but don\u2019t 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.&nbsp;<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">And now, after three days, I&#8217;ve implemented:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>alerts<\/strong>; the app now alerts about the incoming deadline when it becomes &#8220;hot&#8221; using <code>UserNotifications<\/code> framework,<\/li>\n\n\n\n<li><strong>pick a symbol<\/strong>; the app now has preselected SF Symbols to choose from when adding or editing a deadline,<\/li>\n\n\n\n<li><strong>icon<\/strong>; app now has an icon,<\/li>\n\n\n\n<li>app checks that the course name and deadline goal <strong>are not empty<\/strong>,<\/li>\n\n\n\n<li>app checks that the <strong>course name is unique<\/strong> when creating a new course, and<\/li>\n\n\n\n<li>several GUI enhancements and bug fixes.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8230; Though this <em>is<\/em> related to teaching work, really!! Valid work done here!!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One of the important bug fixes was related to setting the date and time for a  deadline. I use the SwiftUI <code>DatePicker<\/code> for this, specifying that I want to select both date and time, using <code>displayedComponents: [.date, .hourAndMinute]<\/code> for the picker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I was falsely assuming that the <code>Date<\/code> object would then contain the <em>exact<\/em> 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 <em>exact<\/em> 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 <em>communicated<\/em> deadline is actually <em>later<\/em> than the <em>intended<\/em> deadline. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What I needed to do was to take the user selected <code>Date<\/code> object, then set the seconds part of the date to zero and then use that modified datetime as the actual deadline date:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>extension Date {\t\n   func secondsRoundedToZero() -&gt; Date {\n      var components = Calendar.current.dateComponents(&#91;.year, .month, .day, .hour, .minute], from: self)\n      components.second = 0\n      return Calendar.current.date(from: components)!\n   }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Oooh, those pesky dates and times can really be hard sometimes&#8230;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"930\" height=\"124\" src=\"https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.47.42.png\" alt=\"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\" class=\"wp-image-1188\" srcset=\"https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.47.42.png 930w, https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.47.42-300x40.png 300w, https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.47.42-768x102.png 768w\" sizes=\"auto, (max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" \/><figcaption class=\"wp-element-caption\">A deadline coming after one day and five hours.<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">And when the deadline has passed, it should be grayed out, like this:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"124\" src=\"https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.49.47.png\" alt=\"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\" class=\"wp-image-1189\" srcset=\"https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.49.47.png 900w, https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.49.47-300x41.png 300w, https:\/\/www.juustila.com\/antti\/wp-content\/uploads\/2025\/05\/Nayttokuva-2025-05-15-kello-9.49.47-768x106.png 768w\" sizes=\"auto, (max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" \/><figcaption class=\"wp-element-caption\">A deadline gone nine days and 17 hours ago.<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">This worked, when you open the app and view the deadlines just loaded from files. But if you open the app and actually <em>watch<\/em> a deadline date\/time <em>coming and passing<\/em>, the <em>formatting did not change<\/em>. WTF?!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <em>coloring did not<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That was because SwiftUI updates the view when the <code>@Observable<\/code> object (<code>Deadline<\/code> in this case) <em>changes<\/em>. But the deadline object actually <em>did not change at all<\/em>. No member variables changed values when the deadline date\/time comes and goes. The deadline&#8217;s date and time was still the same, of course.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The way the time is shown as <em>relative<\/em> time by SwiftUI <code>Text<\/code> element, gave me an illusion that something keeps changing. Since the view shows a countdown including seconds (when the deadline is very near) when <code>.relative<\/code> style is used:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Text(deadline.date, style: .relative)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">From the SwiftUI viewpoint, <em>nothing<\/em> changed so view update was <em>not<\/em> needed, thus the formatting did not change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I solved this by adding a property <code>viewUpdateNeeded<\/code> to the <code>Deadline<\/code>. That property  was then updated in the <code>isReached<\/code> computed property&#8230;:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Observable\nclass Deadline: Codable {\n\/\/ ...\n   var viewUpdateNeeded: Bool = false\n\/\/...\n   var isReached: Bool {\n      viewUpdateNeeded = date &lt;= Date.now\n      return viewUpdateNeeded\n   }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#8230;used by the list item row view:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>HStack {\n   if deadline.isReached {\n      \/\/ Show deadline as passed\n      Text(\"Deadline passed\")\n         .padding(.trailing, 0)\n      \/\/ ...\n   } else {\n      \/\/ Show deadline as forthcoming\n      Text(deadline.date, style: .relative)\n         .padding(.trailing, 0)\n      Text(\"until deadline\")\n         .padding(.leading, 0)\n\/\/ ...\n}\n.foregroundStyle(deadlineColor)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Where the last line, <code>.foregroundStyle(deadlineColor<\/code> determines the color used (where the <code>deadlineColor<\/code> is a property in the deadline row view):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>var deadlineColor: Color {\n   if deadline.isReached {\n      return .gray\n   } else if deadline.isHot {\n      return .red\n   } else if deadline.isDealBreaker {\n      return .orange\n   } else {\n      return .accentColor\n   }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">OK, so the initial value of <code>Deadline<\/code>&#8216;s <code>viewUpdateNeeded<\/code> is <code>false<\/code>. When the view accesses the <code>isReached<\/code> property to decide the formatting, and the deadline is still in the future, the value of <code>viewUpdateNeeded<\/code> is set. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As the value was originally <code>false<\/code>, and the new value is still <code>false<\/code> (since deadline date is not less than current date; in the past), so from the viewpoint of SwiftUI, <em>nothing<\/em> changed and view does not need updating.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When the deadline finally comes and goes, the value of <code>viewUpdateNeeded<\/code> now changes from <code>false<\/code> to <code>true<\/code> (deadline date is smaller than or equal to current datetime) and then SwiftUI <em>sees<\/em> a change in the state of the deadline object, and <em>now<\/em> the view is updated and the also formatting changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Of course I knew this already. Maybe I just got confused seeing the UI changing and thought, when looking at the deadline closing: &#8220;nice, things change and view is updated&#8221; and then was astonished why the <em>formatting<\/em> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Three days ago I wrote about the Course deadlines app: I list some future improvement ideas in the GitHub readme, but don\u2019t 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 &hellip; <a href=\"https:\/\/www.juustila.com\/antti\/2025\/05\/15\/course-deadlines-app-revisited\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Course deadlines app revisited&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[2],"tags":[180,179,181,70,46,47],"class_list":["post-1187","post","type-post","status-publish","format-standard","hentry","category-coding","tag-experience","tag-learning","tag-mistakes","tag-programming","tag-swift","tag-swiftui"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/posts\/1187","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/comments?post=1187"}],"version-history":[{"count":5,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/posts\/1187\/revisions"}],"predecessor-version":[{"id":1203,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/posts\/1187\/revisions\/1203"}],"wp:attachment":[{"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/media?parent=1187"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/categories?post=1187"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.juustila.com\/antti\/wp-json\/wp\/v2\/tags?post=1187"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}