When SwiftUI was first introduced at WWDC 2019, it definitely pushed many aspects of both Swift and Xcode to its very limits — through its heavy use of generics, closure-based APIs, and brand new features, such as property wrappers and function builders.
So its therefore not very surprising that a big focus of the upcoming new version of Swift, 5.3, is to continue to expand the ways in which Swift can be used to build SwiftUI-style domain-specific languages (or DSLs), and to smoothen out a few “rough edges” that many developers have encountered when using SwiftUI with Swift 5.2 and earlier.
This week, let’s take a look at some of those advancements, and how they collectively enhance the overall experience of building views using SwiftUI.

Since the very beginning, Swift has required us to explicitly specify self when accessing an instance method or property within an escaping closure, which sort of acts as an “opt-in” for having that closure capture the enclosing object or value — since doing so could end up causing retain cycles in certain situations.
However, since the risk of retain cycles is often quite negligible when using value types, that requirement of always having to specify self has been somewhat relaxed in Swift 5.3 — by enabling the compiler to implicitly capture struct instances, which SwiftUI views and modifiers are almost exclusively implemented as.
As an example, let’s say that we’ve built the following FavoriteButton using Swift 5.2, which requires us to use self when referencing its isOn property within its underlying button’s action closure:
struct FavoriteButton: View {
@Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}, label: {
Image(systemName: "heart" + (isOn ? ".fill" : ""))
})
}
}
When upgrading to Swift 5.3, however, that self reference can now be completely removed — which gives us a slightly simpler implementation:
struct FavoriteButton: View {
@Binding var isOn: Bool
var body: some View {
Button(action: {
isOn.toggle()
}, label: {
Image(systemName: "heart" + (isOn ? ".fill" : ""))
})
}
}
While the above might be a quite minor change in the grand scheme of things, it does make SwiftUI’s DSL feel slightly more lightweight and easier to use.
Also, as a side-effect, since explicitly specifying self is now only required in situations where doing so really matters (such as when dealing with captured reference types), that should arguably make those parts of our code base “stand out” a bit more, which in turn could make it easier to spot potential retain cycle-related issues within such code.
When building UIs in general, it’s incredibly common to want to use separate view implementations depending on what kind of state that a given app or feature is currently in.
For example, let’s say that we’re currently building an app that uses an AppState object to keep track of its overall state, which includes properties like whether the user has gone through the app’s onboarding flow. We then check that state within the app’s root view to determine whether we should display either a HomeView or an OnboardingView — like this:
struct RootView: View {
@ObservedObject var state: AppState
var body: some View {
if state.isOnboardingCompleted {
return AnyView(HomeView(state: state))
} else {
return AnyView(OnboardingView(
isCompleted: $state.isOnboardingCompleted
))
}
}
}
Note how we’re performing type erasure on both of our above view instances using SwiftUI’s AnyView type — which is done to give our body property a single, unified return type. However, using AnyView like that doesn’t just add a fair amount of “clutter” to our code, it also makes SwiftUI’s type-based diffing algorithm less efficient, since all of the type information contained within our view’s body is currently completely erased.
Thankfully, there’s a better way to implement conditions like the one above — even when using Swift 5.2 or earlier — and that’s to manually add the @ViewBuilder attribute to our view’s body property, which lets us make full use of SwiftUI’s function builder-powered DSL directly within that property’s implementation:
struct RootView: View {
@ObservedObject var state: AppState
@ViewBuilder var body: some View {
if state.isOnboardingCompleted {
HomeView(state: state)
} else {
OnboardingView(isCompleted: $state.isOnboardingCompleted)
}
}
}