WWDC 2021: Demystify SwiftUI
3 main concepts: Identity, Lifetime, and Dependencies
Identity
Identity answers the question: are two views the same or different?
e.g. answer determines whether transition animation will fade in/out (two separate identities) or slide (same view with new position)
2 types of identity: explicit and implicit (aka structural)
Explicit identity - you have to assign the name
- In UIKit, the "name" of a view is just its pointer - however, SwiftUI uses value types, so no pointers
- SwiftUI:
ForEach(dogs, id: \.dogTagID)
- will usedog.dogTagID
as explicit identity HeaderView(rescueDog).id(headerID)
- can then re-use same id inproxy.scrollTo(headerID)
- Note that you don't need to attach
.id
to every view - just the ones you need to refer to somewhere else
Implicit/structural identity - identity based on type and position in view hierarchy
- Whereas explicit identities are optional, every view has an implicit structural identity
- e.g. in an
if
statement, one of the views is the "True" view and the other is the "False" view - But, needs to statically guarantee that view will always be in the same place - uses type hierarchy to convert
if
statement to_ConditionalContent<AdoptionDirectory, DogList>
- General tip: instead of using
if
to return two views (which have different structural identity), use one view that has its modifiers changed
AnyView
is the evil nemesis of structural identity since it is a type erasing type - it hides the actual type from SwiftUI's type system
- Solution is
@ViewBuilder
- In general, avoid using
AnyView
since it hides type information from compiler, and can also lead to worse performance
Lifetime
Views are value types - so identity refers to the "same" view with different values over time
Even though views themselves are values, any internal @State
get memory allocated for them
Key idea: state lifetime = view lifetime
- e.g. if you have an if branch, then when you go back and forth between the views, the state is entirely replaced
- So, you use identity to control the lifetime of state - use a stable identifier to have the state persist across view refreshes
Use Identifiable
protocol and its id
property to tie data lifetime to views
Dependencies
Dependency graph - graph of dependencies between views and states/values
SwiftUI will refresh a dependency by getting the new .body
Value comparison used to cut down on .body
calls
Identity is backbone of the graph
Kinds of dependencies - @Binding
, @Environment
, @State
, @StateObject
, @ObservedObject
, @EnvironmentObject
Identifier stability - since state lifetime = view lifetime, the identifier is directly related to the view's lifetime
You should try to minimize identifier churn - use IDs stored persistently in a database instead of using a random ID every time
Identifier uniqueness - every ID should map to a unique view
Note that if date < .now { content.opacity(0.3) } else { content }
is two views with different structural identities
Can push down conditional into opacity modifier: content.opacity(date < .now ? 0.3 : 1.0)
Key "trick" is the opacity(1.0)
inert modifier which SwiftUI will just remove before rendering (since inert modifiers by definition have no effect on view)
Others: padding(0)
, transformEnvironment(...) { }