figbert.com-website

[ACTIVE] the website and home of figbert on the clearnet
git clone git://git.figbert.com/figbert.com-website.git
Log | Files | Refs | README | LICENSE

index.md (6741B)


      1 +++
      2 title = "Problem Exists Between Keyboard and Chair: How I Spent 2 Days Chasing a Bug that Didn't Exist"
      3 date = 2020-07-28
      4 updated = 2022-06-14
      5 [extra]
      6 type = "post"
      7 +++
      8 
      9 Post-[WWDC2020], I decided to rewrite the backend of txtodo in SwiftUI
     10 using the new [App and Scene structure]. Rebuilding the app from scratch
     11 may have not been the best choice, but during that process I have
     12 massively simplified the app's data structure, despaghettified some messy
     13 UI code, and spent two full days trying to solve a problem that didn't
     14 exist. This is the story of that last bit.
     15 
     16 <!-- more -->
     17 
     18 ## Structural Changes
     19 
     20 The new app, so far, was mostly the same as the old version but without
     21 the `AppDelegate.swift` or `SceneDelegate.swift` files (using the new
     22 XCode 12 multiplatform app template). I also combined the Core Data
     23 `FloatingTask` and `DailyTask` entities into one `Task` entity. By this
     24 point, everything was running well enough so I started to migrate more
     25 code into the new codebase starting with the fetch request:
     26 
     27 ```swift
     28 @FetchRequest(
     29     entity: Task.entity(),
     30     sortDescriptors: [
     31         NSSortDescriptor(keyPath: \Task.completed, ascending: true),
     32         NSSortDescriptor(keyPath: \Task.priority, ascending: false),
     33         NSSortDescriptor(keyPath: \Task.name, ascending: true)
     34     ]
     35 ) var tasks: FetchedResults<Task>
     36 ```
     37 
     38 ## Breaking TaskView
     39 
     40 Tasks are displayed as `TaskView`s in a `ForEach` loop on the
     41 homescreen, which is simple enough. The `TaskView` struct, however, is
     42 relatively complicated. The purpose of `TaskView` is to represent and
     43 manipulate a single `Task`. In the previous version of the app (I'm
     44 going to call the original version 2.0 and the rewrite 3.0 from now on),
     45 this involved passing a number of attributes individually to be
     46 manipulated as the view's `@State`. When migrating the view, I reduced
     47 this to a single `@ObservedObject`. I also removed some of the text
     48 styling, which I planned to port over after I got the UI functional.
     49 
     50 I ran the app on my device, and this happened:
     51 
     52 {{ video(sources=["ascending-checkmarks-error.webm", "ascending-checkmarks-error.mp4"])  }}
     53 
     54 Well that was unexpected. Instead of checking off the tasks I selected,
     55 tasks were checked off starting from the bottom and ascending –
     56 obviously not the intended behavior! My first thought was that it was
     57 caused by the use of `@ObservedObject` to declare the view's task
     58 property – I haven't seen it used to manipulate a Core Data entity
     59 before, but it's worked fine so far in txtodo – so I rewrote the
     60 variables to match version 2.0.
     61 
     62 ```swift
     63 // VERSION 3.0
     64 struct TaskView: View {
     65   @Environment(\.managedObjectContext) var managedObjectContext
     66   @ObservedObject var task: Task
     67   @State var priority: Int
     68   @State private var config = TaskConfig()
     69   // UI...
     70 }
     71 
     72 // VERSION 2.0
     73 struct floatingTaskView: View {
     74   @Environment(\.managedObjectContext) var managedObjectContext
     75   @ObservedObject var task: FloatingTask
     76   @State var completed: Bool
     77   @State var name: String
     78   @State var priority: Int
     79   @State var deleted: Bool = false
     80   @State private var editingText: Bool = false
     81   @State private var editingPriority: Bool = false
     82   @State private var viewingNotes: Bool = false
     83   @State private var confirmingDelete: Bool = false
     84   // UI...
     85 }
     86 ```
     87 
     88 Still no change. It was getting pretty late at this point, but I decided
     89 to stick it out for just a bit longer. I rewrote the `TaskView` struct
     90 from scratch *two more times* to no avail. Something was wrong, but I
     91 had no idea where it was and there was no way I was going to figure it
     92 out at two in the morning by coding it again the exact same way.
     93 
     94 ## Fantastic Bugs and Where to Find Them
     95 
     96 The next morning, I took a look at the code again. If the problem wasn't
     97 in `TaskView`, where was it? The only other thing in the UI was the
     98 button to make a new task, which looked something like this:
     99 
    100 ```swift
    101 Button(action: {
    102     let newTask = Task(context: self.managedObjectContext)
    103     newTask.name = "test"
    104     newTask.priority = 3
    105     newTask.notes = [String]() as NSObject
    106     newTask.id = UUID()
    107     newTask.date = Date.init()
    108     newTask.daily = true
    109     do {
    110         try self.managedObjectContext.save()
    111     } catch {
    112         print(error.localizedDescription)
    113     }
    114 }) {
    115     Text("Add")
    116 }
    117 ```
    118 
    119 Some of you may have figured it out by this point. At the time, I was
    120 still confused – this was the exact method I was using in my previous
    121 app, but with preset values – how could it be broken? I modified the
    122 generation slightly so I could tell the difference between tasks, and
    123 hopefully get to the bottom of the issue:
    124 
    125 ```swift
    126 let newTask = Task(context: self.managedObjectContext)
    127 newTask.name = String(UUID().uuidString.prefix(Int.random(in: 5..<9)))
    128 newTask.priority = Int16.random(in: 1..<4)
    129 newTask.notes = [String]() as NSObject
    130 newTask.id = UUID()
    131 newTask.date = Date.init()
    132 newTask.daily = Bool.random()
    133 ```
    134 
    135 I ran the app again and saw this:
    136 
    137 {{ video(sources=["randomized-test-values.webm", "randomized-test-values.mp4"])  }}
    138 
    139 ## Intentional Behavior
    140 
    141 The tasks weren't being marked off in ascending order. They were being
    142 moved to the bottom automatically when marked as complete, which I
    143 couldn't see because a) all the tasks were identical and b) there were
    144 no animations to indicate that was happening. They were sorted by the
    145 `FetchRequest` with a `NSSortDescriptor`, to make sure that the
    146 unfinished tasks are the first thing the user sees. The "glitch" I had
    147 spent two days chasing down was entirely by design, and I had just
    148 forgotten.
    149 
    150 There were two main things I learned from this experience. First, it's
    151 incredibly important to be able to take breaks. The difference between
    152 spending two days trying to fix a non-existent glitch and realizing it's
    153 a feature you implemented could be as simple as a nap – it was for me.
    154 Secondly, your test and placeholder data is more significant than you
    155 might think: [garbage in, garbage out] definitely applies here. If all
    156 your test data is the same, your tests are not good tests.
    157 
    158 ## Wrap-up
    159 
    160 To make the sorting more clear, I randomized the tasks' priority, name,
    161 and category (as seen above) and added an animation with
    162 `.animation(.easeIn(duration: 0.25))`. The current prototype looks
    163 something like this:
    164 
    165 {{ video(sources=["update-preview.webm", "update-preview.mp4"]) }}
    166 
    167 This has been a really fun blog post to write! A got a big laugh out of
    168 this bug chase, and I hope you've enjoyed reading it.
    169 
    170 Till next time, FIGBERT
    171 
    172 [WWDC2020]: https://web.archive.org/web/20201105203007/https://developer.apple.com/wwdc20/
    173 [App and Scene structure]: https://developer.apple.com/videos/play/wwdc2020/10037/
    174 [garbage in, garbage out]: https://en.wikipedia.org/wiki/Garbage_in%2C_garbage_out