You don’t need SwiftUI to enjoy its layout syntax

John Estropia
Dec 2, 2021 · 7 min read

This is the December 2 article for Eureka’s 2021 Advent Calendar.

I’ll be tackling one of the controversial topics that are dividing iOS/macOS developers these days, mainly the SwiftUI vs. UIKit mindset that is trending in the industry. Hopefully, I can provide some insights that will reconcile both sides on the topic.

In Eureka we cannot use SwiftUI yet, but I maintain SwiftUI utilities for my open-source library CoreStore and wrote one of my hobby apps in pure SwiftUI initially (though I rewrote it in Texture for performance and maintenance reasons). My observations and opinions on SwiftUI are based on my own experience using it.

The reality of SwiftUI

First, let’s take a look at SwiftUI and the paradigm shift that it brought to Apple platforms.

One of the biggest selling points of SwiftUI is the ease of app development. And I think no one would argue against how easy it is to bootstrap a new app from scratch with SwiftUI. There’s a lot to unpack on why that is, but I personally think it’s two main factors: the layout syntax and the out-of-the-box features.

Layout Syntax

This is the biggest draw for all iOS/macOS developers. Even Apple’s SwiftUI intro homepage puts Declarative syntax as its first talking point.

Sample code: SwiftUI

If you haven’t realized yet, this is almost the same structure as Jetpack Compose on Android. I think declarative UI will be the preferred DSL within programming languages in the near future.

Out-of-the-box Features

With SwiftUI, Apple took the effort to provide automatic support for common features that most apps take for granted: animations, accessibility, and multi-platform support, among others.

For example, SwiftUI’s Button control can be used as-is whether you are writing for iOS, macOS, iPadOS, watchOS, or tvOS. For each platform, the corresponding “button” implementation would be used instead, complete with the relevant design, animations, and accessibility features present for buttons in that platform.

Here are the biggest drawbacks I found with SwiftUI:

Two-way Bindings

On paper, a two-way binding would be better to enforce synchronization of the model and the view, but counterintuitively this causes a lot of UI issues because of necessary delays like animations, network latency, and user perception adjustments. In short, between those periods of delay, the source of truth for the Binding is torn between the model and the view. This is still one of the surprising architectural decisions I find in SwiftUI since it was first released because one-way binding (unidirectional data flow) is the standard in other state-driven architectures like Flux, Redux, Jetpack Compose, etc.

This is a common issue with input controls like Pickers and Sliders. When the user fiddles with a control, the Binding updates the model with the control’s new value, which triggers a re-render on the SwiftUI view tree, which then unnecessarily resets the control. On a Picker, this means any rolling animation is cancelled and will cause flickers, and on a Slider this may move the value back to an older value until the user moves their finger again.

Now obviously, there are workarounds for these but it just goes to show that bindings are unnecessarily more complex than expected.

Nondeterministic Layout Timing

One other well-known quirk in SwiftUI Views is the frequency of evaluation of the View’s body. An update to a @State or @ObservableObject may trigger a reevaluation of the whole view tree in some cases, even if that value changes nothing in the visual appearance.

For example, if data is deleted which causes a screen to be dismissed, the label that displayed that data may disappear (flicker) even before the dismiss animation completes. It’s a tiny difference but it makes your app look clunky. With UIKit, we call setNeedsLayout() or explicitly change NSLayoutConstraints to trigger layout updates, but with SwiftUI we have to be aware of how the declarative syntax will translate into “update triggers”. This makes it a lot harder to debug, and while logging tools exist for SwiftUI, following breakpoints is more intuitive during debugging.

Closed-source

And finally, the most unfortunate fact is that SwiftUI is closed-source (instead of say, maintained through a branch of Swift Evolution), which then caused SwiftUI’s implementation to become tied to iOS versions. This means the behavior of your SwiftUI app is different between iOS 13, 14, and 15 (as of 2021). This completely defeats the multi-platform support we mentioned earlier because now your code has branching logic for different iOS versions instead of different platforms.

And then these differences between SwiftUI versions are not trivial either. Each new iteration added features that would make outsiders think: “Wait, you never had that before?”
To give you an idea, the first versions of SwiftUI never had a native way to:

  • display badge numbers in TabItems
  • change List separator colors
  • display multiline TextFields
  • display NSAttributedStrings
  • add pull-to-refresh to Lists
  • show maps
  • show videos
  • etc.

And yes, I know these were all doable in SwiftUI by writing your own UIViewControllerRepresentable, but those same components were already how you’d have done them in UIKit. These components would have been the pet projects of library developers, but because of the iOS version fragmentation issue, it became harder for the community to maintain 3rd-party controls.

Funnily enough, the most popular 3rd-party SwiftUI library just proved how much the closed-source nature of SwiftUI became a burden. SwiftUI-Introspect: Introspect underlying UIKit components from SwiftUI.

Our previous layout system

Now that we’ve explored the pros and cons of using SwiftUI, I will now introduce the layout frameworks that we use in Eureka’s apps and later show you how we have incorporated some of SwiftUI’s paradigms into our existing apps.

We’ve been using Texture (former AsyncDisplayKit) for several years now, and we still vouch for its performance wins in high-traffic UI. It’s also one of the earliest implementations of declarative layout and stack-based layout on iOS, both paradigms we’d eventually see in SwiftUI.

Sample code: Texture (traditional style)

It’s a bit verbose, but notice how close the structure is compared to our previous SwiftUI sample code. It’s also leagues better than how we’d write the same layout using traditional NSLayoutConstraints. While Texture has its own layout mechanism based on Flexbox, it simply manages underlying UIViews and CALayers so it’s easy for existing iOS projects to incrementally migrate to Texture’s architecture. Unlike SwiftUI, we can call setNeedsLayout() precisely at the point when we’re ready to let the layout recompute itself.

But, it left room for more improvement. Although it’s very intuitive, a simple tweak such as adding a padding would mean having to move the existing tree into a parent ASInsetLayoutSpec container. So something like this in SwiftUI:

contents.padding(10)

would translate to this in Texture:

ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10),
child: contents
)

For less-demanding screens where standard UIKit components are simpler, we use EasyPeasy to simplify our AutoLayout configuration. (EasyPeasy code at the bottom part:)

Sample code: EasyPeasy (bottom part)

This code would be a lot bigger if we create layout constraints using traditional NSLayoutConstraint initializers.

Still, our declarative code was only just the constraints setup code. We’ve looked for possible solutions that would let us write more declarative code using our UIViews but never found a promising candidate.

The power of Result Builders

As with Eureka’s iOS engineering culture, features we want that we could not find anywhere else, we write our own: CoreStore, JAYSON, MosaiqueAssetsPicker, NextGrowingTextView, Rideau, StackScrollView, Verge, and more.

And most relevant to this article, TextureSwiftSupport and MondrianLayout, which utilize Swift’s Result Builders feature to provide the same DSL as SwiftUI, all while still taking advantage of Texture and UIKit.

Our colleague Hiroshi Kimura was quite the genius writing this library. With TextureSwiftSupport, our previous sample code becomes:

Sample code: Texture (result builders style)

We have migrated most of our Texture code to use this result builder syntax, and it is the most satisfying refactoring I’ve done in a while. Code reviews are super easy, and with the ability to use if-else and switch-case blocks for conditional layouts, updating an existing UI component is a lot safer because small changes have minimal effect on the layout code.

As added bonus, TextureSwiftSupport also provides two container types:

class NodeView<T: ASDisplayNode>: UIView

and

class ViewNode<T: UIView>: ASDisplayNode

which let us mix and match Texture nodes and UIKit views together depending on our needs.

But Hiroshi didn’t stop there. This year we’d exchange feedbacks while we convert our AutoLayout code to use his new MondrianLayout library. Converting our previous EasyPeasy code, with MondrianLayout our sample code becomes:

Sample code: MondrianLayout

Now we have all the tools for a fully-declarative UI layout.

Conclusion: Getting both the elegance of Result Builder layouts and the power of traditional iOS components

It was a great journey to be able to find and build tools that would let us write fully declarative UI, without getting locked into a relatively restrictive paradigm like SwiftUI. While others spend time improving performance or hack around missing SwiftUI functionality, we stuck with our battle-tested frameworks and made new tools along the way to instead take advantage of what’s most appealing in SwiftUI: its declarative layout syntax using Result Builders.

If I inspired you to try out TextureSwiftSupport and MondrianLayout or have questions about them, feel free to post a comment below!

And that concludes my Advent Calendar topic this year. Enjoy the rest of Eureka’s engineering blogs!

Eureka Engineering

Learn about Eureka’s engineering efforts, product…