View Composition In SwiftUI

Or how to avoid MVB (Massive-View-Bodies) in one easy lesson.

Michael Long
Sep 30 · 7 min read

With dynamic previews, SwiftUI tends to encourage a highly iterative development style. Build a basic view layout. Add some subviews. Add a couple of modifiers to tweak those subviews into shape.

Examine the results. Add more views. Add some more modifiers. Add a few animations. Add some additional state and and toss in some action handlers to mange it.

Repeat as needed. Rinse.

Before long, you have exactly what you wanted.

But if you’re not careful, you probably also have a few other things that you didn’t want.

The MVB Problem

We’re all familiar with MVC (Massive-View-Controllers) and the many problems and issues they create for classic UIKit development.

In SwiftUI, we don’t have ViewControllers. But we do have Views. And view bodies. And during our iterative development process we’ve probably just spent the last hour or so cramming more and more of our code into one.

And so now we have MVB. A Massive-View-Body.

A lot of code in a single file with a ton of responsibilities and logic threaded throughout the entire mess.

Solutions

If you’ve been doing iOS development for awhile, you’re probably familiar with many of the solutions used to mitigate the MVC issue. MVVM (Model-View-ViewModel) is one common approach, with the ViewModel used to separate the Model from the View (and the ViewController).

With MVVM, we attempt to place the majority of our business logic and state management in the ViewModel, with both the Model and the View (and ViewController) being as “dumb” as possible.

MVVM is a classic architectural solution to the “MVC” problem.

But it’s not the only one.

In fact, the “classic” solution dates back all the way to iOS 2.0.

Custom Views

It’s a fact of life that a single application with a certain number of features will need a specific amount of code, and that code has to reside somewhere.

But the operative word in that sentence was somewhere. If your views and subviews are smarter and if, for example, they know how to unpack a specific model type and display the results, then that code doesn’t have to be in the ViewController.

The code still exists, but it’s not in the ViewController and that makes the ViewController smaller, simpler, and easier to understand.

In UIKit you can create custom UIViews and NIBs, but to be frank, it was always a bit of a hassle to create two distinct files, wire them up, and then integrate the results back into your code. You could do it, but often we simply didn’t bother and we lived with the added complexity in our ViewControllers.

Or if we were using a more modern approach and used MVVM, then we lived with the added complexity in our ViewModels. (And which could lead to MVM’s (Massive-View-Models), but that’s another article.)

Fortunately, SwiftUI makes things much, much, much easier. To the point where, today, we really don’t have an excuse not to do it.

Let’s take a look.

The Code

I based the examples in this series on Paul Hudson’s iDine application. If you’re still learning SwiftUI (and who of us isn’t?) then I highly recommend watching Paul’s video tutorial series on creating this demo application.

In one part of the app, users on the detail screen can mark an item on the menu as a favorite, which in turn will cause it to appear on a Favorites screen for easy access.

Let’s look at the relevant portion of the code for the detail view where we place a Favorites button in the navigation bar. (Some code omitted for clarity.)

struct DetailView: View {    @EnvironmentObject var favorites: FavoriteService

let item: MenuItem
var body: some View {
VStack {
...
}
.navigationBarItems(trailing: Image(systemName: favorites.isFavorite(item) ? "star.fill" : "star")
.foregroundColor(.accentColor)
.onTapGesture {
self.favorites.toggleFavorite(self.item)
}
)
}
}

Here we add an image to the trailing navigation bar item.

The star is filled if the item is already a favorite and is empty if it’s not. Finally, we add an onTapGesture to toggle the state in our Favorites service, obtained via an EnvironmentObject.

Straightforward enough, but when it’s mixed into all of the other code it can be a bit difficult to parse this functionality out from everything else that’s going on.

So let’s fix that.

View Functions

One way to simplify the main view body is to simply break out the relevant portions of the view into distinct functions, each returning their segment of the main view.

struct DetailView: View {    @EnvironmentObject var favorites: FavoriteService

let item: MenuItem
var body: some View {
VStack {
...
}
.navigationBarItems(trailing: favoritesButton())
}
func favoritesButton() -> some View {
let name = favorites.isFavorite(item) ? "star.fill" : "star"
return Image(systemName: name)
.foregroundColor(.accentColor)
.onTapGesture {
self.favorites.toggleFavorite(self.item)
}
}
}

Notice how our main body is now easier to understand, and even the favorites button code itself is easier to read and figure out.

That said, even though the code isn’t in the View body it’s is still in our DetailView, as is the supporting environment variable. Can we do better?

We can.

View Decomposition

What we should do, and in fact what Apple recommends in the SwiftUI WWDC video, is break out our favorites button into its own View.

First, let’s examine what the change does to our DetailView….

struct DetailView: View {let item: MenuItemvar body: some View {
VStack {
...
}
.navigationBarItems(trailing: FavoritesButton(item: item))
}

}

Note that our DetailView is now a much cleaner. But the biggest improvement from an architectural standpoint is that it’s no longer mediating between the button display and the FavoriteService provided by the EnvironmentObject.

The DetailView just wants a FavoritesButton in the nav bar. It doesn’t care what that button does or how it does it, just as long as it works as advertised.

FavoritesButton

The button display and behaviors are now in their own struct, and that view component is also much easier to understand, reason about, and test.

struct FavoritesButton: View {    let item: MenuItem    @EnvironmentObject var favorites: FavoriteService    var imageName: String {
favorites.isFavorite(item) ? "star.fill" : "star"
}
var body: some View {
Image(systemName: imageName)
.foregroundColor(.accentColor)
.scaleEffect(1.2)
.onTapGesture {
self.favorites.toggleFavorite(self.item)
}
}
}

Our favorites button is now completely self-contained and — as an added bonus — can now be used anywhere throughout our application.

Performance

As I pointed out in Best Practices in SwiftUI 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.

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.

Late Binding

As also pointed out in the aforementioned article, what I consider to be another SwiftUI “best practice” is to bind state as low in the hierarchy as possible.

Here, the Favorites service is bound directly to the object that needs it. The DetailView neither knows nor cares about it. Someone higher up the chain had to provide it, of course, but that’s a different responsibility for someone else to deal with.

Completion Block

One could argue that this was simply a long-winded take on method refactoring… to which I’d agree. To a point.

But this specific exercise in refactoring is one of the keys to successful application development in SwiftUI. Do it well, and you end up with a clean, well structured SwiftUI application.

Don’t and you’re likely to end up with a mess.

It’s pretty simple.

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

As always, let me know in the comments below.

EDIT: Environment Objects

Environment Objects are a fundamental architectural building block for SwiftUI and they work amazingly well… except when they don’t

To learn more, check out the next article in the SwiftUI series, SwiftUI and the Missing Environment Object.

The Startup

Medium's largest active publication, followed by +516K people. Follow to join our community.

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.

The Startup

Medium's largest active publication, followed by +516K people. Follow to join our community.

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