Peek behind the curtain into the core tenets of SwiftUI philosophy: Identity, Lifetime, and Dependencies. Find out about common patterns, learn the principles that drive the framework, and discover how you can use them to guarantee correctness and performance for your app.
♪ Bass music playing ♪ ♪ Matt Ricketson: Hi, I'm Matt, and later on I'll be joined by Luca and Raj.
Today, we're going to demystify SwiftUI.
Now, we've heard it many times before that SwiftUI is a declarative UI framework.
That means that you describe what you want for your app at a high level, and SwiftUI decides exactly how to make it happen.
Now, most of the time, this works great! And that's when SwiftUI feels magical.
But there will always be those moments when SwiftUI does something that you might not expect.
And in those moments, it helps to understand a bit more about what SwiftUI is doing behind the scenes to build a better intuition for how to get the results you're looking for.
So today's question is, When SwiftUI looks at your code, what does it see? The answer is three things: identity, lifetime, and dependencies.
Identity is how SwiftUI recognizes elements as the same or distinct across multiple updates of your app.
Lifetime is how SwiftUI tracks the existence of views and data over time.
And dependencies are how SwiftUI understands when your interface needs to be updated and why.
Together, these three concepts inform how SwiftUI decides what needs to change, how, and when, resulting in the dynamic user interface you see onscreen.
Today, we'll talk about each of these three concepts in more depth.
Let's start with identity, and I have a few friends here to help me.
These Ruby Spaniels are adorable, but they're also the subject of a deep philosophical question.
Are these two different dogs? Or are these, in fact, two pictures of the same dog? The truth is, it's impossible to say! We just don't have enough information.
But this question of whether things are the same or different is at the heart of what we call "identity." Identity isn't just important for dogs though.
It's also a critical aspect of how SwiftUI understands your app.
Let's look at an example.
This is an app I made called "Good Dog, Bad Dog," which helps me keep track of whether my furry friend has been on her best behavior.
It's pretty simple.
I can just tap anywhere on the screen to flip between the good and bad states.
So what does identity have to do with my app? Well, it's actually very similar to that philosophical question we just asked about the dogs.
Looking at those icons, do those seem like two different views, completely distinct from each other? Or could they be the same view, just in a different place and with a different color? That distinction actually matters a great deal because it changes how our interface transitions from one state to another.
Let's say those icons are, in fact, different views.
That means the icons should transition independently, such as fading in and out.
But what if they're, in fact, the same view? That would instead imply that the view should slide across the screen during the transition because it's the same view moving from one location to the other.
So connecting views across different states is important, because that's how SwiftUI understands how to transition between them.
This is the key concept behind view identity.
Views that share the same identity represent different states of the same conceptual UI element.
In contrast, views that represent distinct UI elements will always have different identities.
Later in the talk, Luca and Raj will talk about the practical impacts of view identity on your app's data and update cycle.
For now, let's look at how identity is represented in your code, focusing on the two different types of identity used by SwiftUI.
First, explicit identity: using custom or data-driven identifiers.
And second, structural identity: distinguishing views by their type and position in the view hierarchy.
Now, to help understand these concepts, let me introduce you to some more of my friends.
OK, remember that it can be difficult to tell dogs apart, especially when they look the same.
So what kind of extra information would help us identify our dogs? One way would be to simply ask for their names.
If two dogs look the same and also share the same name, then I'd say it's pretty likely that they're, in fact, the same dog.
But if they have different names, then we can guarantee that they are, in fact, different dogs.
Assigning names or identifiers like this is a form of explicit identity.
Explicit identity is powerful and flexible, but does require that someone, somewhere keeps track of all those names.
One form of explicit identity you may already be used to is pointer identity, which is used throughout UIKit and AppKit.
Now, SwiftUI doesn't use pointer identity, but learning about it will help you better understand how and why SwiftUI works differently.
Let's take a quick look.
Consider a UIKit or AppKit view hierarchy, like this one.
Since UIViews and NSViews are classes, they each have a unique pointer to their memory allocation.
The pointer is a natural source of explicit identity.
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.
But SwiftUI doesn't use pointers because SwiftUI views are value types, commonly represented as structs instead of classes.
In the "SwiftUI essentials" talk from 2019, we discussed why SwiftUI uses value types instead of classes for its views, so I recommend watching that talk to learn more.
For now, the important thing to know is that value types do not have a canonical reference that SwiftUI can use as a persistent identity for its views.
Instead, SwiftUI relies on other forms of explicit identity.
For example, consider this list of rescue dogs.
The id parameter used here is a form of explicit identity.
The dog tag ID of each rescue dog is used to explicitly identify its corresponding view in the list.
If the collection of rescue dogs changes, SwiftUI can use those IDs to understand what exactly changed and generate the correct animations within the list.
In this case, SwiftUI was even able to correctly animate views moving between different sections.
Let's look at a more advanced example.
Here, we're using a ScrollViewReader to jump to the top of the view using a button at the bottom.
The id(_:) modifier provides a way to explicitly identify a view using a custom identifier -- in this case, our header view at the top of the page.
Then we can pass that identifier to the scroll view proxy's scrollTo(_:) method to tell SwiftUI to go to that specific view.
What's great about this is that we don't have to explicitly identify every view, just the ones we need to refer to elsewhere in the code, like our header text.
By comparison, the ScrollViewReader, ScrollView, backstory Text, and Bbutton do not need explicit identifiers.
But just because their identity isn't explicit, that doesn't mean these views have no identity at all because every view has an identity, even if it's not explicit.
This is where structural identity comes in.
SwiftUI uses the structure of your view hierarchy to generate implicit identities for your views so you don't have to.
Now, let me bring in some more of my friends to help explain what I mean by that.
Let's say we have two similar dogs but we don't know their names, but we still need to identify each one.
Well, suppose these are very good dogs and they're capable of sitting very still.
If we can guarantee they don't move, we could identify them just based on where they're sitting, like "The dog on the left" or "The dog on the right".
We're using the relative arrangement of our subjects to distinguish them from each other -- that's structural identity.
SwiftUI leverages structural identity throughout its API, and a classic example is when you use if statements and other conditional logic within your View code.
The structure of the conditional statement gives us a clear way to identify each view.
The first view only shows when the condition is true, while the second view only shows when the condition is false.
That means we can always tell which view is which, even if they happen to look similarly.
However, this only works if SwiftUI can statically guarantee that these views stay where they are and never swap places.
SwiftUI accomplishes this by looking at the type structure of your view hierarchy.
When SwiftUI looks at your views, it sees their generic types -- in this case, our if statement translated into a _ConditionalContent view, which is generic over its true and false content.
This translation is powered by a ViewBuilder, which is a type of result builder in Swift.
The View protocol implicitly wraps its body property in a ViewBuilder, which constructs a single generic view from the logic statements in our property.
The some View return type of our body property is a placeholder that represents this static composite type, hiding it away so it doesn't clutter up our code.
Using this generic type, SwiftUI can guarantee that the true view will always be the AdoptionDirectory, while the False view will always be the DogList, allowing them to be assigned an implicit, stable identity behind the scenes.
In fact, this is the key to understanding the Good Dog, Bad Dog app from earlier.
With the code on the top, we have an if statement that defines different views for each conditional branch.
This will cause the views to transition in and out because SwiftUI understands that each branch of the if statement represents a different view with a distinct identity.
Alternatively, we could just have a single PawView that changes its layout and color.
When it transitions to a different state, the view will smoothly slide to its next position.
That's because we're modifying a single view with a consistent identity.
Both of these strategies can work, but SwiftUI generally recommends the second approach.
By default, try to preserve identity and provide more fluid transitions.
This also helps preserve your view's lifetime and state, which Luca will talk about in more detail later on.
Now that we understand structural identity, we need to talk about its evil nemesis, AnyView.
To understand the impact of using AnyView, let's look at the effect that it has on the structure of your views.
Earlier we wrote this if statement to switch between the AdoptionDirectory and the DogList.
When SwiftUI looks at this code, it sees the generic type structure on the right.
Now let's look at a different example, one that uses AnyView extensively.
This is a helper function I've written to get a view that represents a dog's breed.
Each conditional branch in the function returns a different kind of view, so I've wrapped them all in AnyViews because Swift requires a single return type for the whole function.
Unfortunately, this also means that SwiftUI can't see the conditional structure of my code.
Instead, it just sees an AnyView as a return type of the function.
This is because AnyView is what's called a "type-erasing wrapper type" -- it hides the type of the view it is wrapping from its generic signature.
But perhaps more importantly, this code is also just really hard to read for us mere humans.
Let's see if we can simplify this code and also make more of its structure visible to SwiftUI.
First, it looks like this branch is conditionally adding a SheepView alongside our BorderCollieView if there are sheep nearby.
We can simplify this by conditionally adding the view inside the HStack rather than conditionally adding the HStack around our views.
With that change, it's now easy to see that we're just returning a single view from each branch, so our local dogView variable isn't necessary.
Instead, we can replace it with return statements inside of each branch.
As we saw earlier, normal SwiftUI View code can use if statements that return different types of views.
But if we just try deleting the return statements and AnyViews from our code, we see some errors and warnings appear.
This is because SwiftUI requires a single return type from our helper function.
So how can we avoid these errors? Recall that the body property of a view is special, because the View protocol implicitly wraps it in a ViewBuilder.
This translates the logic in the property into a single, generic view structure.
Now, Swift does not infer helper functions to be view builders by default, but we can opt into that by manually applying the ViewBuilder attribute ourselves.
And that allows us to remove the return statements and the AnyView wrappers without any warnings or errors.
OK, our code is looking pretty good now! We've gotten rid of all of the AnyViews, making it easier to read than before.
And if we look at the type signature of the result, it now exactly replicates the conditional logic of our function with a tree of conditional content, providing SwiftUI with a much richer perspective of the view and the identities of its components.
But there's one more small improvement we can make.
The top level of our function is just matching against different cases of the dog's breed.
This seems like an excellent use case for a switch statement, which are also supported by view builders.
Now it's even easier to quickly understand all the different cases of our view.
And because switch statements are really just syntactic sugar for conditional statements, our resulting view's type signature on the right remains exactly the same.
Stepping back, we just showed you how AnyViews erase type information from your code, and walked through how to get rid of unnecessary AnyViews by leveraging view builders.
In general, we recommend avoiding AnyViews whenever possible.
Having too many AnyViews will often make code harder to read and understand.
Traditional control flow statements like if/else and switch make it much easier to see the different possible states of a view.
And because AnyView hides static type information from the compiler, it can sometimes prevent helpful diagnostic errors and warnings from being surfaced in your code.
Finally, keep in mind that using AnyView when you don't need to can result in worse performance.
When possible, use generics to preserve static type information rather than passing AnyViews around your code.
And with that, we've finished introducing the basic types of view identity in SwiftUI.
With explicit identity, we can tie the identity of our views to our data, or provide custom identifiers to refer to specific views.
And with structural identity, we've learned how SwiftUI can identify our views just based on their type and position within the view hierarchy.
And now I'll hand things over to Luca to discuss how the identity of your views is related to their lifetime and state.
Luca Bernardi: Thanks, Matt.
Now that we understand how SwiftUI identifies your views, let's explore how identity ties into the lifetime of your views and data.
This will help you better understand how SwiftUI works.
To help illustrate this, I'm also going to bring in a friend.
This is Theseus.
Isn't he adorable, too? Someone will say more adorable, but I digress.
It is very intuitive for us to think that once we name our favorite pet, he'll always be the same adorable cat, even when he is in different states and he moves throughout the day.
When we look at him one moment, he might be sleepy and a moment later, being a proper cat, he's annoyed by my presence -- but he'll always be Theseus.
This is the essence of connecting identity to lifetime.
Identity allows us to define a stable element for different values over time.
In other words, it allows us to introduce continuity over time.
You might be wondering, how does this apply to SwiftUI? So let's bring back a cat-friendly version of the app Matt was working on.
Just like Theseus can be in different states at different moments in time, our views are also in different states throughout their lifetime.
Every single state is a different value for our view.
Identity connects these different values as a single entity -- a view -- over time.
Let's look at some code to clarify this.
Here we have a simple view that displays the intensity of purring.
Spoiler: Theseus is pretty loud.
Through the evaluation of body, SwiftUI will create a new value for this view; in this case, with an intensity value of 25.
Theseus is getting hungry and wants more attention.
body is invoked again with a higher intensity, and the new value for the view is created.
These are two distinct values created from the same view definition.
SwiftUI will keep around a copy of the value to perform a comparison and know if the view has changed.
But after that, the value is destroyed.
What it is important to understand here is that the view value is different from the view identity.
View values are ephemeral and you should not rely on their lifetime.
But what you can control is their identity.
When a view is first created and it appears, SwiftUI assigns it an identity using a combination of the techniques discussed before.
Over time, driven by updates, new values for the view are created.
But from SwiftUI's perspective, these represent the same view.
Once the identity of the view changes or the view is removed, its lifetime ends.
Whenever we talk about the lifetime of a view, we are referring to the duration of the identity associated with that view.
Being able to connect the identity of a view with its lifetime is fundamental to understand how SwiftUI persists your state.
So let's bring State and StateObject into the picture.
When SwiftUI is looking at your view and sees a State or a StateObject, it knows that it needs to persist that piece of data throughout the view's lifetime.
In other words, State and StateObject are the persistent storage associated with your view's identity.
At the beginning of a view's identity, when it's created for the first time, SwiftUI is going to allocate storage in memory for State and StateObject using their initial values.
Here we are focusing on the title state.
Throughout the lifetime of the view, SwiftUI will persist this storage as it gets mutated and the view's body is re-evaluated.
Let's look at a concrete example of how changes in identity affect the persistence of state.
This is an interesting example because we have the same view but in two separate branches.
If you remember from before, because of structural identity, the two views are considered to have different identities.
Matt has discussed how this affects animations, but this also has a profound impact on the persistence of your state.
Let's see this in practice.
When we first evaluate body and we enter the true branch, SwiftUI will allocate persistent storage for the state with its initial value.
Throughout the lifetime of this view, SwiftUI persists the state as it gets mutated by various actions.
But what happens if the value of dayTime changes and we enter the false branch? SwiftUI knows this is a different view with a distinct identity.
It creates new storage for the false view, starting with the state's initial value, and the storage for the true view is deallocated right after.
But what if we go back to the true branch? Well, that's a new view again, so SwiftUI creates new storage, starting again from the state's initial value.
The takeaway here is that whenever the identity changes, the state is replaced.
Let me pause here for a moment and make sure that you understand this important point: the persistence of your state is tied to the lifetime of your views.
This is a very powerful concept because we can clearly separate what is the essence of a view -- its state -- and tie that to its identity.
Everything else can be derived from it.
And your data is so important that SwiftUI has a set of data-driven constructs that use the identity of your data as a form of explicit identity for your views.
The canonical example of this is ForEach.
Let's now take a look at all of the different ways you can initialize a ForEach.
This will help us build a better intuition around this type.
The simplest form of ForEach is one that takes a constant range.
This is a very convenient initializer, especially when you are getting started prototyping some new UI.
SwiftUI is going to use the offset in this range to identify the views produced by the view builder.
By requiring a constant range, we guarantee that the identities are stable for the lifetime of the view.
In fact, it is an error to use this initializer with a dynamic range.
And new this year, you will see a warning when providing a non-constant range.
Let's make things more interesting and bring in a dynamic collection of data.
This initializer takes a collection and a keypath to a property serving as an identifier.
This property must be hashable because SwiftUI is going to use its value to assign an identity to all the views generated from the elements of the collection.
Later, Raj is going to show you some examples of how choosing a stable identity affects the performance and correctness of your app.
This idea of providing a stable identity for your data is so important that the standard library defines the Identifiable protocol to describe this capability.
And SwiftUI takes full advantage of this protocol, allowing you to omit the key path and use the identifier provided by the protocol requirement to define the identity associated with your data and your views.
Something that I really love about Swift is that we can take advantage of its type system to precisely describe the constraints of the problem that we are solving.
So indulge with me in taking a look at the definition of the initializer we are using here.
There are a lot of interesting things in this short definition, so let's try to unpack them.
ForEach needs two main pieces: a collection -- here indicated by the generic argument Data -- and a way to generate a view from each element of the collection.
The shape of this initializer should give you the intuition that ForEach defines a relationship between a collection of data and a collection of views.
But actually, the most interesting part here is that we constrain the elements of the collection to be Identifiable.
Again, the purpose of the Identifiable protocol is to allow for your type to provide a stable notion of identity so that SwiftUI can keep track of your data throughout its lifetime.
In fact, this is very similar to the concepts of identity and lifetime that we discussed earlier.
SwiftUI views that takes an Identifiable type and a view builder are data-driven components.
These views use the identity of the data that you provide to scope the lifetime of the views associated to it.
Choosing a good identifier is your opportunity to control the lifetime of your views and data.
So let's recap what we have discussed in this section.
Views values are ephemeral and you should not rely on their lifetime.
But their identity is not, and is what gives them continuity over time.
You are in control of the identity of your views, and you can use identity to clearly scope the lifetime of state.
And finally, SwiftUI takes full advantage of the Identifiable protocol for data-driven components, so it's important to choose a stable identifier for your data.
And now continuing the tradition, I'm going to hand it over to Raj. Raj? Raj Ramamurthy: Thanks, Luca! So far, we've explained what identity is and how it ties into a view's lifetime.
Next, I'm going to dive into how SwiftUI updates the UI.
The goal is to give you a better mental model for how to structure SwiftUI code.
I'm also going to show a few examples outlining everything at the end.
To kick this discussion of dependencies off, let's look at a view.
Here's a simple view.
It shows a button that rewards a dog with a treat.
Sorry, Luca, but I'm more of a dog person.
Let's focus on the structure of the view.
First, let's look at the top.
There are two properties: one for a dog and another for a treat.
These properties are dependencies of the view.
A dependency is just an input to the view.
When a dependency changes, the view is required to produce a new body.
The body is where you build the hierarchy for the view.
Diving into this view's hierarchy, we have a button with an action.
Actions are what trigger changes to a view's dependencies.
Let's swap the code out for an equivalent diagram.
Here's a diagram of our DogView.
When we tap on the button, it dispatches an action to reward the dog.
Our dog gulps down the treat in a flash.
And that results in a change to the dog -- maybe he wants another.
Because the dependency changed, DogView produces a new body.
To learn more about the general concepts of data flow in SwiftUI, check out "Data essentials in SwiftUI" from WWDC 2020.
Next, let's simplify this diagram a bit.
Focusing in on the view hierarchy, notice how our views form a tree-like structure.
And if we add the dog and treat dependencies back at the top, it still looks like a tree.
However, the DogView is not the only view with dependencies.
In SwiftUI, each view can have its own set of dependencies.
So far, this still looks like a tree.
But note, there can be multiple views dependent on the same state or other data.
For example, one of the descendants might depend on the dog, 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.
In fact, 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.
Take, for example, the dependency at the bottom.
If we examine this dependency, it has two dependent views.
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.
This is another way to look at what Luca discussed earlier.
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.
As Matt said, every view has identity, whether specified explicitly or structurally.
That identity is how SwiftUI routes changes to the right views and efficiently updates the UI.
There are many kinds of dependencies.
We saw a few examples earlier with the treat property and the dog binding, but you can also form dependencies by using the environment, state, or any of the observable object property wrappers.
Next, I'd like to talk about how to improve the use of identity in your views.
This will help SwiftUI better understand your code.
As Luca said, the lifetime of a view is the duration of its identity, and that means the stability of an identifier is crucial.
An identifier that isn't stable can result in a shorter view lifetime.
And having a stable identifier also helps performance, since SwiftUI doesn't need to continually create storage for the view and churn through updating the graph.
As you saw earlier, SwiftUI uses lifetime to manage persisted storage, so stable identifiers are also important for avoiding loss of state.
Let's turn to a code example to explain the importance of identifier stability.
In this example, I have a list of my favorite pets.
We've got an identifier on our pet struct.
But there's actually a bug; every time I get a new pet, everything on screen flashes! Let's stop for a second and look at this code.
Can you spot where the bug is? The bug is here, in our Identifiable conformance.
If you didn't pass the test, don't worry; there are no treats in this section.
The problem is that this identifier isn't stable, so anytime the data changes, we get a new identifier.
What if instead, we used the indices of our pets array? Unfortunately, this has a similar problem.
By using the indices, views are now identified by the position of their respective pet in the collection.
If I decide I have a new first favorite pet, all the other pets will change their identity, which could cause a bad bug.
In this example, the button inserts a new element at index zero, but because the last index is the new one, we get an insertion at the end instead of the start.
This is because, like computed random identifiers, indices are not a stable form of identity.
In this example, we need to use a stable identifier, like one from a database or derived from stable properties of the pet.
Any persistent identifier is a great choice.
Now our animation looks great! But stability isn't the only property we need for good identifiers.
Another property of good identifiers is uniqueness.
Each identifier should map to a single view.
This ensures that animations look great, performance is smooth, and the dependencies of your hierarchy are reflected in the most efficient form.
Let's look at another example.
In this example, I'm working on a view with all of my pet's favorite treats.
Each treat has a name, an emoji, and an expiration date.
I've chosen to identify each treat by its name.
At this point -- I'm sure you can guess -- we have a bug here, too.
What happens when we have more than one of the same kind of treat? I don't know about you, but I like to buy dog biscuits in bulk.
When I add them to the jar, they might not show up! The problem is that the name of a treat is not a unique identifier for it.
Instead, we can use a serial number or other unique ID per treat.
And this ensures all the right data is shown in our jar.
It will also ensure better animations and better performance.
When SwiftUI needs an identifier, it needs your help! Please be careful when using random identifiers, especially in computed properties.
In general, you want all of your identifiers to be stable.
An identifier shouldn't change over time; a new identifier represents a new item with a new lifetime.
And lastly, identifiers need to be unique.
Multiple views can't share an identifier.
SwiftUI relies on these properties to make your app run smoothly and bug-free.
Now that we've talked about explicit identity, I'd like to move on to structural identity.
In this example, I'm working on the treat jar from earlier.
As a responsible pet lover, I only feed my pets the finest, unexpired foods.
To help me tell when treats have gone bad, I've added a new modifier that optionally dims a treat cell when the treat is expired.
I've highlighted the cell that's dimmed.
Let's dive into the modifier.
You can see that in the modifier, I have a date and compare it to the current date to know when to dim the view.
This seems fine at first blush, but there's a subtle problem here.
If the condition changes and our treat becomes expired, we end up with a new identity because there is a branch here.
As Matt discussed, branches are a form of structural identity.
This means we have two copies of the content instead of a single, optionally modified copy.
Note that the branch here is in a modifier.
For clarity, I've put the modifier and its use site on the same slide, but in your project, you might have branches like this across files without even being aware of it! Of course, everything we've discussed here applies to views and view modifiers.
So how can we avoid this? Well, one way is to fold the branches together and move the condition inside the opacity modifier, like so.
By removing this branch, we've correctly described this view as having a single identity.
Furthermore, moving the condition inside the opacity modifier can help performance, because we've tightly scoped the dependent code.
Now when the condition changes, only the opacity needs to change.
The trick to this is that when the condition is true, we have an opacity of 1, which looks like this.
An opacity of 1 has no effect.
We call modifiers like this "inert modifiers," because they don't affect the rendered result.
SwiftUI modifiers are cheap, so there is little inherent cost with this pattern.
Because there is no resulting visual effect, the framework can efficiently prune away the modifier, further reducing its cost.
Branches are great, and they exist in SwiftUI for a reason.
But when used unnecessarily, they can cause poor performance, surprising animations, and, as Luca showed, even loss of state.
When you introduce a branch, pause for a second and consider whether you're representing multiple views or two states of the same view.
As we saw, it often works better to use an inert modifier instead of a branch to identify a single view.
Here are just a few examples of inert modifiers.
I especially love transformEnvironment for conditionally writing to the environment.
Putting everything together, we've shown you today that identity is one of the secrets to amazing performance.
We've discussed explicit and structural identity, and how you can take advantage of each to improve your app.
From identity, we can derive a view's lifetime, which controls its associated storage, transitions, and more.
And we've also explained that SwiftUI uses identity and lifetime to form dependencies, which are represented by a graph that can efficiently update the UI.
Along with demystifying SwiftUI, we've given you some tips and tricks to avoid bugs and improve performance in your apps.
And now that you've learned these tricks, take a tour through your code to see if they can help you.
Thank you, and keep building great apps! ♪
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.