Now that Wallaroo had a unique and good-looking main gallery view along with nice parallax and blurs and everything, my focus shifted to optimizing scrolling performance.
The parallax and lazy custom layout seemed to perform just fine, but rendering the glyphs/buttons on each tile view as well as the actual wallpaper image seemed to be slowing things down a lot.
When I commented each of these bits of code out in turn, scrolling would become silky smooth, but enabling just the glyph/button overlays or just the wallpaper images would result in unacceptable stuttering on my 2016 iPad Pro. Hmm!
Eagle-eyed readers might have noticed earlier that I said I was using AsyncImage
“at the time.” Eventually I replaced it with a custom implementation on my quest to achieve smoother scrolling and then extended it a bit more to solve another problem we ran into.
It seemed (at least on the earlier iOS 16 betas), that AsyncImage
did not prepare the image for display before rendering it. That meant that when two or three images all loaded during the same view update cycle it would cause animation hitches.
I solved this in my custom view by using a .task
modifier so I could async load the image data myself, then convert it to a UIImage
, and then call the async version of UIImage.byPreparingForDisplay
all before finally assigning the SwiftUI Image
instance that would ultimately be displayed to a state variable.
Around this time Craig also realized that as I worked throughout the day my computer was hitting the image server a whole lot and this probably was not going to scale very well once it went live in the App Store.
To address this, I adjusted the URLRequest
for the images to always return from the URL cache directly when possible, but ultimately we fixed our server to set the correct HTTP caching headers so the URL loading system stopped rechecking for freshness all the time and trusted that the cached data was good.
If Craig hadn’t been watching our server logs at the time, I’m betting we’d have totally missed this and then had to deal with a flood of server traffic on launch day!
(Aside: It may sound like we arrived at all of this in a matter or hours, but of course we didn’t. I spent a ridiculous amount of time off and on over weeks trying various caching schemes for my custom AsyncImage
that prepared the image in different ways, kept the prepared image around keyed by URL, used NSCache
or a simple Dictionary
, used a Swift actor
, etc. Happily I was able to get rid of all of that complication once the required pieces finally fell into place. This sort of thing happens a lot and I often spend a bunch of time on things that turn out to be dead ends. I tend not to stop the moment I get something working, but instead keep thinking about it for a while trying to simplify or get rid of as much code as I can. Sometimes the results are good as in this case, but other times it can go badly where stuff becomes much too specific to a single situation and ends up causing problems for myself later. Such is the nature of evolution, I guess?)
Another of the limitations of AsyncImage
that I was able to work around in our custom implementation is that AsyncImage
only delivers the final Image
after the load finishes and not the original data.
We wanted to be able to access the raw downloaded data of the big images as seen in the full wallpaper detail view so we’d have it when saving or setting the wallpaper. I built a way into my custom image view to get the downloaded image’s raw Data
as well as the prepared Image
to avoid needing to re-encode the image or download it again separately. There are other ways to solve this problem (I tried a few), but it seemed the most straightforward to do it this way since it ensured the image data was only ever downloaded once.
After getting the thumbnails to load without a hitch, I moved on to the troublesome issue with the glyphs.
There are at most three symbols on each wallpaper gallery tile: The favorite heart, the multi-variant indicator, and the lock (which only shows up on some of them when unsubscribed). When these overlays were enabled, it caused a ridiculous amount of stuttering.
I had a suspicion as to the root cause: Gedeon had wanted the glyphs to be rendered with a custom shadow using a blend mode that didn’t interfere with the semi-transparent favorite heart but also worked against a wide range of wallpapers. This effect was first implemented early in the prototyping stage and hadn’t been revisited.
Sure enough when I disabled these effects the scrolling was perfectly smooth. I tried a whole bunch of things to get this to perform better until finally stumbling on the fact that I was using a .mask
modifier on the glyph views in order to get the appearance we wanted. When I came up with a much smarter way to make the effect work without masking, the performance skyrocketed. I guess the lesson here is: Don’t use view masking if you can help it!
This highlights one of the weakness of SwiftUI. It is very hard to debug this sort of thing or to get a sense of where time is being spent by the engine because so much of it is inaccessible and outside of our control. Hopefully these things can be improved somehow in the future. For all I know, maybe there are already ways to use instruments to diagnose problems like this – although personally I find a lot of the instruments to be inscrutable and it ends up taking me just as long to trace through a problem with them as it does to hunt around with educated trial and error. But maybe that’s just me.
With most of the issues surrounding the main gallery view taken care of, it was time to finally move on to the much more straightforward wallpaper detail view! How hard can it be?