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