figbert.com-gemini

[ACTIVE] the capsule and home of figbert in geminispace
git clone git://git.figbert.com/figbert.com-gemini.git
Log | Files | Refs | README

2020-07-28-pebkac-txtodo-rewrite.gmi (6724B)


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