SwiftUI under the hood

Omar Radwan
8 min readJul 20, 2023

--

Hello, welcome back with me again. Today our main talk is gonna be on SwiftUI, not gonna explain views or modifiers or animations but How SwiftUI works under the hood, and we will going to demystify it.

I suggest this article to everyone who wants to learn SwiftUI from scratch or also who had knowledge but needs to be deep in SwiftUI.

SwiftUI as a framework

We all know that SwiftUI is a declarative UI framework and a declarative framework is about what to do not how to do it. So That means that you describe what you want for your app at a high level, and SwiftUI decides exactly how to make it happen. That’s why SwiftUI is very easy, unlike imperative frameworks like UIKit. Imperative programming techniques, where we specify the steps that the program should take to achieve a particular result.

SwiftUI behind the scene

When you write your code in SwiftUI it translates to three types of behaviors:- Identity, Lifetime, and Dependencies.

Identity

Identity is how SwiftUI recognizes elements as the same or distinct across multiple updates of your app. Before we deep dive into identity look into this picture.

Same or different cats?

Now if I ask you if you can tell me this one cat with two different positions or these two different cats? you can't tell me, right? That means the view can transition between states and changes (color — position — frame) but it’s still the same view or it can be more than one different view.

For now, let’s look at how identity is represented in your code, focusing on the two different types of identity used by SwiftUI.

Explicit identity: using custom or data-driven identifiers. Assigning names or identifiers like this is a form of explicit identity and we need to keep track of all those unique names. So we need to know the name of the cat || the cats to know if they are the same or not (SwiftUI want not me 😁).

UIKit and AppKit use pointer identity which is one form of explicit identity but SwiftUI doesn’t because its value types use structs however UIKit & AppKit uses classes and their reference type.

UIViews and NSViews are classes & their memory pointers

Each UIView or NSView has a unique pointer to its memory allocation. We can refer to individual views just using their pointer, and if two views share the same pointer, we can guarantee that they are really the same view.

Check this link to know why SwiftUI uses value types instead of classes for its view SwiftUI essentials

Now we will show an example of how SwiftUI uses this special identifier to improve performance and generate correct transitions.

dogTagID — Exipilicit identifier | figure from Apple session “demystify SwiftUI”

if one dog from the rescue dogs section changes and go to adopted dogs SwiftUI will know by dogTagID to understand the changes and perform animation in the right way smoothly.

Note: Every view in SwiftUI has an identity even it’s expilicit or structural identity

— — — — — — — — — — — — — — —

Structural identity: distinguishing views by their type and position in the view hierarchy. The structural identity is how SwiftUI understands your view hierarchy without any specific identifiers. SwiftUI accomplishes this by looking at the type structure of your view hierarchy.

Now let’s see an example to demonstrate what is happening behind the scene on structural identity.

struct ExampleView: View {
let isLogged: Bool
var body: some View {
if isLogged {
FeedView()
} else {
LoginView()
}
}
}

print(Mirror(reflecting: ExampleView(isLogged: false).body))
/* Translate to
_ConditionalContent<
FeedView,
LoginView
>
*/

SwiftUI now translates our hierarchy to act as a tree and with if/else we have branches. SwiftUI translated the if/else to _ConditionalContent view, which is generic over its true and false content by the property wrapper @ViewBuilder.

The View protocol implicitly wraps its body property in a ViewBuilder, which constructs a single generic view from the logic statements in our property. This ViewBuilder is responsible for this translation, which is a type of result builder in Swift.

Learn more about @ViewBuilder in this article

Now SwiftUI knows (In Runtime) that the true view is always FeedView and the false view is LoginView and if any change happens SwiftUI will destroy and deallocate the view state.

Note: rebuild the ExampleView again from scratch by a new state that happens by comparison with the previous state. SwiftUI see what is just changed and change it’s specific state fo that and that very important note to know

Let’s see another Example to understand the Branching algorithm.

struct ExampleView: View {
let isEnabled: Bool
var body: some View {
if isEnabled {
FeedView()
} else {
FeedView()
.disabled(true)
}
}
}

In the current example, we still use the branching with if/else and SwiftUI destroys the view and creates another if isEnabled toggled. In this case, we lost our state for the instance of the FeedView state. The branching algorithm is good and powerful, especially in different views but if the view is the same there is another way recommended by Apple and more efficient in these cases like that.

struct ExampleView: View {
let isEnabled: Bool
var body: some View {
FeedView()
.disabled(isEnabled ? true : false)
}
}

That’s much better for performance and less for writing code. We inline the condition inside the view modifier which allow us to keep our state and our structural identity without any deallocations.

Now that we understand how SwiftUI identifies your views, let’s explore how identity ties into the lifetime of your views and data.

Lifetime

First, What is a lifetime of view or data?

Lifetime in SwiftUI is how SwiftUI tracks the existence of views and data over time. And This will help you better understand how SwiftUI works. Now let’s see what I mean in this example.

figure from Apple session “demystify SwiftUI” wwdc21

Now we see that our emoji/view is in different states with different values over time and moves through its lifetime. Identity allows us to define a stable element for different values and every single state is a different value for view.

We have PurrDecibelView a single view and take in his initializer an intensity that changes the state over time in the same view and when the value changes SwiftUI destroys the old state. SwiftUI will keep around a copy of the value to perform a comparison and know if the view has changed.

Once the identity of the view changes from onAppear() to Disappear or the view is removed, its lifetime ends.

Note: A view’s lifetime is the duration of its identity in memory.

To provide my model type with a stable notion of identity it should conform to Identifiable protocol to make keep track of lifetime efficiency.

struct Emoji: Identifiable {
var id: UUID()
var shape: String
}

Important: your identity needs to be stable over a lifetime and non-changeable.

Dependencies

how SwiftUI understands when your interface needs to be updated and why depends on something. A dependency is just an input injected into the view. When the dependencies change, the view is recomputed and generates a new hierarchy body.

struct CatView: View {
@Binding var cat: Cat // -> Dependencies 1
var food: Food // -> Dependencies 2

var body: some View { // -> Body
Button {
cat.eat(food) // -> Action
} Label: {
Text("Eat")
}
}
}

At our first look at the view, we see that dependency 1,2 is just input to the view. But when a dependency changes, the view is required to produce a new body (Hirariechy of our view). inside our body, we can see the Button with action. Actions are the trigger that changes the view’s dependencies. and let’s see how.

We have [Cat and Food] injected -> [CatView] that contain -> [Button] with gesture make -> [Action] -> Changes the cat after eating and if he want more we gonna to go again to first ~ [Cat and Food] injected -> [CatView] that contain -> [Button] with gesture make -> [Action]…..

When the dependencies changes, The body is recomputed and generates a new one. Check this to understand more about Data flow in SwiftUI.

In SwiftUI, each view can have its own set of dependencies. So far, this still looks like a tree.

Dependency tree

there can be multiple views dependent on the same state or other data. For example, one of the descendants might depend on the cat, too. And this could happen for one of our other dependencies. So we started with a tree, but this structure only loosely resembles a tree now.

In fact, if we rearrange it to avoid overlapping lines, we end up with this structure, which reveals that this is actually a graph, not a tree.

We call this structure the “Dependency graph”.

This structure is important because it allows SwiftUI to efficiently update only those views that require a new body.

The secret of the graph is that if the dependency changes, only those views will be invalidated. SwiftUI will call each view’s body, producing a new body value for each view. SwiftUI will instantiate the values of each invalidated view’s body.

That may result in more dependencies changing, but not always! Because views are value types, SwiftUI can efficiently compare them to only update the right subset of views.

A view’s value is short-lived. The struct value is just used for comparison, but the view itself has a longer lifetime.

And that’s how we can avoid generating a new body for the view in the center.

An identity is the backbone of the dependency graph.

Conclusion

SwiftUI is amazing and simpler than UIKit in many things but you need to understand carefully what are you typing and type it in the right way to achieve the best performance. If you understand carefully these three concepts you will write “Better apps with less code“ 😁🍎 in a perfect way.

Resources

I hope you enjoyed this article and if you have a question you can ask me anytime on my LinkedIn or you can see Apple videos and I confirm that you’ll understand deep dive better.

--

--