Demystify SwiftUI

Hassan Uriostegui
4 min readJun 30, 2021

Here are some quick notes after watching this fantastic video from WWDC21 Demystify SwiftUI

*Requires Apple Account

View vs UIView

Views in SwiftUI are very different from UIKit:

  • protocol based (vs Inheritance UIKit)
  • value type (vs ref type UIKit)
  • views as value types are ephemeral (vs mutating models in UIKit)
  • as such identity may be implicit/ explicit (vs pointer identity UIKit)
  • views/ view-actions are connected through a dependency graph (rather than hierarchical as in UIKit)

Overall, views are analyzed as a dependency graph of identifiable and generic elements that are rendered only when changes are detected.

View Identity

Identity is a fundamental property in order to determine changes over time. In SwiftUI, given that views are ephemeral value types, the concept of identity is not tied with any particular instance as it happens with the so called pointer identity in UIView-UIKit. Instead in SwiftUI, Views might have

  • An implicit identity (or structural identity that will be inferred by the structural position and the class
  • Or an explicit identity that may be set through the identifiable protocol or the .id() method.

Structural Identity and Conditionals

When using conditional operators in SwiftUI views it’s important to keep in mind that unless a explicit identity is set, each logical path will be treated as a unique structural identity.

In the example below despite returning the same view SomeViewA, it will be evaluated each holding a distinct identity.

@ViewBuilder
func someHelper()->some View{
if a == true {
SomeViewA()
}else{
SomeViewA()
}}

That’s the reason why, when animating specific properties it’s better to rely on a ternary operator for short statements or View computed vars for longer evaluations. For instance, below to relative implementations, the first won’t produce a smooth transition given the structural identity change associated with the if else

This implementation will not produce a smooth opacity transition:

@ViewBuilder
func someHelper()->some View{
if a == true {
SomeViewA().opacity(1.0)
}else{
SomeViewA().opacity(0.0)
}}

This implementation will produce a smooth opacity transition:

@ViewBuilder
func someHelper()->some View{
SomeViewA().opacity(a == true ? 1.0 : 0.0)
}

Dependency Graph

Views and actions mutating the @State or @ObserverdObjects will cause the dependency graph to be recomputed. As views are value based, SwiftUI can afford to compute ahead a new version to determine if any changes are present, if so, then a new view-body is evaluated potentially cascading until the graph is fully evaluated for changes (ie. stops on the links where a view skips producing a new body).

Stable Ids

Unless intentional, we typically require a stable form of identification for views. A common mistake is to unintentionally create a new Id during the view instantiation, as follows:

struct ContentView: View, Identifiable{var id = UUID() //unstable ID}

Instead an Id may be obtained from a database or contextual reference.

struct ContentView: View, Codable, Identifiable{var dbId:String
var id:String{ dbId }
}

LifeCycle

Views are evaluated following a dependency graph approach, this means a view will be instantiated twice as part of the dependency graph diffing mechanism. The propagation through the graph will be stop once a no further changes are detected.

As such, when creating/ evaluating views, parameters play a very important role to determine if the current view-body need to be reevaluated. Once a change is determined, a new body is created potentially propagating changes to children subviews.

@State properties will be persisted as long as the implicit (structural) or explicit identity for that given view stays alive.

State Properties

@State properties are tied to the view and last only during the duration of that particular view identity lifecycle (not the view value per se, meaning the values will mutate in the view and the @State vars will be the same

AnyView

The AnyView struct is a type-eraser that allows to obfuscate the inner generics within a given view hierarchy. It may be helpful in situations where no other options (that would keep the generic types intact) are available.

For instance when creating functions that may return different view types, then its better to rely in the ViewBuilder to support up to 10.

@ViewBuilder
func someHelper()->some View{
if a == true {
SomeViewA()
}else{
SomeViewB()
}}

Switch Statements

When possible consider using switch states as syntactical sugar.

@ViewBuilder
func someHelper()->some View{
switch value{
case .a:
SomeViewA()case .b:SomeViewB()default:EmptyView()}}

ForEach

There are multiple ways to init the ForEach operator

  • ForEeach(1...5) With a static range
  • ForEach(identifiableObjects) With an array of conformingidentifiable objects
  • ForEach(array, id:\.someKeyPath) With a specific identity keypath source for each element.

It’s important to keep in mind that ForEach will implicitly identify each element based on either the identifiable.id accessor or through the specific keypath. This may bring some insights on why dynamic ranges are not supported.

--

--

Hassan Uriostegui

Sr. Software Architect | EB1A USC | 10+yrs Innovating in Silicon Valley | Passionate about AI, AR, SwiftUI, Combine, React, JS, Python, CGI, VFX and more!