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