Blog

Latest Post

Static Types in SwiftUI

When SwiftUI was announced, one of the early details that caught our attention was the use of static types to optimize parts of the view graph that can’t change:

It’s heavily inspired by the Elm Architecture and React. The biggest unusual (not sure if novel) thing is the use of the type system to optimize out the static subset of the view graph

— Joe Groff, on Twitter.

To see what this means in practice, let’s consider the following example:

let stack = VStack {
    Text("Hello")
    Rectangle()
        .fill(myState ? Color.red : Color.green)
        .frame(width: 100, height: 100)
}

If we ask Xcode for the type of this view, we get a type that contains some View. This is helpful when reading and writing code, but for our purposes we want the real underlying type, without some. We can get the full type by putting the expression above in a variable and then using dump, or by using a mirror:

print(Mirror(reflecting: stack).subjectType)
// output: VStack<TupleView<(Text, ModifiedContent<_ShapeView<Rectangle, Color>, _FrameLayout>)>>

To quickly see what’s going on, we can visualize the type as a diagram:

2019 11 05 diagram

The type of an expression in Swift is constant: it is computed at compile time. During the lifetime of our SwiftUI program, we can change state variables, observe our model, and render complicated view trees. During each update SwiftUI recomputes the view tree; it then uses the new view tree to update the screen, for instance, inserting or removing views. One thing that doesn’t change is the type of our view tree.

To see why this matters, let’s consider some frameworks similar to SwiftUI.

In our implementation of The Elm Architecture, we do something similar: when the state changes, we re-render our view tree. However, our implementation also includes a diffing step, effectively diffing at tree-level to add and remove views, and update any necessary properties. This approach is common to frameworks like React, languages like Elm, and similar approaches. It can feel wasteful, especially if only a single property has changed.

In SwiftUI, the implementation works differently. In our stack above, SwiftUI knows the type: a vertical stack view with two subviews. During the execution of the program this type will never change — it’s a static property of the code. As such, our program will always render a vertical stack view with a text and a rectangle. When the state changes, some of the views’ properties might change, but the stack view with the two subviews will always persist.

This hard guarantee from the type system means that SwiftUI doesn’t need to do a tree diff. Instead, it only needs to look at the properties of each view, and update those on screen. Theoretically, this still involves walking the entire tree, but walking a tree has a much lower complexity than diffing a tree.

Of course, many applications can’t be written with a fully static view tree: perhaps you want to display items conditionally, or display a list of items (with a variable length), or even display completely different views depending on the state.

Let’s look at these three in detail.

Optional Views

When we want to optionally display a view (based on some state), we can write an if-condition in SwiftUI:

 let stack = VStack {
    if myState {
        Text("Hello")
    }
    Rectangle()
        .fill(myState ? Color.red : Color.green)
        .frame(width: 100, height: 100)
}

The type of stack has now changed:

`VStack<TupleView<(Text?, ModifiedContent<_ShapeView<Rectangle, Color>, _FrameLayout>)>>`

The Text type has also changed, to Text?. At each render pass, SwiftUI needs to decide whether to insert, remove, or update that Text. (Note that it doesn’t need to figure out the position of Text in the stack, it’s statically encoded in the type).

Variable-Length Views

For view trees that have a variable length, SwiftUI uses ForEach. We won’t go into detail on ForEach, but SwiftUI requires you to provide either a constant range, or, if the length is truly dynamic, to use an identifier for each element you’re displaying. When the elements change, ForEach uses the identifier to uniquely identify elements during a diffing step.

Different Views

Sometimes, you want to display completely different views based on the state. Inside a ViewBuilder you can use an if-else with differently typed branches:

 let stack = VStack {
    if myState {
        Text("Hello")
    } else {
        Rectangle()
            .fill(myState ? Color.red : Color.green)
            .frame(width: 100, height: 100)
    }
}

The type of the expression above is:

`VStack<_ConditionalContent<Text, ModifiedContent<_ShapeView<Rectangle, Color>, _FrameLayout>>>`

This means that SwiftUI will either display the left branch of the conditional content, or the right branch. When you can, use an if or if-else statement to convey as much information to the type system as possible. Outside of a view builder, you might need to wrap your views inside another layer to achieve this.

Sometimes, it’s really hard or impossible to know the types up front. For example, if you display a news feed with different items, the types of the views might depend on data coming from the network. For these cases, there’s a last resort: AnyView. For example, while it’s impossible to create an array of some View, you can wrap each news item inside an AnyView, and then create an [AnyView].

An AnyView is a type-erased view, and as such, it provides no information at compile-time about what’s inside. SwiftUI will need to do more work at runtime to verify changes (as mentioned here, and here).

Conclusion

There’s a very deep connection between the type system and the efficiency of view rendering in SwiftUI. Understanding the connection will help you write more efficient code.

If you’ve enjoyed this insight, our weekly Swift Talk video series explores SwiftUI in more depth. In our latest public episode, we explore how to build a shake animation — which is less straightforward than it seems!

To learn with us, become a Subscriber.

Previous Posts

SwiftUI: Setting Environment Values

One of the simplest things to do in SwiftUI is rendering a text with a color. Yet doing this can tell us a lot about how SwiftUI works under the hood.

For example, let’s consider the following view hierarchy:

Text("Hello").foregroundColor(.red)

Simple enough. The type of this value is Text. When we use dump to print this value to the console, we get a Text value that consists of the text storage and a modifier for the color.

Now let’s wrap our Text inside another view struct before we apply the foreground color:

struct Wrapper<V: View>: View {
    let body: V
}

Wrapper(body: Text("Hello")).foregroundColor(.red)

When we use dump again, we can see that the type has (radically) changed:

ModifiedContent<
    Wrapper<Text>,
    _EnvironmentKeyWritingModifier<
         Optional<Color>>>

This change in type is fundamental to how SwiftUI works.

We can write .foregroundColor on any view. In the example above, we used two different implementations of the same method:

  • On Text, the call to foregroundColor simply sets the foreground color of the text
  • On View (to which our wrapper conforms), the call to foregroundColor adds a new layer around the current view. When rendered, this layer changes the environment by adding or modifying the foreground color.

The environment is a list of keys and values, which are passed down from the root view to all of its children. At each step during the rendering, views pass their environment to their children. At any point, views are also free to change the environment; for instance, we can pass a new default foreground color, as shown above.

During rendering, the Text view needs to pick its foreground color. If no color is specified for the Text directly, as in the second example, it gets the color from the environment.

This is a powerful concept, and it means that we can change default values for entire hierarchies. For example, we can write something like this:

VStack {
    Text("Hello")
    HStack {
        Text("Beautiful")
        Text("World")
    }
}.font(.title)

Here, the VStack receives an environment with a custom font, the “Hello” text receives its environment from the VStack, and likewise for the HStack and its labels.

As an interesting aside, it’s possible to inspect the current environment for a view using the following wrapper:

struct DumpingEnvironment<V: View>: View {
    @Environment(\.self) var env
    let content: V
    var body: some View {
        dump(env)
        return content
    }
}

For example, you could dump the environment for a leaf node of the view above:

VStack {
    Text("Hello")
    HStack {
        DumpingEnvironment(content: Text("Beautiful"))
        Text("World")
    }
}.font(.title)

When you run the code, it prints a huge list of keys and values, containing font sizes, colors, safe area insets, presentation mode, undo managers, accessibility properties, display properties, and much more.

If you’ve enjoyed this little insight, our weekly Swift Talk video series explores SwiftUI in more depth.

Learn with us as we experiment, become a Subscriber.

SwiftUI: Shake Animation

Last week, we linked to Robert Böhnke’s Animations Explained, an article from Issue 12. Among other things, it shows a shake animation.

When we first began building animations in SwiftUI, it wasn’t obvious how to achieve certain effects. How, for example, would you create a shake animation similar to Robb’s?

2019 10 01 shake animation

Most animations in SwiftUI are set up implicitly: we change the value of our view’s attributes, and tell SwiftUI that we want this change animated. In this case, however, we don’t really want to change the model; the text field should stay in the same place, it should just shake a little.

As a first attempt, we might try something like:

struct Wrong: View {
    @State var wrongAttempt: Bool = false

       var body: some View {
           VStack {
            Rectangle()
                .fill(Color.pink)
                .frame(width: 200, height: 100)
                .offset(x: wrongAttempt ? -10 : 0)
                .animation(Animation.default.repeatCount(5))
               Spacer()
               Button(action: { self.wrongAttempt.toggle() }, label: { Text("Login") })
           }
       }
}

Here, a boolean set to true triggers an animation, which moves the view by 10 points, and repeats that animation 5 times. As the struct’s name might suggest, there is a problem with the code. After the first animation the button’s x position will be 10 points to the left, and after the next animation it will be correct again.

Instead of animating our model, we can use a modifier. To be precise, we’ll use a GeometryEffect that transforms the view’s x position based on a counter, which we expose as animatableData:

struct Shake: GeometryEffect {
    var amount: CGFloat = 10
    var shakesPerUnit = 3
    var animatableData: CGFloat

    func effectValue(size: CGSize) -> ProjectionTransform {
        ProjectionTransform(CGAffineTransform(translationX:
            amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)),
            y: 0))
    }
}

To use this, we have to modify our code slightly to track the number of attempts. By increasing this number, the animatable data of our Shake modifier will increase by 1. SwiftUI will interpolate between the old and new value, and animate that change.

In the example above, x will animate from 0 to 10, back to -10, back to 10, and stop at 0 (based on shakesPerUnit = 3 and amount = 10).

struct Correct: View {
    @State var attempts: Int = 0

    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.pink)
                .frame(width: 200, height: 100)
                .modifier(Shake(animatableData: CGFloat(attempts)))
            Spacer()
            Button(action: {
                withAnimation(.default) {
                    self.attempts += 1
                }

            }, label: { Text("Login") })
        }
    }
}

We explore animations in more detail in Swift Talk’s SwiftUI Collection. For other interesting animations using GeometryEffect, we recommend The SwiftUI Lab’s article, Advanced SwiftUI Animations — Part 2: GeometryEffect.

To learn with us as we experiment, become a Subscriber.

SwiftUI: Animating Timing Curves

When you animate a property in SwiftUI, the value gets interpolated using an interpolation function, represented by the Animation type. For example, if you animate from x = 200 to x = 400 using a 1-second linear animation, the value gets interpolated linearly. After half a second, the value will be 300, and after .75 seconds it will be 350.

For a more natural animation, an easeInOut timing function is often used. This is an animation that starts slowly, gets progressively faster towards the halfway point, and then slows down again. This is reminiscent of how objects in the real world behave: as you apply a force to an object, the velocity increases, overcoming inertia; when you stop applying the force, the velocity decreases due to friction.

We wanted to know what the timing curves were for all the built-in animations. To plot them, we used something of a hack: a GeometryEffect with a single CGFloat value that gets animated from 0 to 1. Anytime SwiftUI sets that value, we record the value and the corresponding time, and save that into an array. Then we simply create a number of different animations, animate a random view using the custom geometry effect, and plot out all those values.

Here’s the result:

Animation Timing Curves

It’s interesting to see that the default animation is indeed an .easeInOut with a duration of 0.35 seconds, and how the interpolating spring overshoots and then comes back. The non-auto-reversing repeat animation seems strange at first, but it turned out to be useful for us in episode 165, where we showed how to animate along a path.

The code we used to generate this is available as a gist.

For more reading on animations, check out our issue on animations from 2014 — it still contains a lot of relevant advice! In particular, the Animations Explained article by Robert Böhnke goes into detail about animation curves. Another interesting read is the Advanced SwiftUI Animations series by The SwiftUI Lab, which shows many different ways to implement animations in SwiftUI.

Our SwiftUI Collection contains over five hours of video and growing. To support our experiments, and learn with us, subscribe here.

Animating Explicit Changes

In SwiftUI, there are multiple ways to animate changes to your views. In Swift Talk Episode 165, we showed how to animate a shape along a path, by building a custom shape that conforms to the Animatable protocol.

Here’s the relevant animation code:

struct ContentView: View {
    @State var position: CGFloat = 0
    // ...
    var body: some View {
        VStack {
            ZStack {
                // ...
                OnPath(shape: rect, pathShape: Eight(), offset: position)
                    .foregroundColor(.black)
                    .animation(Animation.linear(duration: 5).repeatForever(autoreverses: false))
            }
            .onAppear(perform: {
                self.position = 1
            })
            // ...
        }
    }
}

We wanted to animate the position from 0 to 1 whenever the view appears, and repeat this animation forever, with the shape animating along the path indefinitely. When we run the application, the animation looks like this:

However, there’s a problem with the code. When we rotate the device, it animates in a very strange way:

Currently, our OnPath shape gets animated implicitly whenever anything it depends on changes: the position, its container size, and so forth. To fix this problem, we need to make sure that our animation only applies to changes to the position property.

Instead, we should rewrite our code like this:

struct ContentView: View {
    @State var position: CGFloat = 0
    // ...
    var body: some View {
        VStack {
            ZStack {
                // ...
                OnPath(shape: rect, pathShape: Eight(), offset: position)
                    .foregroundColor(.black)

            }
            .onAppear(perform: {
                withAnimation(Animation.linear(duration: 5).repeatForever(autoreverses: false)) {
                    self.position = 1
                }
            })
            // ...
        }
    }
}

Now, we’re only adding the repeating path animation when the position changes, which only happens when the view appears. Rotating the device no longer influences the infinite animation.

If you’d like to learn alongside us as we experiment with SwiftUI, we have a growing Swift Talk Collection, with a new episode added every week. Episodes 156: A First Look at SwiftUI, 158: The Swift Talk App, and 167: Building a Collection View are free to watch.

To watch the entire SwiftUI collection, subscribe here.