Best Practices in SwiftUI Composition

Some thoughts on SwiftUI view composition, code readability and application performance

Michael Long
Jun 15 · 8 min read

SwiftUI will be a game changer in how we build future applications for iOS, iPadOS, macOS, tvOS, and watchOS.

But the full impact of SwiftUI doesn’t lie just in eliminating UIKit and swapping Views for UIViews, or Lists for UITableViews, or even by totally eliminating the need for UIConstraints and UIView anchors by the thousands.

Nor does it lie in the dramatic simplification our apps will achieve by eliminating Storyboards, IBOutlets, IBActions, Segues, and all of the other associated boilerplate that’s chained down our applications for so long. Not to mention zapping the bugs that often lurk deep inside your code due to accidentally disconnected outlet’s and actions.

Don’t get me wrong. SwiftUI will give us all of those benefits. And more. But from my perspective, SwiftUI’s primary impact will come not in how we build our application’s interfaces…

But in how we architect our applications.


SwiftUI View Best Practices

I’ve got plenty to say about view models and view state management. More than enough for several articles in fact.

But today I’m going to focus on what I consider to be some best practices in coding our user interfaces, and our views, and view hierarchies.

  • View Composition
  • Focus on functionality, not appearance
  • Use Semantic Color
  • Architect with other platforms in mind
  • Let the system do it’s thing
  • Bind state as low in the heirarchy as possible

Ready? Let’s dig in…

View composition

If one thing was repeated over and over again during the SwiftUI sessions at WWDC, it’s that SwiftUI Views are extremely lightweight and that there’s little to no performance penalty involved in creating them.

Unlike UIViews in UIKit, most SwiftUI Views exist as Swift structs and are created, passed, and referenced as value parameters. While this may have some unwanted ramifications at this point in time, the use of structs avoids a plethora of memory allocations and the creation of many heavily subclassed and dynamic message-passing UIKit-based UIViews.

Further, and again unlike UIView’s, the parameters and modifiers on a nested SwiftUI view are composited together into a single entity during the layout and display cycles. Moreover, the nodes in the view tree are monitored for state changes and — if unchanged — usually don’t need to be rerendered.

Each and every UIView, on the other hand, is allocated and exists as a linked subview somewhere on the layout rendering tree and is an active part of the layout process.

All this means that in SwiftUI it’s to your distinct advantage to create as many distinct and special purpose views as your app may require.

struct FootnoteText : View {
let text: String
var body: some View {
MultiLineText(text: text, alignment: .center)
.font(.footnote)
}
}
struct MultiLineText: View {
var text: String = ""
var alignment: HAlignment = .leading
var body: some View {
Text(text)
.lineLimit(nil)
.multilineTextAlignment(alignment)
}
}

In the above example, a MultiLineText view would probably be used quite a bit throughout an application. A Footnote view is simply a specialized MultiLineText with a specific font modifier attached.

So throughout the app you can simply reference…

FootnoteText(text: $model.disclamer)

…as needed, and as opposed to scattering the following through your code:

Text($model.disclamer)
.lineLimit(nil)
.multilineTextAlignment(.center)
.font(.footnote)

A properly named view like FootnoteText more formally announces your intent to anyone else who later comes along and reads your code.

Small, well-contained views are also easier to reason about, and are a lot less likely to contain bugs and unintentional side effects.

Take a page from the Smalltalk playbook, where many functions seemingly consist of a line or two of code before delegating the functionality to another function… which in turn does the same thing.

UIKit’s core design philosophy is inheritance.

SwiftUI’s is composition.

Focus on functionality, not appearance

Pop quiz, in the following code, what color is the text?

Text($model.disclamer)
.foregroundColor(.red)
.foregroundColor(.green)

(Sound of Jeopardy music playing in the background…)

Answer? The text is .red. In this case the closest value is bound to the view.

So what does that mean? Well, you might be tempted to specify the footnote text in the first example as:

struct FootnoteText : View {
let text: String
var body: some View {
MultiLineText(text: text, alignment: .center)
.foregroundColor(.gray)
.font(.footnote)
}
}

Where you specified the color as gray because that’s the color you’ve wanted so far. But the problem is that you’re now stuck with it, and attempting…

FootnoteText(text: $model.disclamer)
.foregroundColor(.red)

Still yields gray text. Other problems abound with our example now, such as we’ve probably screwed up automatic Dark Mode adaptation by specifying a specific single text color value, and we’ve also probably screwed up the possibility of using our FootnoteText view on other platforms with differing appearances and color schemes.

So hold off on the purely visual appearance modifiers in your view components. Especially color.

Use Semantic Color

But if you must set colors, strongly consider using Semantic Colors.

Color.primary, Color.secondary, and Color.accentColor are all just examples of colors provided by the system and environment. Even a color like .orange can and will properly adapt to light and dark mode, varying slightly in the process.

You can also define your own semantic colors in Xcode. And like Apple, you can even adjust them for light and dark mode.

The nice thing with this approach is that you can easily change your color schemes and branding application-wide in a single spot. You can even modify your schemes per platform without changing your code just by providing a different color set in that platform’s xcassets directory.

Speaking of which…

Architect with other platforms in mind

With SwiftUI, it’s easier than ever to take the same code and use it across platforms. To some extent you could do this before taking your models and API code and business logic from platform to platform, but now it’s entirely possible to use many of your UI elements across platforms as well.

This was readily apparent in Apple’s SwiftUI on All Devices presentation during WWDC.

I won’t go into a lot on this since the presentation covers it so well, but keep in mind that a lot of the cross-platform user interface sharing between iOS apps and iPadOS apps and macOS apps and tvOS apps come from plugging the same content views you’ve created on one platform into a different navigation structure on another platform.

So again, consider where you might want to specify things like fonts, colors, and the like. SwiftUI has several mechanisms for providing or delaying those specifications.

We discussed semantic colors above, but here’s another example.

Group {
MyCustomTextField($model.username)
MyCustomTextField($model.password)
}
.font(.headline)
.background(Color.white.opacity(0.5))
.relativeWidth(1)

Group is a powerful tool that lets you well, group things that need to be manipulated together or, in this case, that share common attributes.

In this sample the group modifiers are applied to each MyCustomTextField,and as such each one will pick up the headline font, the background color, and be sized to the full width of the parent container.

MyCustomTextField deals with functionality. Let the context deal with style.

Let the system do it’s thing

Also remember that SwiftUI automatically translates views into the visual interface elements appropriate for a given platform. A Toogle view, for example, renders differently — and correctly — on iOS, macOS, tvOS, and watchOS.

Further, SwiftUI will properly adjust colors, spacing, padding, and the like based on the platform, on the current screen and/or container size, on control state. It will also take into consideration any changes needed for any accessibility features that might be enabled, do the right thing for Light/Dark mode, and more.

Just to pick one example at random, paragraph text padding presents one way on iOS, but tends to have more border space on an iPad’s screen… unless your content is now compressed by appearing in a Slide-Over view. A lot of considerations that, to be honest, we don’t always take into account.

In UIKit we probably just plugged a value of 15 into the constraint field on the storyboard and moved on.

What that means for us in the here and now is that we need to relax and let the system do its thing. Avoid obsessing trying to match pixel-perfect design layouts often created for a single “best case” layout on a specific screen size for the default accessibility mode.

Take a cue from web developers, who’ve moved from highly rigid design layouts to highly responsive ones, well suited to each platform and to the needs of the individual user.

This may mean several conversations with your UI/UX design team.

Apple’s gone to a lot of trouble to make sure SwiftUI does the right thing at the right time, just as long as we don’t jostle its elbow. So don’t.

And I want to be clear here. I’m not saying that you can’t, either. Apple’s given us a lot of control over presentation. We just need to know when to let go and save the special cases for very special cases.

Let the system do it’s thing, and you’ll not only write less code, but I’m willing to bet that you’ll have fewer bugs as well.

Bind state as low in the hierarchy as possible

As mentioned earlier, I’m going to save most of my thoughts on view models and state management for later articles, but this concept fits in with the examples shown thus far, so I’ll wrap up with this one.

Observe our trusty FootnoteText view in the following code.

struct MyMainView : View {
@ObjectBinding var model: MainViewModel
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
MainContentView(model)
MainContentButtons(model)
FootnoteText(text: settings.fullVersionString)
}
}
}

Note here that MyMainView is automatically importing an environment object named settings, and is passing settings.fullVersionString to our FootnoteText view.

All well and good… but why does MyMainView know about UserSettings at all? What if we did the following instead?

struct MyMainView : View {
@ObjectBinding var model: MainViewModel
var body: some View {
VStack {
MainContentView(model)
MainContentButtons(model)
ApplicationVersionFootnote()
}
}
}

And defined ApplicationVersionFootnote elsewhere as…

struct ApplicationVersionFootnote : View {
@EnvironmentObject var settings: UserSettings
var body: some View {
FootnoteText(text: settings.fullVersionString)
}
}

Here our environment variable is acquired and used lower in the view hierarchy. MyMainView knows nothing of UserSettings, nor should it care. Nor does MainViewModel, for that matter.

The latter point is a key one. For some time now we’ve attempted to avoid Massive-View-Controller syndrome by embracing some form of view model structure, be it MVVM, VIPER, or whatever.

Unless we were very careful, however, all we managed to do was replace our massive view controllers with massive view models.

In a more traditional MVVM implementation, our UserSettings object would probably be injected into our MainViewModel, and some function or variable or binding created to expose fullVersionString on our view model. This complicates our view model, complicates our injection strategies and initialization code, and contributes to making our application’s components more tightly coupled and rigid.

But in our last example, our ApplicationVersionFootnote view is effectively a small, highly specific, special-purpose view model that couples our environment UserSettings data to a FootnoteText view.

I see a lot of potential for this sort of thing in the future, and it very much fits in with SOLID’s Single Responsibility Principle.

Completion Block

So what do you think? On point? Disagree? Am I missing something?

As always, let me know in the comments below.

Better Programming

Advice for programmers.

Michael Long

Written by

Michael Long (RxSwifty) is a Senior Lead iOS engineer at CRi Solutions, a leader in cutting edge iOS, Android, and mobile corporate and financial applications.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade