Wallaroo and SwiftUI on iOS
Wallaroo is the first app we’ve built entirely with SwiftUI, so in this series of more technical posts, I want to talk about some specific issues we ran into along with how we solved them. In a separate post, Craig discusses development of the whole app so if you prefer to get a bird’s eye view of Wallaroo’s creation, be sure to check his article out.
[…]
I suspected this was going to be tricky because the framework didn’t (and still doesn’t) have even a simple flow layout, let alone something complicated like a flowing irregular grid! Luckily WWDC22 had just announced the new Layout protocol for SwiftUI, so I hoped I’d be able to use it to make this work.
See also: The SwiftUI Layout Protocol – Part 1 and Part 2.
We were a few days into this, and I was feeling pretty good about it when the first real-world complication reared its ugly head: Custom SwiftUI layouts aren’t lazy!
[…]
What I needed to solve this was some way to discern if the view was actually on screen or not and then emit the appropriate view.
I decided that knowing where the containing gallery’s frame was would likely make sense as an environment value, and so I added the
visibleGalleryFrame
that was computed in the code snippet in part 1 as an environment value for all of the child tile views to use. To get the parallax to work, the tile views measure the frame of whatever area they want to apply the parallax effect to and compare it with the environment’s gallery frame and offset things accordingly.[…]
It feels like the only thing preventing people from using the environment more often (and perhaps “incorrectly”) is the fact that defining a new environment value involves a bunch of boilerplate in an
EnvironmentValues
extension! It doesn’t seem ideal to only depend on that friction to guide behavior, though, especially since I’d love to see some kind of new Swift syntax or feature that could eventually make the boilerplate go away!
See also: You can create your own SwiftUI Environment.
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, usedNSCache
or a simpleDictionary
, used a Swiftactor
, 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.[…]
Another of the limitations of
AsyncImage
that I was able to work around in our custom implementation is thatAsyncImage
only delivers the finalImage
after the load finishes and not the original data.[…]
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.
Sean Heber (Craig Hockenberry):
The biggest surprise for me was how much trouble I had implementing the paging view to swipe left and right between wallpaper variants. […] I ran across a bunch of tutorials that were manually implementing panning gestures and paging and I was flabbergasted. Did I really need to do all of this myself from scratch?
[…]
Eventually I decided that maybe I should just wrap a
UIScrollView
to accomplish what we needed and started to dive into that.I was pretty far into this when I accidentally stumbled across a Stack Overflow comment noting that SwiftUI actually does have a native paging view – it’s just cleverly hidden as a
TabView
style! […] Unfortunately it is extremely buggy.[…]
There’s also something off about how SwiftUI documentation is written and organized. It frequently feels next to impossible to find the name of whatever view modifier you might be needing unless you more or less already know what you’re looking for. To make matters worse, the fact that SwiftUI’s view modifiers almost all exist as function extensions on
View
means just pressing the period key and browsing autocompletion suggestions tells you nothing about what might or might not make sense to use in your current context.
Instead of passing an object around that was nothing more than a thin wrapper of the navigation path, I created an action struct that implemented
callAsFunction()
just as Apple does withDismissAction
,OpenURLAction
,RefreshAction
and others. The root view then added the action to the environment so all child views could use it.With this approach, the
NavigationPath
is private to the root view which remains in control of adding things to the path. The root view also already implemented.navigationDestination
for the views in the navigation stack, so it made sense to me that it should own how pages get pushed, too.When the detail view needs to open a new gallery page, it uses my new
OpenPageAction
from the environment to request the new page much like this:openPage(.gallery(.tag("abstract")))
.[…]
In my opinion, a lot of the app’s ancillary views would have required far more code and time to build with UIKit and that’s an important point in favor of SwiftUI despite the occasional trouble in some corners.
SwiftUI is new with some rough edges. Part of the @wallaroo_app project was to explore what those edges looked like – and this series of blog posts may leave you with the notion that you shouldn’t go near this stuff.
But the reality is that both Sean and I loved working with it.
We’re enough years in to it now for me to be pretty sure SwiftUI is not the way forward for my apps, on Apple’s existing platforms. It’s just not how I want to write code. I remain open to it being potentially great for building new kinds of apps on Apple’s headset, but I’m not holding my breath (nor perhaps do I need to write new kinds of apps if the old kinds are best). I hope there always remains a bigger, more-powerful alternative under the hood that I can drop down to to build better apps.
Previously:
- Converting the Streaks Apple Watch App to SwiftUI
- Thoughts on SwiftUI After WWDC 2022
- SwiftUI Changes at WWDC 2022