The upcoming release of Grocery focuses on speed. We can all agree that the main thing your app does should be fast, and for Grocery that's checking items off your list. As we added features to the apps the performance of checking items off a list slowed down - a lot. For some users, checking off items could take 3-4 seconds on iPhone, and 5+ seconds on Apple Watch. We needed to address that now before adding more features.
The problem on iPhone was simple to understand and solve using Instruments. When you mark an item off your list on the iPhone, we were waiting for the update to Reminders to finish before moving the table cell. Making that update asynchronous and moving the cell immediately solved the problem. When you mark an item off your list now, the list update happens instantly, just like it should be!
The problem on Apple Watch was harder to understand and harder to solve, and that's the focus of this blog post. Troubleshooting performance on Apple Watch can be tricky. You can try to identify red flags if they exist by running Instruments against the simulator, but the only way to truly evaluate performance on Apple Watch is with the physical device itself. Everything that seems slow on device will feel completely normal on the simulator. You have to test on hardware to solve the problem.
Grocery's Apple Watch app is very simple - just one table view where tapping on a cell checks the item off your list. That's why this particular issue was so perplexing: the app isn't doing anything that seems too complicated. When an item button is pressed, the app sends a message to the iPhone to tell it which item was checked off, and then removes the cell from the table view. The app isn't updating the Reminders database itself, so why is it so slow? Time to investigate!
This blog post describes my nineteen step process to faster buttons on Apple Watch, which is composed of the following individual steps that can be used repeatedly starting with the first one:
- Disable Everything and Re-evaluate Performance
- Refactor/Extract Functionality As You Go Into Their Own Methods
- Start Adding Trivial/Simple Functionality Back
- Turns Out Some "Trivial" Functionality Actually Hurts Performance
- Identify Major Problem Areas and Either Improve or Remove Them
- Put it All Together and Test The Final Version on Device
Step 1 - Disable Everything and Re-evaluate Performance
When tapping on a button feels slow on the Apple Watch the best thing to do is remove everything from the IBAction method and test it again. Button taps should feel as close to instant as possible on the watch. That's a major focus for watchOS 4 with the Unified Process Runtime - making touch interaction feel even faster and more reliable. If you remove everything from your action method and performance returns to normal, then you know something in that method is causing the slow down.
Commenting out the implementation also made me realize just how much functionality had been added to that button press action over time. What started out as a very simple method now included a laundry list of functionality:
- Update the Apple Watch's Sorting Database
- Updating the Table Data Model
- Removing the Table Cell
- Sending a WatchConnectivity Message to the iPhone
- Playing a Haptic Feedback
- Updating the Remaining Items Count
- Hiding a Placeholder Group
- Updating the Complication
Sure enough, after commenting all of that out things felt fast again. This approach is also very motivating because you get to see how fast it can feel which makes you want to achieve that performance with the rest of the functionality restored.
Step 2: Refactor/Extract Functionality As You Go Into Their Own Methods
While you're working on the button action method I think it's a great idea to refactor and re-organize the functionality of that method into smaller methods with a single responsibility. As I had been adding features and functionality to that button action I had just been adding them to the same method. I took this opportunity to move each area of functionality into its own method. This has the dual benefit of cleaning up the code as well as making it easier to see what you're turning on and off while evaluating button performance.
Step 3-7: Start Adding Trivial/Simple Functionality Back
I started adding functionality back one piece at a time, beginning with the most trivial pieces that I assumed wouldn't have any impact on performance. I installed a new device build after each piece to test performance on a physical watch.
Most of the trivial features didn't affect performance at all. Haptics and hiding the placeholder group had no impact. Drawing a strike-through line through the item label with an attributed string didn't seem to hurt. Removing the table cell and removing the item from the array didn't hurt either.
Updating the remaining item count was the first place that I noticed a small change in performance. That involved counting the number of remaining items and updating the Interface Controller's title property. The change was barely noticeable though, so I decided to keep that feature in.
Step 8-10: Turns Out Some "Trivial" Functionality Actually Hurts Performance
The next seemingly trivial feature I added back to my button action was updating the complication. Updating the complication isn't slow on its own, but the way I was updating the complication was triggering a reload from the Reminders database. When I added this method back to my button action performance slowed down considerably. Once that happened it gave me an area to investigate further, which lead to identifying the database reload. By addressing that issue I was able to reload the complication after marking an item off the list without hurting button performance!
Step 11-18: Identify Major Problem Areas and Either Improve or Remove Them
The two major problem areas turned out to be updating the sorting order on the watch, and sending the message to the iPhone to tell it which item was marked off the list.
Updating the sorting order was actually completely unnecessary. In an earlier version of the Apple Watch app we had been moving marked off items down to the bottom of the list instead of removing them. Removing them from the list made more sense because of the small size of the Apple Watch screen. When we changed that behavior we didn't remove the sorting change - which was actually a pretty significant performance penalty. Removing that made a huge difference!
Sending the message to the iPhone using Watch Connectivity made more of an impact than I expected it would. Making that method call asynchronous by calling it on a background queue made our button action feel a lot faster, so that was the only change we needed to make there.
Step 19: Put it All Together and Test The Final Version on Device
Once all of the button action features are refactored, removed, disabled, moved, or improved then it's time to put it all back together and test the final product out to make sure that all the effort actually made a difference in performance. After a few days of testing it's definitely feeling like a big difference.
Troubleshooting performance on Apple Watch can be tricky but the effort is well worth it, especially for a device intended for small and quick interactions. It's truly a device where every second counts, and a little bit of testing can help make the difference between an app that feels fast and an app that feels too slow to use. Even with the upcoming improvements to app performance with watchOS 4, anything that we can do to help our apps feel faster will make a big difference for Apple Watch users.