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 (6720B)


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