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