0% found this document useful (0 votes)
116 views252 pages

Pro SwiftUI - Paul Hudson

This document serves as a preface and introduction to a book on SwiftUI, emphasizing the importance of understanding the framework's underlying mechanisms for effective app development. It outlines the layout process in SwiftUI, focusing on the relationship between parent and child views, and introduces key concepts such as view modifiers and the six values that determine view sizes. The author encourages readers to engage with sample code and offers bonus content for members, while also sharing personal reflections and updates related to the book.

Uploaded by

fedorstm1511
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
116 views252 pages

Pro SwiftUI - Paul Hudson

This document serves as a preface and introduction to a book on SwiftUI, emphasizing the importance of understanding the framework's underlying mechanisms for effective app development. It outlines the layout process in SwiftUI, focusing on the relationship between parent and child views, and introduces key concepts such as view modifiers and the six values that determine view sizes. The author encourages readers to engage with sample code and offers bonus content for members, while also sharing personal reflections and updates related to the book.

Uploaded by

fedorstm1511
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 252

Contents

Preface

Layout and Identity

Animations and Transitions

Environment and Preferences

Custom Layouts

Drawing and Effects

Performance
OceanofPDF.com
Preface
OceanofPDF.com
Welcome
SwiftUI makes it astonishingly easy to create beautiful, fast, native apps for
all of Apple’s platforms, and I don’t think I’ll ever grow tired of watching
folks be amazed at how fast we can put apps together.

However, once you’re past the basics it’s common to find some parts of
SwiftUI confusing – you try some code out and wonder why it doesn’t
behave the way you expect, and it’s easy to fall into the trap of
experimenting with various modifiers and workarounds until you get
exactly the result you want.

Although I can’t solve all your problems with SwiftUI, the core goal of this
book is to help you build a better understanding of what SwiftUI is doing
behind the scenes – to really understand what it’s doing and why, and in
understanding that learn to write better code. So, rather than just show you a
huge range of different APIs that are on offer, we will instead be focusing
on the real core fundamentals of SwiftUI so you can see exactly what
makes it tick.

We’ll accomplish this partly by looking at the small amounts of the source
code for SwiftUI that gets exposed by Swift’s interface file, but also by
writing a whole bunch of code ourselves so you can see exactly how
SwiftUI responds to various scenarios.

If you work your way through the whole book, including trying all the
sample code, you should come away with a much deeper understanding of
what SwiftUI is doing when it runs our code.

Of course, a book full of behind the scenes explanations wouldn’t be much


fun, so I’ve tried to include a variety of more graphical techniques too – I
feel confident everyone will learn something while following along!

Working with SwiftUI’s public interface


At various points in this book I will ask you to run a specific command. It’s
quite long, so I’ve made a GitHub gist from it at this link:
https://bit.ly/swiftinterface

In case that link doesn’t work, here’s the command in full:

xed
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulat
or.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Fra
meworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/x86_64-
apple-ios-simulator.swiftinterface

If that command works immediately, great – you should see Xcode open
with an editing window full of code! But don’t worry if you get an error,
because I’ll address that in a moment.

The command above asks Xcode to open SwiftUI’s public interface file for
reading. That’s not the same as the generated interface Xcode generates for
us using Open Quickly or Jump to Definition, but is instead the public API
interface file SwiftUI actually ships with.

These two files have many things in common, but the public API interface
file provides a great deal more detail, and, critically for us, also includes
small amounts of Swift code that implement various SwiftUI features. This
is required for performance reasons, because some parts of SwiftUI are so
trivial or so commonplace that Swift literally copies Apple’s own SwiftUI
code into our files at build time as part of a process called inlining.

The reason I’m saying this up front is two-fold: so I don’t need to keep
explaining that this public interface file provides useful snippets of Apple’s
own source code that show us how a feature is implemented, but also
because you might get an error when running it.

So, if you see an error like this one: “xcode-select: error: tool 'xed' requires
Xcode, but active developer directory
'/Library/Developer/CommandLineTools' is a command line tools instance”,
it means your Mac has a small misconfiguration that is easily corrected.
Run this command to fix the problem: sudo xcode-select -s
/Applications/Xcode.app. That will prompt you for your password, but
once that’s done you’ll be able to run the original command without
problem.

Anyway, you’re welcome to close the file for now; I’ll let you know when I
want to dig into it.

Frequent Flyer Club


You can buy Swift tutorials from anywhere, but I'm pleased, proud, and
very grateful that you chose mine. I want to say thank you, and the best way
I have of doing that is by giving you bonus content above and beyond what
you paid for – you deserve it!

Every book contains a word that unlocks bonus content for Frequent Flyer
Club members. The word for this book is HARMONY. Enter that word,
along with words from any other Hacking with Swift books, here:
https://www.hackingwithswift.com/frequent-flyer

Updates
When you buy your books from Hacking with Swift, you get Swift updates
for free. You can read the full version of my update policy at
https://www.hackingwithswift.com/update-policy, but the abridged version
is this: whenever I release to the public an updated book or video to reflect
these changes, all existing buyers will get that update free.

This edition has the version 2022-10-25.

Dedication
This book is dedicated to my father, who died earlier this year. After he
passed I looked through photos I had taken of him, and realized although I
had a great many of him with my children I had very few of him with me.
So, if you’re reading this and are lucky enough to have your parents or even
grandparents around, go and make some memories with them!

OceanofPDF.com
Chapter 1
Layout and Identity
OceanofPDF.com
Parents and children
At the core of SwiftUI is its three-step layout process:

1. A parent view proposes a size for its child.


2. Based on that information, the child then chooses its own size and the
parent view must respect that choice.
3. The parent view then positions the child in its coordinate space.

It sounds so trivial, and you’re probably wondering why I’m starting out by
mentioning something that is so straightforward, but the simple truth is that
this simple process unlocks a huge amount of power and the more you
really understand it the more you’ll be able to get SwiftUI doing exactly
what you want.

The key to the power is answering a simple question: what is the “parent
view” in that process? When you’re learning SwiftUI, the answer seems
obvious. For example:

VStack {
Text("Hello, world!")
.frame(width: 300, height: 100)

Image(systemName: "person")
}

If I asked a learner what the parent of the text view was, they would
probably answer “the VStack.” And honestly that’s a perfectly fine answer,
and I wouldn’t correct a beginner who said it – it feels natural, and it fits the
hierarchy we can see when the code runs. However, it’s also wrong, and
once you have sufficient experience with SwiftUI it’s important you
understand why the answer is wrong, and more importantly what is right.

Let’s start simple: when we use any modifier in SwiftUI, we are most of the
time creating a new view that wraps the original view to add some extra
behavior or styling. For example, this is just one view:
Text("Hello, world!")

Whereas this is two views:

Text("Hello, world!")
.frame(width: 300, height: 100)

This makes sense if you break it down and keep the three-step layout
process in mind: the text view can’t position itself, because that’s the job of
the parent. So, the only way for “Hello, world!” to be aligned center in a
300x100 container is for the parent – the frame – to be that 300x100
container. This is also why we can stack many modifiers to create more
complex effects: we aren’t modifying the original view again and again, but
instead modifying a new view that modifies the original.

Once you understand this process of creating new views using modifiers, so
much of the rest of SwiftUI makes sense. This is why I repeatedly
encourage folks to print out the underlying types of their views, like this:

Text("Hello, world!")
.frame(width: 300, height: 100)
.onTapGesture {
print(type(of: self.body))
}

When you do that, you’ll see the ModifiedContent type appear a lot – not
exactly once for every modifier, because again not all modifiers create new
views. ModifiedContent is itself a struct that conforms to the View
protocol, and I’d like you to look it up using Open Quickly.
If you aren’t familiar with it, Open Quickly is an Xcode feature that lets you
type to search through your code and also Apple’s own APIs; activate it
using Shift+Cmd+O, then type ModifiedContent and press return. This will
open Xcode’s generated interface file for SwiftUI, and you should be able
to find this in there:

@frozen public struct ModifiedContent<Content, Modifier>

A little further down you’ll also see its View conformance:

extension ModifiedContent : View where Content : View,


Modifier : ViewModifier {

These things aren’t magic, and they aren’t secret – you can use
ModifiedContent directly if you want, because it’s public API. For
example, back in ContentView.swift we could create a custom a modifier
like this one:

struct CustomFont: ViewModifier {


func body(content: Content) -> some View {
content.font(.largeTitle)
}
}

We could apply that to some text using ModifiedContent, like so:

struct ContentView: View {


var body: some View {
ModifiedContent(content: Text("Hello"), modifier:
CustomFont())
}
}

That’s obviously a lot more wordy than the normal SwiftUI code we’d
write, but what I want you to understand is that the end result is identical to
what we’d get by using modifier() like this:

Text("Hello")
.modifier(CustomFont())
.onTapGesture {
print(type(of: self.body))
}

This is what SwiftUI’s result builder does for us: it repeatedly transforms
our modifiers into ModifiedContent views, nesting them again and again
to get exactly the right result. This is all done at compile time rather than
run time: the actual underlying type of these two views are identical, rather
than just the finished, rendering layouts.

So, when we write code like this:

Text("Hello, world!")
.frame(width: 300, height: 100)

That creates the original text view, plus a new ModifiedContent around it
that happens to contain the fixed frame instructions. The text still has its
original frame – the original size it wants to work with – but now we’ve
added a second frame around it.

You can see the original frame is alive and kicking by passing in a custom
alignment for the outer frame:

Text("Hello, world!")
.frame(width: 300, height: 100, alignment:
.bottomTrailing)
If you think about it, the only way the text can be aligned to the bottom
trailing edge is if it knows its original frame. So, our text view has its own
default frame that exactly matches the natural size for its text, and no
amount of futzing from us can ever force that text to extend its bounds
beyond the natural width and height of its lines.

However, by applying the frame() modifier we’re creating a new


ModifiedContent view around the text that takes up more space – it’s not
strictly a “frame” view in its own right, but I think it’s helpful to talk about
it like that.

OceanofPDF.com
Fixing view sizes
Let’s look at this simple code again:

Text("Hello, world!")
.frame(width: 300, height: 100)

Like I said, despite attaching a frame() modifier, no amount of futzing from


us can ever force that text to extend its bounds beyond the natural width and
height of its lines – that’s just not how SwiftUI works.

Of course, the real question here is this: what is the natural size for the text?
Well, the answer is that text likes to live on one long line, and that’s exactly
what it will do unless you ask for something else. For example, we might
say that our frame had a width of only 30 rather than 300, like this:

Text("Hello, world!")
.frame(width: 30, height: 100)

Now there isn’t enough space for the text, it’s forced to wrap across several
lines to fit into the tiny box we’ve allocated to it.
On the surface this sounds like it breaks the second rule of SwiftUI’s layout
system: “the child chooses its own size and the parent view must respect
that choice.” In this case the child is the text view and the “frame” view is
its parent, so how come the text is being forced to accept the size of its
parent, the frame?

What’s really happening here is that all views use six values to decide how
much space they want to use for layouts, and understanding how they
interact is key to getting the most out of SwiftUI’s layout system.

The six are:

Minimum width and minimum height, which store the least space this
view accepts. Anything lower than these values will be ignored,
causing the view to “leak” out of the space that was proposed to it.
Maximum width and maximum height, which store the most space this
view accepts. Anything greater than these values will be ignored,
meaning that the parent must position the view somehow inside the
remaining space.
Ideal width and ideal height, which store the preferred space that this
view wants. It’s okay to provide values outside these, as long as they
still lie in the range of minimum through maximum. (If you’re coming
from a UIKit background, think of this as being like the intrinsic
content size of your view.)

It’s that last one that stores the natural size for our text: the text has ideal
width and height suitable to store its characters on one single line, but it has
no minimum size – it doesn’t care if we try to squeeze the text into a limited
width, because it will automatically wrap its letters around to multiple lines.

This is where the fixedSize() modifier comes in, which has the job of
promoting a view’s ideal size to be its minimum and maximum size. It’s
used like this:

Text("Hello, world!")
.fixedSize()
.frame(width: 30, height: 100)
When that code runs, our text will appear at its original width, despite us
trying to override it. Take a moment to think about it: what do you think is
actually happening internally with that code?

Tip: I know it’s really tempting to skip ahead and read my discussion for
this question, but I promise you’ll learn more if you just pause for a
moment and think of your own answer to the question: how will SwiftUI
interpret that code, and in what order? Remember, the fixedSize() modifier
create a parent view around the text, then in turn the frame() modifier
creates parent view around the fixedSize().

Still here?

Okay, what we end up with is this:

We have three views in total: two ModifiedContent views and a Text


view.
In terms of parent-child relationships, our frame is the overall parent,
and it has a fixed size view for its child, which in turn has a text view
for its child.
When we create a 30x100 frame, it will offer that full space to
fixedSize() child.
The view created through fixedSize() proposes that same size to its
Text view.
The text has no idea it’s going to be placed in a 30x100 frame, so it
says, “well, my ideal size is 95x20, but I’m happy to take up less space
if needed.”
The fixedSize() modifier then uses that same information, except now
it turns the ideal size into a fixed size – it effectively returns the
equivalent of self.frame(width: text.idealWidth, height:
text.idealHeight).
So now the frame gets told it has to position a child much bigger than
the size it proposed, and does so – it doesn’t have a choice.

So, fixedSize() is how we promote ideal size up to be fixed size. You can
use fixedSize() with no parameters to get both axes fixed at the same time,
or provide Boolean parameters to fix one specific axis if you prefer.
In the case of text views, remember that fixing its horizontal size will
default to it going over one line no matter how long its text is. If that’s what
you want, great! If not, you might find that fixing only its vertical axis is
more useful, because it allows the text to be squashed horizontally while
still growing as tall as needed to handle its lines wrapping.

But what will other view types do? Consider code like this:

Image("singapore")
.frame(width: 300, height: 100)

On its surface that appears to request a 300x100 image, but any SwiftUI
veteran will know it doesn’t work like that – the image will be its original
size, happily overflow the 300x100 frame we allocated for it. If you’re
using Xcode’s preview canvas in selection mode you’ll be able to see the
thin outline of the frame right there.
You can see it more clearly if you use the clipped() modifier to see what’s
really happening:

Image("singapore")
.frame(width: 300, height: 100)
.clipped()

Perhaps now you have a better idea of what’s happening here: image views
get their ideal width and height directly from the image data you load into
them, and just like text views no amount of futzing from us can override
that.

“But Paul,” I hear you say, “surely we can override the ideal size by making
the image resizable?” Nope! Again, no amount of futzing from us can
override the ideal size of an image – you can see it for yourself with code
like this:

Image("singapore")
.resizable()
.fixedSize()
.frame(width: 300, height: 100)

That makes the image resizable, but then promotes the ideal size into a
fixed size – lo and behold, the image is back to its original size again.

Earlier I said, “when we use any modifier in SwiftUI, we are most of the
time creating a new view that wraps the original view to add some extra
behavior or styling.”

Well, resizable() is one of the modifiers that doesn’t create a new view: it
sends back an image with the resizing behavior in place, but that isn’t
wrapped in some kind of “resizable” modifier – all we did was tell it to
have a flexible width and height, but the underlying ideal size is still there.

The key here is to remember that whatever frame you try to apply to the
image will automatically inherit values from the image that you don’t
specifically override.
For example, a common problem SwiftUI learners hit is when they use a
very wide image alongside some text, like this:

VStack(alignment: .leading) {
Image("wide-image")
Text("Hello, World! This is a layout test.")
}

If that image is very large compared to the device you’re using, e.g.
2000x1000, then it will stretch the width of the VStack beyond the edges of
the screen, which will cause the text to be placed off screen too. This is
rarely what you want – how would you go about fixing it?
To fix this without squashing the image, the simplest thing to do is wrap it
in a frame with a completely flexible width, like this:
VStack(alignment: .leading) {
Image("wide-image")
.frame(minWidth: 0, maxWidth: .infinity)
Text("Hello, World! This is a layout test.")
}

Critically, if you remove the minWidth parameter there, the frame will get
its minimum width from the image, which again wants to show its entire
picture at its natural size. And even with both minimum and maximum
width provided, adding fixedSize() afterwards shows that the ideal width
and height of the image is still there!
Another common problem beginners face is making two views the same
width or height depending on their content. For example, they might have a
layout like this:
HStack {
Text("Forecast")
.padding()
.background(.yellow)
Text("The rain in Spain falls mainly on the Spaniards")
.padding()
.background(.cyan)
}

In that layout, the HStack proposes the available space to its children, then
splits up the space based on what they sent back. In practice, the larger text
view will need significantly more space than the smaller one, and when
space is restricted the text will wrap – how can we make them the same
size?

Well, the background() modifiers will create frames using whatever size
they receive from the text they wrap, but if we add a custom frame to them
then the backgrounds become free to expand to fill more space:

HStack {
Text("Forecast")
.padding()
.frame(maxHeight: .infinity)
.background(.yellow)
Text("The rain in Spain falls mainly on the Spaniards")
.padding()
.frame(maxHeight: .infinity)
.background(.cyan)
}

Now, remember: even though we’ve told the background it has a flexible
maximum height, we haven’t overridden its ideal height – that still comes
through from the text it contains. So, each piece of text has its own ideal
height that exactly fits its content, the background inherits that ideal height,
and the HStack around it calculates its own ideal height to be the maximum
of the ideal heights of the two views it contains. As the text views have
infinite maximum heights and will therefore expand to fill all the available
height, the HStack will also expand to fill all the available height.

As a result, if we make the HStack use fixedSize(), we can make our two
text views have the same height with very little code:

HStack {
Text("Forecast")
.padding()
.frame(maxHeight: .infinity)
.background(.yellow)
Text("The rain in Spain falls mainly on the Spaniards")
.padding()
.frame(maxHeight: .infinity)
.background(.cyan)
}
.fixedSize(horizontal: false, vertical: true)

Telling the HStack to fix its size is different from telling each of the text
views to fix their size: we want them to resize upwards to some upper limit,
which in this case is the ideal size of their container.
In their documentation, Apple describes fixedSize() as “the creation of a
counter proposal to the view size proposed to a view by its parent,” which is
quite apt once you understand what’s happening internally.

OceanofPDF.com
Layout neutrality
Not all views have a meaningful ideal size, and in fact some views have
very little sizing preference at all – they will happily adapt their own size
based on the way we use them alongside other views. This is called being
layout neutral, and a view can be layout neutral for any combination of its
six dimensions.

In its purest form, layout neutrality it looks like this:

struct ContentView: View {


var body: some View {
Color.red
}
}

That will fill the whole space with red, because the color will occupy
whatever space is available. On the other hand, we could use the color as a
background:

Text("Hello, World!")
.background(.red)

Because the color doesn’t actually care how much space it takes up, it will
simply read the ideal and maximum sizes from its child, the text, and use
that for itself. It doesn’t read the minimum size because like I said earlier
the text view is itself layout neutral for its minimum width and height – the
text doesn’t mind being squeezed smaller, so the background color doesn’t
mind either.

If we wanted to describe this behavior accurately, we’d say that the


background color has an ideal width, ideal height, maximum width, and
maximum height, but is layout neutral for its minimum width and minimum
height. In practice this means it will fit snugly around the text it wraps
rather than expanding to fill all the available space, but it’s also happy to be
squeezed downwards if needed.
Now take a look at this code:

struct ContentView: View {


var body: some View {
Text("Hello, World!")
.frame(idealWidth: 300, idealHeight: 200)
.background(.red)
}
}

What do you think that might do, and why? Remember to work backwards
– the layout starts with background(.red) as the parent, and works inwards
from there.

Take a moment to pause and think about it before continuing.

If you break it down, the flow works like this:

The background has the whole screen to work with, and Color.red is
completely layout neutral so it’s happy to occupy whatever is
available. If this were the entirety of our layout, the color would
expand to fill the screen.
The background passes on the size proposal it received (the whole
screen) to its child, frame(), which is layout neutral for minimum
width, minimum height, maximum width, and maximum height.
The frame proposes the whole screen to its child, the text, which is
layout neutral for minimum width and minimum height, but cares very
much about its ideal width, ideal height, maximum width, and
maximum height.
The text sends back to the frame the four values it cares about, but
because the frame has provided its own ideal width and height those
two are effectively ignored – the frame will use its own ideal width
and height to override whatever the text asked for. However, because
the frame is layout neutral for its maximum width and height, it will
inherit those from the text.
The frame then sends its final size up to its parent view, the
background: it has the 300x200 ideal size we set, but a maximum size
of whatever the text says it needs, e.g. 95x20.
That 95x20 space then gets filled with the red background.

So, it really is important to think about all six sizing values when working
with layout – they combine together in really interesting ways that may not
necessarily make sense at first, but if you break it all down into a sort of
blow-by-blow conversation then hopefully the exact behavior will become
clear.

Helpfully, all six of these sizing values are optional, which means you can
switch between layout neutrality and a fixed value by using either a number
or nil. This is most commonly done using a ternary conditional operator,
like this:

struct ContentView: View {


@State private var usesFixedSize = false

var body: some View {


VStack {
Text("Hello, World!")
.frame(width: usesFixedSize ? 300 : nil)
.background(.red)

Toggle("Fixed sizes", isOn:


$usesFixedSize.animation())
}
}
}

What that code does at runtime depends on the value of usesFixedSize:

When it’s true, the frame will propose 300 points of width to the text
When it’s false, the frame will propose to the text whatever size it
receives from the VStack – it effectively does nothing at all.

So, if nil forces a view to be layout neutral for one particular dimension,
what happens if we have a view that’s layout neutral for every dimension?
We can certainly do this with the frame() modifier, but at some point every
view has some kind of size data, even if that’s just a nominal ideal size in
order to keep our layouts from exploding.
To see an example of what I mean, try code like this:

ScrollView {
Color.red
}

What size can the scroll view propose to Color.red?

Usually Color.red is happy to fill all the available space, but it wouldn’t
make any sense here because it would lead to an infinitely sized scroll view.
In this situation, the red color will get a nominal 10-point height – enough
that we can see it’s being placed, but it won’t expand beyond that.

You can get a better idea of what’s happening if you try attaching a frame to
the color, like this:

ScrollView {
Color.red
.frame(minWidth: nil, idealWidth: nil, maxWidth: nil,
minHeight: nil, idealHeight: nil, maxHeight: 400)
}
}

We’re now explicitly giving the color a maximum height to work with, but
it won’t matter – it will still stay 10 points high, because our frame is layout
neutral for its ideal height. This isn’t just that the color remains small while
the frame grows empty around it, which you can see if you try coloring the
frame blue:
ScrollView {
Color.red
.frame(minWidth: nil, idealWidth: nil, maxWidth: nil,
minHeight: nil, idealHeight: nil, maxHeight: 400)
.background(.blue)
}
}

You won’t see any background there, because the frame still has an ideal
height matching the color it contains. In order to get the color to expand, we
need to override its ideal height in its frame parent, like this:

ScrollView {
Color.red
.frame(minWidth: nil, idealWidth: nil, maxWidth: nil,
minHeight: nil, idealHeight: 400, maxHeight: 400)
}
}

Now the red color will expand to be 400 points high – internally the color
has an infinite maximum height but gets capped to the 400 points it is
offered by the frame, but now the frame won’t adopt the 10-point ideal
height of the color and will instead use 400 points so that the color can
grow freely.

OceanofPDF.com
Multiple frames
Many SwiftUI modifiers can be stacked to create interesting effects, such as
perhaps adding multiple backgrounds to some text:

Text("Hello, World!")
.frame(width: 200, height: 200)
.background(.blue)
.frame(width: 300, height: 300)
.background(.red)
.foregroundColor(.white)

However, it’s possible and indeed common to apply a frame() modifier


twice back to back – with no other modifiers in between. This is because
SwiftUI separates the concepts of fixed frames and flexible frames: a single
view can have a fixed width or height, or it can have flexible dimensions
provided, but it can’t have both.
Of course, often we do want both. For example, if you were designing a
macOS app you might say want a fixed width for part of your UI, but have
a minimum height so that users can’t make the window really tiny:

Text("Hello, World!")
.frame(width: 250)
.frame(minHeight: 400)

I don’t want to sound like a broken record, but remember the golden rule
here: no amount of futzing from us can ever force that text to extend its
bounds beyond the natural width and height of its lines. We aren’t making
the text flexible at all, because it isn’t possible. Instead, we’re wrapping it in
a new view that has a fixed width of 250 points, then wrapping that in
another new view that has a flexible height of at least 400 points.

The best way to check your understanding is correct is to look at code like
this:

Text("Hello, World!")
.frame(width: 250)
.frame(minWidth: 400)

What do you think that will do when run? It sounds like we’re giving
SwiftUI completely contradictory instructions, but we really aren’t, and
hopefully the answer becomes clear if you try to think like SwiftUI does.

Again, pause for a moment and think it through to decide for yourself
before I walk you through what actually happens.

Still here? Okay:

Our text has an ideal width and height matching its contents.
We place that inside a frame that is 250 points wide.
We place that frame inside another frame that is at least 400 points.

So, there is no contradiction at all, and if you try adding background colors
after the text and each frame you’ll see exactly what’s happening:
Text("Hello, World!")
.background(.blue)
.frame(width: 250)
.background(.red)
.frame(minWidth: 400)
.background(.yellow)

OceanofPDF.com
Inside TupleView
You’ve seen how using modifiers with a single view gets transformed by
@ViewBuilder into nested ModifiedContent views, but the other side of
this coin is how Swift handles multiple views. Try this:

VStack {
Text("Hello")
Text("World")
}
.onTapGesture {
print(type(of: self.body))
}

When that runs, you’ll see the type is


ModifiedContent<VStack<TupleView<(Text, Text)>>,
AddGestureModifier<_EndedGesture<TapGesture>>>, but really the
important part in all that is TupleView<(Text, Text)> because that’s how
SwiftUI encodes multiple views: a special view type that accepts other
views inside it.

TupleView isn’t underscored, which means it’s public API, and I encourage
folks to look it up using Open Quickly because it explains one of the key
restrictions in SwiftUI. If you look for where TupleView is used, sooner or
later you’ll find this:

public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7,
C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _
c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) ->
TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0
: View, C1 : View, C2 : View, C3 : View, C4 : View, C5 :
View, C6 : View, C7 : View, C8 : View, C9 : View

That’s a generic result builder method that accepts 10 views, and you’ll also
find alternatives that accept 9 views, 8 views, and so on – but, critically,
you won’t find one that accepts 11 views, which is why SwiftUI isn’t able
to statically represent more than 10 views in its type system. (If you look
for other examples of buildBlock you’ll see there are some examples that
are significantly more complex – see TableColumnBuilder for a real eye
opener!)

There’s no software restriction making 10 a hard limit, instead it’s a


pragmatic choice by the SwiftUI team: they need to draw a limit
somewhere, and 10 is a reasonable amount. If you wanted, you could use
Open Quickly to look for buildBlock in SwiftUI’s generated interface, then
copy it into your own code and add to it to allow 11 views, like this:

extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6,
C7, C8, C9, C10>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _
c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9, _
c10: C10) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8,
C9, C10)> where C0 : View, C1 : View, C2 : View, C3 : View,
C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 :
View, C10 : View {
TupleView((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9,
c10))
}
}

That uses 11 different views, so now you’ll find you can include 11 children
inside any container. Heck, you could even just create a TupleView directly
whenever you needed using as many views as you want, like this:
TupleView((
Text("1"),
Text("2"),
Text("3"),
Text("4"),
Text("5"),
Text("6"),
Text("7"),
Text("8"),
Text("9"),
Text("10"),
Text("11"),
Text("12"),
Text("13"),
Text("14"),
Text("15")
))

Tip: Note the double opening and closing parentheses – the first is because
we’re calling the TupleView initializer, and the second is to mark a tuple of
our views, which is why we need to use commas to separate each view.

This isn’t magic, or a hack – it’s exactly what SwiftUI does internally, albeit
now extended to go one higher than the SwiftUI team chose. In fact, you
can see all this for yourself because all these TupleView instances are
directly inlined into our code at build time for maximum efficiency using
SwiftUI’s public interface file.

I mentioned this in the introduction to the book, but I’d like you to open it
now. Run this command:

xed
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulat
or.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Fra
meworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/x86_64-
apple-ios-simulator.swiftinterface

(If you hit the error about CommandLineTools, see the book introduction
for how to fix it!)
If you look for extension SwiftUI.ViewBuilder { in the file that gets
opened, you’ll see all the TupleView creation, like this:

extension SwiftUI.ViewBuilder {
@_alwaysEmitIntoClient public static func buildBlock<C0,
C1>(_ c0: C0, _ c1: C1) -> SwiftUI.TupleView<(C0, C1)> where
C0 : SwiftUI.View, C1 : SwiftUI.View {
return .init((c0, c1))
}
}

That’s literally doing the same thing we did, except it can use the shorthand
.init(()) rather than TupleView(()) because Swift can see the return type of
the method.

SwiftUI doesn’t care how your TupleView instances are structured,


meaning that you can place a TupleView inside a TupleView inside another
TupleView and it will still get flattened down to a single collection of
views. This means we can use Swift’s partial block results builders to allow
any number of view children – try adding this:

extension ViewBuilder {
static func buildPartialBlock<Content>(first content:
Content) -> Content where Content: View {
content
}

static func buildPartialBlock<C0, C1>(accumulated: C0,


next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
TupleView((accumulated, next))
}
}

Just having those two partial block builders will cause SwiftUI to nest many
TupleView instances, but that’s okay – it really doesn’t care how the views
are structured, because internally it uses runtime reflection that inspects the
type metadata to figure out exactly how many children existed. If you
desperately wanted to get the exact same TupleView layout as SwiftUI has
by default, while also permanently removing the 10-view limit, you would
need to add a bunch more buildPartialBlock() methods to handle the full
set of C0 through C9 views.

OceanofPDF.com
Understanding identity
Every view in SwiftUI must be uniquely identifiable, by which I mean that
SwiftUI needs to know exactly which view is located where at all times.
This means all views have an identity: something about it that makes the
view unique.

In SwiftUI these identities come in two forms:

An explicit identity, where we tell SwiftUI the identity for a particular


view.
A structural identity, where SwiftUI implicitly generates identities for
our views based on where we use them in our code.

Regardless, all views must have an identity: SwiftUI will provide these as
much as possible, but there are two key places where we use explicit
identity:

1. When we’re dealing with dynamic data, such as looping over an array.
2. When we need to refer to a particular view, such as scrolling to a
particular location.

Understanding why identity is so important actual boils down into what


might be the single biggest misunderstanding about SwiftUI: “when you
make a change in your view hierarchy, SwiftUI performs tree diffing to
figure out what changed.” Tree diffing would effectively mean Swift
looking at the state of your view hierarchy before your change, and looking
at it after the change, then walking through each to figure out what was
added and removed.

Previously I said that learners often think the parent of a view is whatever
directly contains it – a text view’s parent might be a VStack, for example –
and that it’s okay to take this approach while you’re learning. I think this is
a similar situation: tree diffing feels natural when you’re learning because
you can imagine exactly how it happens at runtime, and so I think it’s a
perfectly fine answer that helps folks make progress.
However, in practice tree diffing never happens thanks to the concept of
identity: as it builds your view code, the Swift compiler has complete
oversight into every subview you’re using, alongside every modifier, every
condition, every loop, and more, and these all get encoded directly into the
type of your views. We saw this earlier when using type(of: self.body) to
examine what the actual type of our view body was, but it’s really important
you understand this extends to logic as well.

Try this, for example:

VStack {
if Bool.random() {
Text("Hello")
} else {
Text("Goodbye")
}
}
.onTapGesture {
print(type(of: self.body))
}

When that runs and you tap the text you’ll see it prints the following into
Xcode’s console: ModifiedContent<VStack<_ConditionalContent<Text,
Text>>, AddGestureModifier<_EndedGesture<TapGesture>>>. If we
break that down, you can see:

1. At the top level we have a ModifiedContent view that contains a


VStack as its view and an AddGestureModifier as its modifier.
2. Inside the VStack is a _ConditionalContent view, which contains
two Text views.

That second part is what’s important here: the _ConditionalContent view,


which is underscored because it’s considered a private implementation
detail rather than something we should attempt to manipulate directly, is
literally our if statement encoded into Swift’s type system.

Like I said, _ConditionalContent is not exposed as public API, but we can


at least see where it’s being made – use Open Quickly (Shift+Cmd+O) to
look for “buildEither”, and you should find this in the SwiftUI generated
interface:

public static func buildEither<TrueContent, FalseContent>


(first: TrueContent) -> _ConditionalContent<TrueContent,
FalseContent> where TrueContent : View, FalseContent : View

So, that handles any kind of view for the true case, and any kind of view for
the false case. When our condition changes – which it might whenever the
body property is reinvoked, because it’s random – SwiftUI doesn’t need to
do any view diffing because it just flips from the TrueContent view to the
FalseContent view.

It even does this when handling switch statements, which it collapses down
into a binary tree of all possible states. Try this to see what I mean:

enum ViewState {
case a, b, c, d, e, f
}

struct ContentView: View {


@State var loadState = ViewState.a

@ViewBuilder var state: some View {


switch loadState {
case .a:
Text("a")
case .b:
Image(systemName: "plus")
case .c:
Circle()
case .d:
Rectangle()
case .e:
Capsule()
case .f:
RoundedRectangle(cornerRadius: 25)
}
}

var body: some View {


Button("Press") {
print(type(of: state))
}
}
}

I’ve wrapped the various states up in a separate property to make the type
easier to read, but when you press the button you’ll see the type is
_ConditionalContent<_ConditionalContent<_ConditionalContent<Tex
t, Image>, _ConditionalContent<Circle, Rectangle>>,
_ConditionalContent<Capsule, RoundedRectangle>> – it’s a binary tree
covering all our cases, so SwiftUI would need to jump through true, true,
true to get some text, or true, true, false to load the image, and so on.

This behavior of converting logic into types has two important side effects,
both of which directly affect how we use SwiftUI:

SwiftUI needs to be able to statically (i.e. at compile time) represent


complex view layouts such as a stack containing three text views, then
an image, then a nested stack, then a button, etc.
That complex view layout is the actual underlying type of our view
body.

Remember, using modifiers transforms our views into nested


ModifiedContent views, and using things like VStack causes SwiftUI to
group multiple views into a TupleView. All these transformations create
extremely long types for our views, which is where Swift’s opaque return
types come in: when we write some View as the return type for our view
body, it means we don’t want to explicitly have to write out the return type
for our layout beyond saying “it will be some type that conforms to the
View protocol,” but – importantly – we aren’t trying to hide that
information from Swift.

Remember, SwiftUI needs to know exactly what is in our view hierarchy in


order to be able to update its layouts efficiently, but if we had sent back a
regular protocol – if we were able to return View rather than some View,
for example – then we’re explicitly hiding data from Swift. Elsewhere in
our code that is often what we want, usually because we want to retain
some flexibility for the future, but with SwiftUI it’s a very bad idea because
it wants to identify all our views based on their type and position within our
view hierarchy.

This is all made possible because the View protocol contains this line of
code:

@ViewBuilder var body: Self.Body { get }

That explicitly marks the body property of our views as using


@ViewBuilder – a result builder that converts our layout into a carefully
curated collection of TupleView, ModifiedContent,
_ConditionalContent, and more. I used this explicitly earlier because only
body gets it automatically applied by the protocol.

So, ModifiedContent is generic over some kind of view and some kind of
modifier, and TupleView is generic over all the views it contains, all so that
SwiftUI has a complete overview of our layout – it can literally guarantee
the contents of a VStack, for example, even when using conditions and
loops to assemble it, which in turn it means all the views have clear
structural identity.

Now, maybe you’re wondering why all this matters, and the answer is that
the identity of our view dictates its lifetime: as soon as the identity of a
view changes, either structurally or explicitly, the view is destroyed.

From a performance perspective this is pretty bad, because SwiftUI will


throw away your platform views – the underlying UIView or NSView used
to render your SwiftUI layout to the screen – when their matching SwiftUI
lifetimes end, but more importantly it will also destroy any data your views
were storing, because as far as SwiftUI is concerned you asked for your
view to be destroyed.

You already saw how _ConditionalContent is generic over its true and
false content types, so perhaps you can see where this is leading: whenever
we flip between two views using an if condition, that causes SwiftUI to
throw away its platform views and all the state we created each time.
You can see this in action with the following code:

struct ExampleView: View {


@State private var counter = 0

var body: some View {


Button("Tap Count: \(counter)") {
counter += 1
}
}
}

struct ContentView: View {


@State private var scaleUp = false

var body: some View {


VStack {
if scaleUp {
ExampleView()
.scaleEffect(2)
} else {
ExampleView()
}

Toggle("Scale Up", isOn: $scaleUp.animation())


}
.padding()
}
}

That renders the same view in two slightly different ways: one scaled up to
200%, and one at its default size. The scaling happens using an animated
Boolean, and ExampleView stores some state for how many times its
button was tapped.

When you run that code you’ll notice two things:

1. The scaling effect happens as a fade – the smaller button fades out,
while the larger one fades in.
2. The tap count for your view gets reset to 0 every time you toggle the
switch.
Both of these happen because of that _ConditionalContent flip: SwiftUI
destroys the original ExampleView along with its platform rendering, and
creates a new ExampleView in its place – the fade effect happens because
we’re seeing a transition, rather than an animation. As for the @State being
lost, again this is because SwiftUI considers the original view to be
destroyed, so it removes all its data at the same time.

We’re losing data, we’re losing smooth animations, and we’re making
SwiftUI do a lot of extra work, because as far as SwiftUI is concerned these
are two separate views.

This problem remains even if we removed the view modifier so all that was
changing was the way ExampleView was created:

struct ExampleView: View {


@State private var counter = 0
let scale: Double

var body: some View {


Button("Tap Count: \(counter)") {
counter += 1
}
.scaleEffect(scale)
}
}

struct ContentView: View {


@State private var scaleUp = false

var body: some View {


VStack {
if scaleUp {
ExampleView(scale: 2)
} else {
ExampleView(scale: 1)
}

Toggle("Scale Up", isOn: $scaleUp.animation())


}
.padding()
}
}
Where things get interesting is what happens if we add some explicit
identity, like this:

var body: some View {


VStack {
if scaleUp {
ExampleView(scale: 2)
.id("Example")
} else {
ExampleView(scale: 1)
.id("Example")
}

Toggle("Scale Up", isOn: $scaleUp.animation())


}
.padding()
}

You might expect that to work, because now we’re telling SwiftUI that both
our ExampleView instances are the same, but no dice – we’ll still get the
views being destroyed and recreated, with all the associated problems of
that.

The problem isn’t having two ExampleView instances; that’s actually fine,
and thanks to the id() modifier SwiftUI is able to figure out that they are the
same view. The actual problem is _ConditionalContent, because as we
saw earlier it explicitly stores its data as two separate views – it is hard-
coded to think of its two pieces of data as being distinct, no matter what
identifiers we attach to them. This means from a structural identity
perspective our two views are different, no matter what explicit identifiers
we give them.

Remember, _ConditionalContent exists because the View protocol


explicitly marks its body property as using @ViewBuilder. If we get our
code out of the body then we don’t get @ViewBuilder unless we ask for it,
which means we don’t get _ConditionalContent, and that in turn means
SwiftUI is able to rely on the explicit identity we provide.

We can see this for ourselves if we move our two ExampleView instances
into a computed property, like this:
struct ContentView: View {
@State private var scaleUp = false

var exampleView: some View {


if scaleUp {
return ExampleView(scale: 2)
.id("Example")
} else {
return ExampleView(scale: 1)
.id("Example")
}
}

var body: some View {


VStack {
exampleView

Toggle("Scale Up", isOn: $scaleUp.animation())


}
.padding()
}
}

Now SwiftUI will work as expected: it will preserve the state between both
views, animate correctly, and perform efficiently. Even better, we can
actually remove the id() modifier here and SwiftUI can still figure out it’s
the same view.

As usual, I’m going to explain why in a moment, but first I’d like you think
about it before reading my answer – how is SwiftUI able to understand the
two ExampleView instances are the same now, even without an explicit
id() attached, when it couldn’t before?

Still here?

The answer is that removing @ViewBuilder removes the


_ConditionalContent struct entirely, which means flipping between the
two view states no longer means flipping between two different “True”
view and a “False” view.
Instead, because the exampleView property is used in a fixed position in
our layout, SwiftUI can rely on good old structural identity to realize that
the two structs should both map to the same underlying view – the property
might return ExampleView(scale: 2) or ExampleView(scale: 1), but either
way it’s an instance of ExampleView so SwiftUI considers them the same
view.

This might sound ridiculous, or perhaps even dangerous: our code specifies
two different ExampleView instances being sent back from a single
property, so why should SwiftUI consider them the same? Understanding
this gets to the very heart of what @State does and why it even exists.

You see, every time SwiftUI evaluates a view’s body property it creates
new instances of all the view structs inside – it has to, because structs
always have a unique owner, and can’t somehow be “reused” from a
previous body invocation.

So, even a trivial view struct will be recreated often, which means SwiftUI
regularly needs to map the old struct to the new struct – to recognize that
two instances of a view struct are the same and should share the same state
– so that it can preserve state correctly. When our exampleView property
returns the same view type without using @ViewBuilder, it’s really no
different from it having no condition at all and just returning a single view.

Of course, by ditching @ViewBuilder we’ve lost all the things that make
SwiftUI natural – we have to return one specific type of view from both our
condition cases, so we’re out of luck if one case wants to add a background
color while the other doesn’t.

What SwiftUI really wants – and the kind of code we should really be
striving for – is for us to use the ternary conditional operator, like this:

struct ContentView: View {


@State private var scaleUp = false

var body: some View {


VStack {
ExampleView(scale: scaleUp ? 2 : 1)
Toggle("Scale Up", isOn: $scaleUp.animation())
}
.padding()
}
}

Using this approach we’re back to having structural identity do all the hard
work for us: regardless of the value of scaleUp we have an ExampleView
as the first child of our VStack, so SwiftUI will keep it alive as the Boolean
changes.

I think the problem is pretty clear in our current code, but it might take a
little more thought when dealing with modifiers depending on which iOS
version you’re targeting. For example, we might have some code to show a
message in bold if the user hasn’t read it yet:

struct ContentView: View {


@State private var isNewMessage = false

var body: some View {


if isNewMessage {
Text("Message title here").bold()
} else {
Text("Message title here")
}
}
}

How could we write that as a single view? In iOS 16 and later the bold()
modifier takes a Boolean saying whether it’s active or not, but if you’re
targeting 15 or earlier – you either add the modifier or you don’t.

If you aren’t able to target 16 or later, the fix here is to remove bold()
entirely and replace it with fontWeight(), which does accept other options:

Text("Message title here").fontWeight(isNewMessage ? .bold :


.regular)
We could get even simpler because fontWeight() actually accepts an
optional weight, where nil means “the default”:

Text("Message title here").fontWeight(isNewMessage ? .bold :


nil)

Some SwiftUI modifiers simply refuse to accept a customization parameter,


with the most notorious being hidden() – it unconditionally hides a view in
our layout, while leaving space where it was. This means using it puts back
to the state loss problem from earlier:

struct ContentView: View {


@State private var shouldHide = false

var body: some View {


VStack {
if shouldHide {
ExampleView(scale: 1)
.hidden()
} else {
ExampleView(scale: 1)
}

Button("Toggle") {
withAnimation {
shouldHide.toggle()
}
}
}
}
}

Remember, the problem here is @ViewBuilder, not ExampleView – even


if you tried to build an improved hidden() modifier that accepts a Boolean,
you’ll fall foul if you adopt @ViewBuilder:

extension View {
@ViewBuilder func hidden(_ hidden: Bool) -> some View {
if hidden {
self.hidden()
} else {
self
}
}
}

Once again the key is to use a ternary conditional operator, which means no
more need for @ViewBuilder:

extension View {
func hidden(_ hidden: Bool) -> some View {
self.opacity(hidden ? 0 : 1)
}
}

That preserves our view’s identity regardless of the value of hidden, which
ensures the same view stays alive the entire time.

So, while it might take a little extra work sometimes, using identity
properly increases performance, preserves program state, and creates better
animations.

OceanofPDF.com
Intentionally discarding identity
There are a handful of places where you want to intentionally discard the
identity of your views – to tell SwiftUI that two instances of a view are
different no matter what it might otherwise think.

For example, consider the following list that shuffles its contents when a
button is tapped:

struct ContentView: View {


@State private var items = Array(1...20)

var body: some View {


VStack(spacing: 0) {
List(items, id: \.self) {
Text("Item \($0)")
}

Button("Shuffle") {
withAnimation {
items.shuffle()
}
}
.buttonStyle(.borderedProminent)
.padding(5)
}
}
}

Using withAnimation() here will trigger the default iOS list animation,
where each row will slide from its old position to its new position. That
might be what you want, but in many places this effect is rather hard on the
eye – when a single row moves this animation looks great, but when
everything moves it just causes a jumble.
To fix this we can provide SwiftUI with an explicit identity for our list, but
use a random value for that identity so that it changes every time the view is
evaluated. This means SwiftUI sees the same structural location for the
view but a different explicit identity, and so will consider the two lists to be
different. In practice, that means it will remove one and insert the other
using a default fade transition just by changing the code to this:

List(items, id: \.self) {


Text("Item \($0)")
}
.id(UUID())
That immediately looks better when many rows are changing at once, but it
also means we now have complete control over how the animation happens
rather than being forced to use the default list reorder animation. So, we
could make a 1-second ease-in-out animation like this:

Button("Shuffle") {
withAnimation(.easeInOut(duration: 1)) {
items.shuffle()
}
}

Or we could add a custom transition rather than fading, like this:

List(items, id: \.self) {


Text("Item \($0)")
}
.id(UUID())
.transition(.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading)))

So, now we have complete control over the animation, which means you
can create something less intense as the default row slide, or perhaps
something just plain different like our edge transition. If you tap Shuffle
quickly, you’ll even see multiple overlapping lists arrive and depart in swift
succession.
Remember, discarding identity does have the downside that SwiftUI will
destroy any underlying data storage and recreate any platform views, so be
careful – there is a cost to this work, particularly when dealing with more
complex views such as List.

In the simplest case, this technique is useful when you want to let the user
cycle through various options. For example, we could make a simple icon
generator by selecting random colors and SF Symbols:

struct ContentView: View {


let colors: [Color] = [.blue, .cyan, .gray, .green,
.indigo, .mint, .orange, .pink, .purple, .red]
let symbols = ["run", "archery", "basketball", "bowling",
"dance", "golf", "hiking", "jumprope", "rugby", "tennis",
"volleyball", "yoga"]
@State private var id = UUID()

var body: some View {


VStack {
ZStack {
Circle()
.fill(colors.randomElement()!)
.padding()

Image(systemName: "figure.\
(symbols.randomElement()!)")
.font(.system(size: 144))
.foregroundColor(.white)
}
.transition(.slide)
.id(id)

Button("Change") {
withAnimation(.easeInOut(duration: 1)) {
id = UUID()
}
}
.buttonStyle(.borderedProminent)
.padding(.bottom)
}
}
}
Just changing the id property to a new value is enough to pick a new
random color, a new random SF Symbol, and having the changes animate in
smoothly – all by explicitly discarding identity.
OceanofPDF.com
Optional views, gestures, and more
We all know that optionals are a core feature of the Swift language, but
don’t underestimate the usefulness of optionals in SwiftUI – you’ll find
optionals are baked right into key places for extra flexibility.

For example, if you were just to look at Xcode’s autocompletion options


you would see that the background() modifier accepts any kind of View,
Shape, or ShapeStyle, but it doesn’t accept optionals – you need to provide
a concrete instance of one of those types.

However, SwiftUI is really smart here, and to see why I’d like you to try
this code:

Text("Hello")
.background(Color.blue)
.onTapGesture {
print(type(of: self.body))
}

When that runs, you’ll see it has the type


_BackgroundStyleModifier<Color>.

In comparison, we could make the background optional, like this:

.background(Bool.random() ? Color.blue : nil)

When that runs you’ll see the type is now


_BackgroundModifier<Optional<Color>> – the background of our view
is a Color?, meaning that it might be there or might not depending on the
result of our random Boolean.

This is possible because Optional uses conditional conformance to become


a View in its own right. To see how, use Open Quickly to bring up the
generated interface for SwiftUI by searching for something like
NavigationStack, then search in there for extension Optional. You’ll see
that SwiftUI extends the Optional enum to conform to a range of protocols
where it wraps types that conform to those protocols, like this:

extension Optional : Commands where Wrapped : Commands


extension Optional : Gesture where Wrapped : Gesture
extension Optional : View where Wrapped : View

So, Optional conforms to Commands where the thing inside the optional
also conforms to Commands, etc.

This is what makes it possible to conditionally apply backgrounds or


overlays, or to conditionally enable a gesture based on some program state
– use the gesture when your state is true, or use nil otherwise to remove it.

OceanofPDF.com
Chapter 2
Animations and Transitions
OceanofPDF.com
Animating the unanimatable
Almost everything can be animated in SwiftUI, although you’ll find there
are quite a few things that take a little… encouragement, shall we say?

First the easy stuff. We can trigger an explicit animation using


withAnimation():

struct ContentView: View {


@State private var scale = 1.0

var body: some View {


Text("Hello, World!")
.scaleEffect(scale)
.onTapGesture {
withAnimation {
scale += 1
}
}
}
}

And we can use implicit animations instead:

struct ContentView: View {


@State private var scale = 1.0

var body: some View {


Text("Hello, World!")
.scaleEffect(scale)
.onTapGesture {
scale += 1
}
.animation(.default, value: scale)
}
}

Tip: Do not use implicit animations without providing the value parameter
– that’s deprecated from iOS 15 and later because it would animate every
change, including device rotation.
But not everything works this way. For example, if we had several
overlapping views, we might want to animate the Z index of one view:

struct ContentView: View {


@State private var redAtFront = false
let colors: [Color] = [.blue, .green, .orange, .purple,
.mint]

var body: some View {


VStack {
Button("Toggle zIndex") {
withAnimation(.linear(duration: 1)) {
redAtFront.toggle()
}
}

ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(.red)
.zIndex(redAtFront ? 6 : 0)

ForEach(0..<5) { i in
RoundedRectangle(cornerRadius: 25)
.fill(colors[i])
.offset(x: Double(i + 1) * 20, y:
Double(i + 1) * 20)
.zIndex(Double(i))
}
}
.frame(width: 200, height: 200)
}
}
}

That code is correct, but won’t work: the red box will jump to the front,
despite the animation request. This is because Z index can’t be animated
with SwiftUI – or at least not by default.
We can make our Z index code animate with a surprisingly small change,
and the technique involved can be applied in numerous other places to
animate practically anything.

The key is to create a new ViewModifier that conforms to the Animatable


protocol, which has the job of handling whatever you need in your your
animation. So, we might write this:

struct AnimatableZIndexModifier: ViewModifier, Animatable {


var index: Double

func body(content: Content) -> some View {


content
.zIndex(index)
}
}

Tip: The name of the view modifier is “animatable” not “animated” – we’re
providing the ability for this change to be animated, but whether or not it
actually is animated depends on how it’s used.

While it’s possible to apply modifier structs directly to a view, it’s usually a
better idea to wrap them in a View extension to make our code easier:

extension View {
func animatableZIndex(_ index: Double) -> some View {
self.modifier(AnimatableZIndexModifier(index: index))
}
}

And now rather than using zIndex() on the view we want to change, we use
animatedZIndex() instead:

RoundedRectangle(cornerRadius: 25)
.fill(.red)
.animatableZIndex(redAtFront ? 6 : 0)

However, that still won’t work – no animation will take place.

You see, all the Animatable protocol really does is give us the ability to
read and write some kind of interpolated value over time. As we’re
animating between the values 0 and 6, Animatable will send us values like
0.1, 1.35, 4.825, and so on, as it moves smoothly from 0 through to 6 based
on whatever timing curve the animation is using. That’s all it does: it sends
in the interpolated value, and it’s down to us to decide what should happen
to it.

In this case, those interpolated values are exactly what we want for our Z
index, so that our view moves smoothly from Z index 0 through to 6. So,
when the Animatable protocol attempts to provide some animating data for
us, we just need to assign that to our index property – add this property to
AnimatableZIndexModifier now:
var animatableData: Double {
get { index }
set { index = newValue }
}

That’s it! If you run the code again you’ll see our animation works great.

Tip: You can just create a regular stored property called animatableData to
get the same result, but it might result in quite clumsy code.

If you’re curious, try adding a print() statement into the setter we just made
so you can see exactly what the Animatable protocol is doing:

var animatableData: Double {


get { index }
set { print(newValue); index = newValue }
}

When that runs you’ll now see all the interpolated values being passed in –
it’s another feature of SwiftUI that looks like magic, but is actually
surprisingly simple internally.

Having this kind of control is particularly important if you need to support


iOS versions below 16, because a number of things could not be animated
in iOS 15.6 and below. For example, animating the system font to a new
size works out of the box in iOS 16 and later, but if you need backwards
compatibility it means relying on Animatable like this:

struct AnimatableFontModifier: ViewModifier, Animatable {


var size: Double

var animatableData: Double {


get { size }
set { size = newValue }
}

func body(content: Content) -> some View {


content
.font(.system(size: size))
}
}

Again, it’s a good idea to create a View extension to make it easier to use:

extension View {
func animatableFont(size: Double) -> some View {
self.modifier(AnimatableFontModifier(size: size))
}
}

And now you can create a view to use it:

struct ContentView: View {


@State private var scaleUp = false

var body: some View {


Text("Hello, World!")
.animatableFont(size: scaleUp ? 56 : 24)
.onTapGesture {
withAnimation(.spring(response: 0.5,
dampingFraction: 0.5)) {
scaleUp.toggle()
}
}
}
}

The result looks fantastic, but keep in mind that using it means SwiftUI has
to create the system font at every size increment passed in by Animatable –
it’s a great effect, but it’s easily overused. I’m hoping that Apple’s own
solution from iOS 16 and later is somehow more optimized!

OceanofPDF.com
Avoiding pain in iOS 15.6 and below
If you need to target iOS 15.6 and below (or similar versions of macOS,
tvOS, and watchOS), there is one particular thing that isn’t animatable by
default even though it seems like it ought to be, and that’s the
foregroundColor() modifier. This kind of code won’t work at all:

struct ContentView: View {


@State private var isRed = false

var body: some View {


Text("Hello, World!")
.foregroundColor(isRed ? .red : .blue)
.font(.largeTitle.bold())
.onTapGesture {
withAnimation {
isRed.toggle()
}
}
}
}

We could go down a complex route of making it animatable, but there is no


neat solution with this approach – you’d need to store both the before and
after colors right inside the view you want to work with, then use the values
from the Animatable protocol to manually interpolate between the RGBA
values of those two colors.

Fortunately, we can cheat a little, because the colorMultiply() method is


animatable. This multiplies the original color of a view with some other
color, meaning that the red value of the original is multiplied by the red
value of our other color, then the green, then the blue, and then the alpha. If
we use white as our original color, then multiplying by any other color will
return that same color because we’re multiplying each of its components by
1.

So, if we give the text a white color we can multiply over it using the colors
we’re trying to animate between, like this:
Text("Hello, World!")
.foregroundColor(.white)
.colorMultiply(isRed ? .red : .blue)

And that will work, without having to go into the mess of trying to make
something custom. If you wanted, you could still wrap up this behavior in a
neat modifier, like this:

extension View {
func animatableForegroundColor(_ color: Color) -> some
View {
self
.foregroundColor(.white)
.colorMultiply(color)
}
}

Fortunately for all of us, foregroundColor() is animatable in iOS 16.0 and


later.

OceanofPDF.com
Creating animated views
I said it earlier, but it bears repeating: all the Animatable protocol really
does is give us the ability to read and write some kind of interpolated value
over time. This means it isn’t restricted to ViewModifier, and actually
works perfectly fine with a plain old View too.

As an example, we could make a view that knows how to animate a number


between various values – we’d start off by making something that knows
how to draw some text with a specific fraction length, like this:

struct CountingText: View, Animatable {


var value: Double
var fractionLength = 8

var body: some View {

Text(value.formatted(.number.precision(.fractionLength(fracti
onLength))))
}
}

That’s barely doing anything, but thanks to SwiftUI the next step is trivial –
how do you think we upgrade that so it supports animation?

Simple: we just add an animatableData property to get and set value, like
this:

var animatableData: Double {


get { value }
set { value = newValue }
}

Now we can go ahead and use it just like any other view:

struct ContentView: View {


@State private var value = 0.0
var body: some View {
CountingText(value: value)
.onTapGesture {
withAnimation(.linear) {
value = Double.random(in: 1...1000)
}
}
}
}

The point is that Animatable sends in whatever value should be used and
it’s up to us what we do with it – we might display it immediately, we might
apply it to a bunch of other modifiers, or perhaps we stash the values away
for use later on.

I’d like you to try it yourself: try creating a TypewriterText view that
accepts a string to display, and is able to type it out using an animation.

Have a go and see how you get on! I’ll add my solution below.

The simplest solution looks like this:

struct TypewriterText: View, Animatable {


var string: String
var count = 0

var animatableData: Double {


get { Double(count) }
set { count = Int(max(0, newValue)) }
}

var body: some View {


let stringToShow = String(string.prefix(count))
Text(stringToShow)
}
}

We could then use it something like this:

struct ContentView: View {


@State private var value = 0
let message = "This is a very long piece of text that
appears letter by letter."

var body: some View {


VStack {
TypewriterText(string: message, count: value)
.frame(width: 300, alignment: .leading)

Button("Type!") {
withAnimation(.linear(duration: 2)) {
value = message.count
}
}

Button("Reset") {
value = 0
}
}
}
}

That works well, but we could improve the effect a little more by adding a
hidden copy of our text inside a ZStack, so that SwiftUI preallocates the
right amount of space for the text:

ZStack {
Text(string)
.hidden()
.overlay(
Text(stringToShow),
alignment: .topLeading
)
}
But we can do better! Having this typewriting effect is nice for lots of folks,
but what about folks who rely on VoiceOver, or folks who have specifically
asked apps to reduce the amount of animation they use? If we factor in both
those we can make this view even better.

First, add two properties to the view:

@Environment(\.accessibilityVoiceOverEnabled) var
accessibilityVoiceOverEnabled
@Environment(\.accessibilityReduceMotion) var
accessibilityReduceMotion

And now we can modify the body property to return two different things
depending on whether we want the animation or not:

if accessibilityVoiceOverEnabled || accessibilityReduceMotion
{
Text(string)
} else {
let stringToShow = String(string.prefix(count))

ZStack {
Text(string)
.hidden()
.overlay(
Text(stringToShow),
alignment: .topLeading
)
}
}

With that in place we have a great solution that works for everyone.

OceanofPDF.com
Custom timing curves
SwiftUI gives us fine-grained control over how our animation movements
take place: rather than relying on linear movements or ease-in-out, for
example, we can instead create completely custom cubic Bézier paths that
match whatever acceleration and deceleration we want.

For example, we could create a timing curve that very slowly around the
center of an animation, but bounces hard on the edges:

extension Animation {
static var edgeBounce: Animation {
Animation.timingCurve(0, 1, 1, 0)
}

static func edgeBounce(duration: TimeInterval = 0.2) ->


Animation {
Animation.timingCurve(0, 1, 1, 0, duration: duration)
}
}

Notice how I’ve added two variations of the same curve: one as a property,
and one as a method that accepts a duration. This matches the same way
Apple’s own timing curves are created – e.g. .easeIn and
.easeIn(duration:) – so it makes it more natural to use our custom curves.

With that extension in place, we can now create animations using our
custom timing curve just like we would use one of the built-in curves:

struct ContentView: View {


@State private var offset = -200.0

var body: some View {


Text("Hello, world!")
.offset(y: offset)
.animation(.edgeBounce(duration:
2).repeatForever(autoreverses: true), value: offset)
.onTapGesture {
offset = 200
}
}
}

A particularly common animation curve is called “ease in out back”, which


is like a double spring animation where the change goes in the wrong
direction first, then moves forward normally, then overshoots the
destination, then move back to the finished value. You’ll often see this in
Apple’s own designs, such as the App Store: when you tap on one of their
featured stories in the Today tab, the image shrinks a little, then scales up to
fill the screen.

We can implement this ourselves:

extension Animation {
static var easeInOutBack: Animation {
Animation.timingCurve(0.5, -0.5, 0.5, 1.5)
}

static func easeInOutBack(duration: TimeInterval = 0.2) -


> Animation {
Animation.timingCurve(0.5, -0.5, 0.5, 1.5, duration:
duration)
}
}

Or create a stronger effect by increasing the steepness of the curve:

static var easeInOutBackSteep: Animation {


Animation.timingCurve(0.7, -0.5, 0.3, 1.5)
}

static func easeInOutBackSteep(duration: TimeInterval = 0.2)


-> Animation {
Animation.timingCurve(0.7, -0.5, 0.3, 1.5, duration:
duration)
}

Rather than try to guess the various X/Y values for your Bézier curves, a
much better idea is to use a website such as https://cubic-bezier.com that
lets you drag handles around visually to control exactly how the movement
should work.

Once you’re done, that site lets you preview the movement compared to
other common curves, and provides the current parameters to input into
your timing curve code – it really is the easiest way to get the exact effect
you want, and gives you lots of chance to experiment to create some unique
animation effects.

OceanofPDF.com
Overriding animations
Animations can be triggered in all sorts of ways and places in SwiftUI, but
we have API available to us that helps control the way animations happen –
we can inject custom functionality into the process to get whatever specific
result we’re aiming for.

Previously I showed you how we can make an Animatable view selectively


disable its animations by watching the environment, but it’s not always
possible to write code to bypass the animation in that way. In fact, a lot of
the time you shouldn’t even be calling withAnimation() unless you
actually want animation to happen.

So, rather than having view modifiers try to override an animation request,
we could write a small global function to give us more control over the
process, like this:

func withMotionAnimation<Result>(_ animation: Animation? =


.default, _ body: () throws -> Result) rethrows -> Result {
if UIAccessibility.isReduceMotionEnabled {
return try body()
} else {
return try withAnimation(animation, body)
}
}

As that’s a free function, we don’t have access to the SwiftUI environment


to query the current setting for reducing motion, but
UIAccessibility.isReduceMotionEnabled works just fine. Using this
approach allows us to make our intent clear: when we say withAnimation()
we mean this is a non-movement animation such as an opacity change,
whereas when we use withMotionAnimation() we mean this involves
movement and therefore might need to be skipped based on the user’s
settings.

Use it like this:


struct ContentView: View {
@State var scale = 1.0

var body: some View {


Button("Tap Me") {
withMotionAnimation {
scale += 1
}
}
.scaleEffect(scale)
}
}

That solves the problem for times when we create an explicit animation:
just switch withAnimation() for withMotionAnimation() and our function
takes care of the rest. But that doesn’t solve implicit animations like this
one:

struct ContentView: View {


@State var scale = 1.0

var body: some View {


Button("Tap Me") {
withMotionAnimation {
scale += 1
}
}
.scaleEffect(scale)
.animation(.default, value: scale)
}
}

Even with withMotionAnimation() being used, our implicit animation will


ignore the Reduce Motion setting – the implicit overrides the explicit. We
could fix this by adding a new modifier that only selectively applies the
animation, based on the user’s preferences:

struct MotionAnimationModifier<V: Equatable>: ViewModifier {


@Environment(\.accessibilityReduceMotion) var
accessibilityReduceMotion
let animation: Animation?
let value: V

func body(content: Content) -> some View {


if accessibilityReduceMotion {
content
} else {
content.animation(animation, value: value)
}
}
}

As always, adding a View extension makes this much easier to use:

extension View {
func motionAnimation<V: Equatable>(_ animation:
Animation?, value: V) -> some View {
self.modifier(MotionAnimationModifier(animation:
animation, value: value))
}
}

And now we can use that to get implicit animations that automatically
respect the user’s settings:

Button("Tap Me") {
scale += 1
}
.scaleEffect(scale)
.motionAnimation(.default, value: scale)

That’s a big step forward, but it still only solves part of the problem: what if
we need to override the implicit animation on a case-by-case basis, rather
than always overriding it? That is, what if we want the default animation
most of the time, but in one particular event – when a particular button is
clicked, for example – we don’t want it?

In this instance we need to use a transaction, which gives us control over


what’s happening in the current animation. Transactions are SwiftUI’s
stores all the context for an animation that is currently in flight, allowing it
to be passed around the view hierarchy. We can create them by calling
withTransaction() then customizing the new transaction, which is
effectively what withAnimation() is doing – albeit with less code.

In particular, what we care about is the disablesAnimations property of


transactions, which lets us disable implicit animations that would otherwise
be part of this update.

So, we could disable our implicit animation like this:

Button("Tap Me") {
var transaction = Transaction()
transaction.disablesAnimations = true

withTransaction(transaction) {
scale += 1
}
}
.scaleEffect(scale)
.animation(.default, value: scale)

That means we’ll get the default animation for all changes, except for those
triggered by the button tap.

This behavior is so useful that I find it best to make another global


animation function to wrap it all up in one place:

func withoutAnimation<Result>(_ body: () throws -> Result)


rethrows -> Result {
var transaction = Transaction()
transaction.disablesAnimations = true
return try withTransaction(transaction, body)
}

When we use withAnimation() we are effectively creating a new


transaction with whatever new animation we want, so I think creating this
similar withoutAnimation() function is a great counterpart.
That global function works great for the times when you want to blanket
disable animations, but transactions let us go further: what if we have an
implicit animation that we want to override – we want a different animation
to happen, rather than just skipping animations entirely? Transactions are
perfect here, because if set disablesAnimations to true we still get to apply
our own animation in its place.

Once again, this kind of functionality is best wrapped up in another global


function for easier access:

func withHighPriorityAnimation<Result>(_ animation:


Animation? = .default, _ body: () throws -> Result) rethrows
-> Result {
var transaction = Transaction(animation: animation)
transaction.disablesAnimations = true
return try withTransaction(transaction, body)
}

We can now write code like this:

struct ContentView: View {


@State var scale = 1.0

var body: some View {


Button("Tap Me") {
withHighPriorityAnimation(.linear(duration: 3)) {
scale += 1
}
}
.scaleEffect(scale)
.animation(.default, value: scale)
}
}

That has a default implicit animation, but we’re explicitly overriding it with
a 3-second linear animation – we get the implicit animation most of the
time, but an explicit override for the times we need it.

So far we have looked at:


Disabling explicit animations based on Reduce Motion
Disabling implicit animations based on Reduce Motion
Disabling implicit animations on a case-by-case basis
Replacing implicit animations with an explicit animation on a case-by-
case basis

But there’s one more situation you’re likely to encounter: what happens if
part of your view hierarchy wants to override an animation?

This is another place where transactions solve the problem for us, but this
time they are applied differently: we don’t want to create a new transaction
to replace our global transaction, but instead we want each view to
selectively override just their part of the transaction.

This is done using the transaction() modifier, which provides us with an


inout transaction object to modify – we can just go ahead and modify it in
place, and it will be used for any animation transactions that apply to this
view.

Important: Apple very strongly recommends against using the


transaction() modifier on container views, because it could generate huge
amounts of work. Instead, use it on leaf views – views that don’t have any
children.

To demonstrate this modifier in action, here’s an example view that creates


a grid of circles in either red or blue:

struct CircleGrid: View {


var useRedFill = false

var body: some View {


LazyVGrid(columns: [.init(.adaptive(minimum: 64))]) {
ForEach(0..<30) { i in
Circle()
.fill(useRedFill ? .red : .blue)
.frame(height: 64)
}
}
}
}
That view has no idea about animations – we haven’t added them
anywhere, implicitly or explicitly, but thanks to the way SwiftUI works we
can trigger an animation externally like this:

struct ContentView: View {


@State var useRedFill = false

var body: some View {


VStack {
CircleGrid(useRedFill: useRedFill)

Spacer()

Button("Toggle Color") {
withAnimation(.easeInOut) {
useRedFill.toggle()
}
}
}
}
}

Earlier I said that using withAnimation() effectively creates a new


transaction with whatever new animation we want, and here’s where that
behavior becomes important: that code says “start a new transaction, inside
that set a Boolean to be true, which will cause all the circles to turn red.”
That color adjustment will take place with our custom transaction in place,
which means the circles will change in a particular way.

Now, even though all those circles in the grid have no idea an animation is
taking place, they can still exert control over any animations that do take
place – they can examine or override any animation that affects them.

For example, we could say that our circles don’t actually care what
animations they have, as long as they start with a delay:

Circle()
.fill(useRedFill ? .red : .blue)
.frame(height: 64)
.transaction { transaction in
transaction.animation =
transaction.animation?.delay(Double(i) / 10)
}

With that in place, any animation that happens to the circle will now happen
with a delay – we haven’t touched the rest of the animation, just that one
small part.

The end result is quite beautiful, I think: even though the circles have no
idea what kind of animation if any is taking place, we’ve now made them
change color in a wave, and you can even press the Toggle Color button
multiple times to move smoothly between the two states.

OceanofPDF.com
Advanced transitions
SwiftUI’s transitions system allows us to customize the way views are
inserted or removed, but because the built-in selection is pretty tame you’d
be forgiven for thinking the transitions system isn’t that capable. Well, the
truth is that transitions can do pretty much whatever the heck you want with
your views: you can insert a whole range of new views around whatever
you’re transitioning, create local state, add complex animations, and much
more.

To demonstrate this I want to recreate a small but complex animation: the


“heart” animation from Twitter. When you like a tweet, several things
happen:

1. A circle grows out from the center.


2. That circle then shrinks from the inside out.
3. Some colorful confetti pieces fly out from the edges of the circle.
4. The filled heart icon springs out from the center, and bounces to its
final position.

If we take screen captures of it along the way it looks like this:

Honestly, it’s best if you just try favoriting something on Twitter a few
times – it’s a very fast animation, but there’s a lot going on.

We can recreate this entirely with SwiftUI’s transitions, meaning that we get
a simple, reusable way of adding this kind of animation to any view that is
being shown.
As this involves quite a few simultaneous animations, we’re going to start
small and build our way up.

Tip: When working with complex animations like this one, I recommend
you go to the simulator’s Debug menu and select Slow Animations so you
can see exactly what’s happening.

First, we’ll create a simple view modifier that stores three properties and
renders its content unchanged. The properties are:

The speed the animation will take place. This will be used in various
places as both animation duration and delay, so having it one central
place makes our code easier to follow.
The color to render the effect. This will be used to render the circle and
confetti.
How big to draw the confetti. This is helpful if the user is bringing in a
larger view, where chunkier confetti look much better.

Add this new view modifier now:

struct ConfettiModifier: ViewModifier {


private let speed = 0.3

var color: Color


var size: Double

func body(content: Content) -> some View {


content
}
}

Again, that body() method does nothing right now – it just renders
whatever it’s given, which is fine given that we’re just setting up a skeleton.
To make that easier to use, we’re going to extend AnyTransition with both
a property and a method, just like SwiftUI’s own built-in transitions. The
property will force default values of .blue and 3 for color and size, whereas
the method will allow the user to customize them as needed.

So, add this extension:

extension AnyTransition {
static var confetti: AnyTransition {
.modifier(
active: ConfettiModifier(color: .blue, size: 3),
identity: ConfettiModifier(color: .blue, size: 3)
)
}

static func confetti(color: Color = .blue, size: Double =


3.0) -> AnyTransition {
AnyTransition.modifier(
active: ConfettiModifier(color: color, size:
size),
identity: ConfettiModifier(color: color, size:
size)
)
}
}

And finally we can create a simple test view that renders an SF Symbol
using three fonts, so you can see how it looks at various sizes:

struct ContentView: View {


@State private var isFavorite = false

var body: some View {


VStack(spacing: 60) {
ForEach([Font.body, Font.largeTitle,
Font.system(size: 72)], id: \.self) { font in
Button {
isFavorite.toggle()
} label: {
if isFavorite {
Image(systemName: "heart.fill")
.foregroundStyle(.red)
.transition(.confetti(color:
.red, size: 3))
} else {
Image(systemName: "heart")
.foregroundStyle(.gray)
}
}
.font(font)
}
}
}
}

Okay, that’s our set up code done. You’re welcome to run it if you want but
you won’t be impressed – our view modifier really does nothing at all, so it
will just flip between the unfilled and filled heart symbols.
That skeleton does give us a great place to build on, though, because now
we can start to build up the animation step by step. If you remember, the
first step in the animation is creating a circle that grows out from the center,
meaning that it starts invisibly small and grows to fill all the available space
and then some – it needs to be larger than the icon itself, not least because
the final filled heart icon uses a spring animation and so overshoots its
target size a little.

To make this circle animation, we’ll start by adding a new property to


ConfettiModifier to track the circle’s growing size, starting at a very low
value:

@State private var circleSize = 0.00001

Important: SwiftUI will complain loudly if you try to scale something to


0.0, so small values like 0.00001 are preferred.

Now I’d like you to add several modifiers to the content line in body():

We’ll use hidden() to make the actual view we’re transitioning


invisible, but still reserve space for it. Remember, the finished heart
icon animates in separately at the very end, so we need to get the
actual view out of the way.
We’ll use padding(10) to make our circle area bigger than the view
we’re transitioning, so we have space to overshoot.
We’ll use overlay() to render the circle.
We’ll use padding() a second time, this time with a value of -10.
Finally, we’ll use onAppear() to start an animation to make circleSize
1.

Now, you might wonder why padding() is in there twice, and the answer is
simple: we need to add some padding before the overlay in order that the
overlay is able to take up more space than our transitioning view, but we
don’t want that padding to stick around after the overlay – we don’t want it
to move over views away from our buttons. So, whatever padding we add
before the overlay needs to be removed after the overlay, to keep things
balanced.
Go ahead and modify your body() method to this:

content
.hidden()
.padding(10)
.overlay(
// circle here
)
.padding(-10)
.onAppear {
withAnimation(.easeIn(duration: speed)) {
circleSize = 1
}
}

Now, I left out the important overlay code because it deserves its own
explanation. You see, if we place a circle into our overlay it will
automatically take up all the available space – it will automatically be the
same size as our transitioning view, plus the 10 points of padding.

This circle needs to be filled in with a color, but the second step in this
animation is to make the circle shrink from the inside out. That is, once it
has grown to its full size, it starts to become hollow, and shrinks thinner and
thinner until it has finally disappeared.

Getting this effect means we need to stroke our circle rather than fill it, and
in particular we need to make sure the entire stroke is inside the circle, and
also that the stroke width exactly is exactly half the available space so that
it is always filled in completely – or least until we come to implement the
hollowing out in the second animation step.

Getting exactly half the available space means using a GeometryReader,


and because we’ll be adding colorful confetti later we’ll place that inside a
ZStack. So, replace the // circle here comment with this:

ZStack {
GeometryReader { proxy in
Circle()
.strokeBorder(color, lineWidth: proxy.size.width
/ 2)
.scaleEffect(circleSize)
}
}

Go ahead and run the app again and you should see our first animation step
is done. I know, I know, it’s pretty dull, but things get faster from here on!

The second animation step is where we need to hollow out our circle. This
is actually pretty straightforward because we’re using strokeBorder(): if
we reduce the lineWidth of our stroke it will automatically hollow out our
circle for us.
So, first add a new property to track how much of the stroke we want to
draw:

@State private var strokeMultiplier = 1.0

Second, modify your strokeBorder() modifier to multiply the line width by


that multiplier:

.strokeBorder(color, lineWidth: proxy.size.width / 2 *


strokeMultiplier)

And finally add another withAnimation() call after the previous one,
setting strokeMultiplier to a very small value:

withAnimation(.easeOut(duration: speed).delay(speed)) {
strokeMultiplier = 0.00001
}

Notice how I’ve made that delay by speed seconds, so that it waits until the
previous animation has completed before starting.

Now if you run the app you’ll see our animation is starting to come
together!
The third step of our animation is to make some colorful confetti fly out
from the edges of the circle. These have fairly precise movements: they
start near the circle edge, move out a small amount, then disappear by
shrinking away to nothing.

We can get a similar effect by adding three new properties to


ConfettiModifier: one to track whether the confetti should be visible, one
to track how far the confetti has moved, and a third to track the scale of the
confetti. We’ll measure movement relative to the size of our
GeometryReader, where 1.0 will mean “the very edge of the view”.
Add these three to ConfettiModifier now:

@State private var confettiIsHidden = true


@State private var confettiMovement = 0.7
@State private var confettiScale = 1.0

Those need to be animated to alternative values, namely false, 1.2, and


0.00001, which means adding two more withAnimation() calls alongside
the others. These don’t need to be put in any specific order because they
don’t depend on each other, but it’s generally a good idea to structure them
in the order they will execute.

Add these two:

withAnimation(.easeOut(duration: speed).delay(speed * 1.25))


{
confettiIsHidden = false
confettiMovement = 1.2
}

withAnimation(.easeOut(duration: speed).delay(speed * 2)) {


confettiScale = 0.00001
}

Again, note the careful use of delays to make sure these happen at exactly
the right time.

Drawing the confetti particles takes more work, and in fact this the most
complicated part of the whole transition because there are lots of very
precise modifiers. To make things easier to follow, I’ll break this down into
small parts, starting with something easy – add this inside the
GeometryReader, below the circle code:

ForEach(0..<15) { i in
Circle()
.fill(color)
// more modifiers to come
}
First, we need to give these circles a frame, otherwise they’ll all be huge.
We already added a size property, but if we pass our i loop variable through
sin() we’ll be able to modulate the size just a little – some confetti will be a
bit bigger and some a bit smaller.

Add this modifier now:

.frame(width: size + sin(Double(i)), height: size +


sin(Double(i)))

Next, we need to scale our confetti up or down depending on the value of


confettiScale, which is an easy one:

.scaleEffect(confettiScale)

Moving on, we need to move our confetti outwards as the effect animates.
This means pushing our circles outwards by our radius (half the proxy
width), multiplied by whatever is in confettiMovement. When the view is
first created that will put them at 70% of the radius because
confettiMovement has an initial value of 0.7, but our animation moves
them out to 120% of the radius so they fly outwards a good distance.

Now, we could do this using the following modifier:

.offset(x: proxy.size.width / 2 * confettiMovement)

That works, but’s a bit dull because every confetti piece would move
exactly the same distance. To make things a bit more varied, we’re going to
add some extra movement to every other piece, like this:

.offset(x: proxy.size.width / 2 * confettiMovement +


(i.isMultiple(of: 2) ? size : 0))

It’s a small difference, but when you see the final effect I think you’ll
appreciate it!
At this point we’ve moved all our confetti pieces out to the side of our
circle, but they are all on the same side. So, our next step is to rotate the
circles by 24 times i so they spread out across the entire circle, and we’re
using 24 because we have 15 circles being created – 24 x 15 is 360, which
covers all the angles.

Add this modifier now:

.rotationEffect(.degrees(24 * Double(i)))

We have just two more modifiers to go here, but the first one might be a bit
confusing: we’re going to use offset() again.

The reason for this should become clear if you break down what’s
happening:

1. When we create views inside a GeometryReader they are placed in


the top-left corner.
2. Our first offset() pushed our confetti views half way across the
GeometryReader horizontally.
3. Our rotationEffect() modifier caused those views to rotate around
their origin, which again is the top-left corner.
4. So our confetti views are now fanned out in a circle around the top-left
corner of our GeometryReader.
5. We want them to be centered, which means offsetting them again, this
time by half the width and height of our proxy.

Now, even though the confetti views are small, for real accuracy here we
need to subtract half our confetti size from these offsets, because we want to
make sure the particles are centered rather than positioned from their top-
left.

Add this modifier now:

.offset(x: (proxy.size.width - size) / 2, y:


(proxy.size.height - size) / 2)
Hopefully you can see why that’s needed, but if not try commenting out the
modifier once you’ve seen it working – when it’s not active it will be
immediately obvious what the problem is!

The final modifier is there to make sure our confetti stays hidden until we
say we’re ready for it, like this:

.opacity(confettiIsHidden ? 0 : 1)

That completes the confetti work – if everything has gone to plan your
finished code should look like this:

ForEach(0..<15) { i in
Circle()
.fill(color)
.frame(width: size + sin(Double(i)), height: size +
sin(Double(i)))
.scaleEffect(confettiScale)
.offset(x: proxy.size.width / 2 * confettiMovement +
(i.isMultiple(of: 2) ? size : 0))
.rotationEffect(.degrees(24 * Double(i)))
.offset(x: (proxy.size.width - size) / 2, y:
(proxy.size.height - size) / 2)
.opacity(confettiIsHidden ? 0 : 1)
}

Go ahead and try it out and see what you think! I think the default size of 3
looks about right for the smaller buttons, but the bigger one would probably
benefit from a custom size.
Anyway, we still have one last step to write in order to complete the Twitter
animation: our filled heart icon needs to spring out from the center,
bouncing to its final position.

Just like our other work, this means adding a property to track its
movement, placing a view somewhere in our layout, then animating it. Start
with this new property:

@State private var contentsScale = 0.00001


Again, using a value of 0.00001 rather than 0.0 avoids warnings from
SwiftUI.

The view for this is simple, because it’s just the content parameter that was
passed into the method, albeit with a scale effect so we can animate it. Add
this after the GeometryReader but still inside the ZStack:

content
.scaleEffect(contentsScale)

And now to finish off the whole effect we need to add one final
withAnimation() call next to the others. I said earlier that it’s a good idea
to structure your animation code in the order it executes, but it’s a bit
trickier here because we want a spring animation rather than a specific
duration. So, if I were you I’d place this in the middle of the four existing
animations – after the strokeMultiplier animation but before the
confettiIsHidden animation – because it has a delay that makes it fit into
that spot well.

Add this final code now:

withAnimation(.interpolatingSpring(stiffness: 50, damping:


5).delay(speed)) {
contentsScale = 1
}

Now run the project again and see what you think! It’s not identical to
Twitter’s animation, but it’s close enough that you’d be hard pressed to tell
the difference unless you zoomed in close and compared the two side by
side.
Yes, it did take quite a bit of code, but that’s only because there are
numerous overlapping animations taking place and I’ve tried to make it
fairly accurate to Twitter’s original. Hopefully it’s given you a good idea of
just how powerful SwiftUI’s transitions can be – with the ability to insert
completely custom views and animations, there’s really no limit to what
they can do.

Want to go further?
At this point you might well have had enough of transitions, but if you’re
keen to take this to the next level there is one small but important change
we can make: rather than forcing our transition to use a color, we can in fact
let it use any kind of shape style including gradients.

Honestly, this takes very little work to do, so give it a try!

First, we need to make the modifier generic over some kind of ShapeStyle:

struct ConfettiModifier<T: ShapeStyle>: ViewModifier {

Second, we need to change its color property to be of type T rather than


Color:

var color: T

And third we need to adjust the confetti() method inside our


AnyTransition extension so that it’s also generic over some kind of
ShapeStyle:

static func confetti<T: ShapeStyle>(color: T = .blue, size:


Double = 3.0) -> AnyTransition {

And that’s it – we can now transition using a much wider variety of styles.
For example, rather than using .red for the color, we can now use this:

.transition(.confetti(color: .red.gradient))

Or we could provide a wholly custom gradient for something really bright:

.transition(.confetti(color: .angularGradient(colors: [.red,


.yellow, .green, .blue, .purple, .red], center: .center,
startAngle: .zero, endAngle: .degrees(360))))
OceanofPDF.com
Chapter 3
Environment and Preferences
OceanofPDF.com
The environment
When you apply a modifier to a view, we are most of the time creating a
new view that wraps the original view to add some extra behavior or styling
– this is something I’ve said a few times now, but it matters!

This isn’t always the case. One common example is Text, where there are a
whole batch of modifiers we can apply directly to some text without
creating wrapped views, like this:

Text("Tap")
.font(.title)
.foregroundColor(.red)
.fontWeight(.black)
.onTapGesture {
print(type(of: self.body))
}

When you tap that text in the simulator, you’ll see its type is still just Text –
it silently just absorbs all the modifiers into itself. This is what allows us to
create complex text with various fonts and colors, then use operator
overloading to bring it all together into a single Text view.

You can see this for yourself in the SwiftUI interface file – search for
“internal var modifiers”, and you’ll see that all Text views store an array of
enum cases with associated values for their modifiers. (If you forgot the xed
command to use, and the fix in case you have problems, please see the
introduction to this book!)
Tip: This is exactly how Text views have different natural sizes when
changing the font – the font gets absorbed directly into the view, and is used
as part of its size calculations.

However, there’s a whole batch of modifiers that are more complex,


because they propagate changes downwards into child views. SwiftUI
doesn’t mark these out very clearly, or indeed at all, but you can see them in
action if we modified our code to this:

VStack {
Text("Tap")
}
.font(.title)
.onTapGesture {
print(type(of: self.body))
}

Now we’re applying title() to the VStack rather than directly to Text, and
the resulting type of our view will be very different – you’ll see
_EnvironmentKeyWritingModifier in there.

First, this dual behavior of font() is possible because of Swift’s approach to


overload resolution, which really boils down to “the most constrained
wins.” In the case of font(), if you look in the SwiftUI interface file you’ll
see func font is in there twice: once on Text, and once on View. Because
Text is the more constrained of the two (it’s one specific struct rather than a
whole group of structs that conform to a protocol), when we call font()
directly on a Text view we’ll get Text.font() rather than View.font().

Second, you can see exactly why _EnvironmentKeyWritingModifier


comes back in our type when you look at the font() method attached to
View rather than Text: it’s marked @inlinable, which means Swift has the
option of replacing a call to this View.font() method with the actual body of
the method – it’s able to copy the code from the method right into our view
at compile time. Obviously this requires Swift to have access to the code to
copy, which is why we can see exactly what SwiftUI is doing here:

@inlinable public func font(_ font: SwiftUI.Font?) -> some


SwiftUI.View {
return environment(\.font, font)
}

Tip: Search the SwiftUI interface for return environment(\ to see other
instances of this behavior.

So, we get two different results for font() depending on where it’s called:
for Text views it gets absorbed into an internal array of enum values, but
for all other views it is merely syntactic sugar that silently gets converted
into the following:

VStack {
Text("Tap")
}
.environment(\.font, .title)
.onTapGesture {
print(type(of: self.body))
}

The question is: why? Understanding the answer is the key to understanding
the environment in SwiftUI: this approach lets a view modifier flow
downwards through all the child views of our VStack, rather than just being
applied to a single view – we can adjust the font of everything inside the
VStack at once, even without the view internally realizing it was
happening.

The power of SwiftUI’s environment is that it flows downwards to every


view contained in wherever you apply it, but views only need to read a
value if they care about it.

To demonstrate this, we could make a simple TextField wrapper that


understands the concept of required fields by showing a small red asterisk
next to required fields.

This process takes at least two steps for every new environment key you
want to add. First, we make a new struct that conforms to the
EnvironmentKey protocol, which requires that provide a default value for
times when there is nothing in the environment:

struct FormElementIsRequiredKey: EnvironmentKey {


static var defaultValue = false
}

Second, we make an extension on EnvironmentValues telling the system


how to read and write our setting from the environment:

extension EnvironmentValues {
var required: Bool {
get { self[FormElementIsRequiredKey.self] }
set { self[FormElementIsRequiredKey.self] = newValue
}
}
}

Now we can go ahead and use it. For our TextField wrapper this means
using @Environment(\.required) to read the current required state in our
environment, then creating the view as normal:

struct RequirableTextField: View {


@Environment(\.required) var required
let title: String
@Binding var text: String

var body: some View {


HStack {
TextField(title, text: $text)

if required {
Image(systemName: "asterisk")
.imageScale(.small)
.foregroundColor(.red)
}
}
}
}

Now we can go ahead and the new RequirableTextField view anywhere


we want it, sending in the environment value for \.required as needed:

struct ContentView: View {


@State private var firstName = ""

var body: some View {


Form {
RequirableTextField(title: "First name", text:
$firstName)
.environment(\.required, true)
}
}
}

Earlier I showed you how the View.font() modifier is nothing more than a
wrapper around .environment(\.font), and honestly this is good practice
because it makes our code easier to read. In this case it would mean adding
a new View extension like this:

extension View {
func required(_ makeRequired: Bool = true) -> some View {
environment(\.required, makeRequired)
}
}
With that in place, we can now just called required(), like this:

RequirableTextField(title: "First name", text: $firstName)


.required()

Tip: As you can see, making the Boolean parameter have a default value of
true makes for much more natural use at the call site – saying required() is
the same as required(true).

Now, as we’re applying required() directly to our custom text field, the
environment approach might seem overkill – why not just pass it directly
into the RequirableTextField initializer?

Well, consider code like this:

struct ContentView: View {


@State private var firstName = ""
@State private var lastName = ""

@State private var makeRequired = false

var body: some View {


Form {
RequirableTextField(title: "First name", text:
$firstName)
RequirableTextField(title: "Last name", text:
$lastName)
Toggle("Make required", isOn:
$makeRequired.animation())
}
.required(makeRequired)
}
}

Now we’re making the whole form required at once, which flows the
environment key downwards into each text field automatically – they could
even have been in a different subview entirely, but would still have been
able to access the environment data.

I’d like you to try making a custom environment key now: can you create
one that stores a stroke width for all the shapes you want to draw?

Have a go and see how you get on! I’ll add my solution below.

First, we need to define the custom environment key and an extension on


EnvironmentValues:
struct StrokeWidthKey: EnvironmentKey {
static var defaultValue = 1.0
}

extension EnvironmentValues {
var strokeWidth: Double {
get { self[StrokeWidthKey.self] }
set { self[StrokeWidthKey.self] = newValue }
}
}

We could even add a View extension if you wanted:

extension View {
func strokeWidth(_ width: Double) -> some View {
environment(\.strokeWidth, width)
}
}

Now we can go ahead and use it with some drawing:

struct CirclesView: View {


@Environment(\.strokeWidth) var strokeWidth

var body: some View {


ForEach(0..<3) { _ in
Circle()
.stroke(.red, lineWidth: strokeWidth)
}
}
}

And then set that value at some higher point in the environment, like this:

struct ContentView: View {


@State private var sliderValue = 1.0

var body: some View {


VStack {
CirclesView()
Slider(value: $sliderValue, in: 1...10)
}
.strokeWidth(sliderValue)
}
}

Tip: We’ll be using StrokeWidthKey and CirclesView in the next chapter,


so stash your code somewhere safe and put mine in its place for easier
reference.

OceanofPDF.com
@Environment vs @EnvironmentObject
We can write any kind of data into environment keys, but the environment
never watches for changes in observable objects. This means if you try to
store a class in there then update it, the environment won’t know to update
any views that are watching.

In practice, this makes @Environment more suited to value type data, as


compared to @EnvironmentObject, which is specifically designed to store
class instances – the clue is right there in the name.

There are two compelling reasons why, where possible, you should aim to
use simple environment keys rather than passing in environment objects.

The first is simple: the EnvironmentKey protocol requires that we provide


a default value for any custom keys we create, whereas environment objects
can be missing entirely – and will trigger a hard crash in your code when
this happens. Yes, in theory this is the kind of thing we should spot in
development, but “should” in software development really means “might”,
so why leave things up to chance?

The second is a little more complex: when an observable object announces


that it has changed, SwiftUI makes all views that use it get refreshed. That
sounds straightforward, but it has an important impact for times when we
are publishing lots of data, only some of which views might care about.

To demonstrate this, we could add another custom environment key


alongside the StrokeWidthKey we made in the previous exercise, this time
to store the font that should be used for title text:

struct TitleFontKey: EnvironmentKey {


static var defaultValue = Font.custom("Georgia", size:
34)
}

extension EnvironmentValues {
var titleFont: Font {
get { self[TitleFontKey.self] }
set { self[TitleFontKey.self] = newValue }
}
}

Again, we could make a View extension to make it easier to access:

extension View {
func titleFont(_ font: Font) -> some View {
environment(\.titleFont, font)
}
}

Now we can send both values into the environment, like this:

struct ContentView: View {


@State private var sliderValue = 1.0
@State private var titleFont = Font.largeTitle

var body: some View {


VStack {
CirclesView()
Text("Hello, world!")
.font(titleFont)

Slider(value: $sliderValue, in: 1...10)

Button("Default Font") {
titleFont = .largeTitle
}

Button("Custom Font") {
titleFont = TitleFontKey.defaultValue
}
}
.strokeWidth(sliderValue)
.titleFont(titleFont)
}
}

That works great: the stroke width in CirclesView changes with the slider,
and the font style in ContentView changes as the buttons are pressed.
I’d like to add one more line of code so you can see what’s going on –
modify the body property of CirclesView to this:

var body: some View {


print("In CirclesView.body")

return ForEach(0..<3) { _ in
Circle()
.stroke(.red, lineWidth: strokeWidth)
}
}

That prints out a message every time body is called, which is helpful here
because it lets us run the project back and see the message being printed
again and again as we drag around the slider. But, importantly, it won’t be
printed when pressing the buttons: those also change the environment, but
SwiftUI knows CirclesView doesn’t actually use the titleFont environment
key so it doesn’t need to reinvoke body.

So, that’s how environment keys work, but what happens if we had used an
environment object here instead? To find out, we would start by making
some kind of class to store our two values together as theme data:

class ThemeManager: ObservableObject {


@Published var strokeWidth = 1.0
@Published var titleFont = TitleFontKey.defaultValue
}

In CirclesView we aren’t going to watch for precise keys, but instead we’ll
expect to receive a whole environment object of theme data:

struct CirclesView: View {


@EnvironmentObject var theme: ThemeManager

var body: some View {


print("In CirclesView.body")

return ForEach(0..<3) { _ in
Circle()
.stroke(.red, lineWidth: theme.strokeWidth)
}
}
}

And now in ContentView we would make an instance of that using


@StateObject, the inject it into the environment:

struct ContentView: View {


@StateObject private var theme = ThemeManager()

var body: some View {


VStack {
CirclesView()
Text("Hello, world!")
.font(theme.titleFont)

Slider(value: $theme.strokeWidth, in: 1...10)

Button("Default Font") {
theme.titleFont = .largeTitle
}

Button("Custom Font") {
theme.titleFont = TitleFontKey.defaultValue
}
}
.environmentObject(theme)
}
}

When you run the project now you’ll see an important difference: every
time titleFont is set our "In CirclesView.body" message is printed, even
though the CirclesView doesn’t care about it. This generates significantly
more work for our views, with no actual benefit at all – the body property is
even reinvoked if titleFont is set to its existing value!

From a SwiftUI perspective this behavior makes absolute sense: the class is
sending a change notification, so SwiftUI doesn’t really have a way of
checking exactly what changed inside the view – maybe strokeWidth also
changed as a result of titleFont changing.

Try to think of it like this: every time you make a view use an
@ObservedObject or an @EnvironmentObject, you are effectively
creating a dependency on that data. It doesn’t matter if the actual body
property doesn’t change as one of the @Published values changes, because
if you recall Swift doesn’t perform tree diffing.

So, when you bring together the extra safety of always having a default
value and the extra performance of skipping unnecessary work, I hope you
can see why using environment keys are preferable where possible!

Of course, as with many things there is a middle ground: you can store your
data as a shared theme, then expose it using environment key. This works
when you want to be able to read and write the data in one object, but
you’re still keen to avoid surprise crashes from missing data.

Using this approach we might abstract our theme information into a


protocol, like this:

protocol Theme {
var strokeWidth: Double { get set }
var titleFont: Font { get set }
}

We can then make structs adopting that protocol, based on whatever theme
requirements we have:

struct DefaultTheme: Theme {


var strokeWidth = 1.0
var titleFont = TitleFontKey.defaultValue
}

Next, we’re going to wrap that in a class that is able to publish changes as
the theme updates. Apps only ever have one theme active at a time, so we
could implement this as a singleton:

class ThemeManager: ObservableObject {


@Published var activeTheme: any Theme = DefaultTheme()

static var shared = ThemeManager()


private init() { }
}

Now we can expose all that to the environment, focusing only on the
internal Theme struct:

struct ThemeKey: EnvironmentKey {


static var defaultValue: any Theme =
ThemeManager.shared.activeTheme
}

extension EnvironmentValues {
var theme: any Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}

We need to expose this to our views in a natural way, but we can’t just make
a simple View extension this time because we need to actually watch the
ThemeManager for changes. We don’t own this theme manager object
because we’re using a singleton, so a simple @ObservedObject is the right
choice:

struct ThemeModifier: ViewModifier {


@ObservedObject var themeManager = ThemeManager.shared

func body(content: Content) -> some View {


content.environment(\.theme,
themeManager.activeTheme)
}
}

And now we can wrap it up in a View extension for easier use:

extension View {
func themed() -> some View {
modifier(ThemeModifier())
}
}

With that all done, we can switch ContentView over to using


@ObservedObject for its theme manager, so it’s able to read and write
data:

@ObservedObject var theme = ThemeManager.shared

That means using theme.activeTheme everywhere, because now we want


to modify the theme struct directly. Once that’s done, add a themed()
modifier to ContentView so the active theme is sent into the environment.

The interesting part is in CirclesView, where now we can watch the


environment key rather than using @EnvironmentObject:

struct CirclesView: View {


@Environment(\.theme) var theme

var body: some View {


print("In CirclesView.body")
return ForEach(0..<3) { _ in
Circle()
.stroke(.red, lineWidth: theme.strokeWidth)
}
}
}

You’ll see that still triggers when the stroke width changes or when the font
changes, but doesn’t change when the font is changed to its existing value –
SwiftUI is smart enough to discard that.

But we can get even better: because @Environment uses a key path rather
than always accepting a whole observable object like
@EnvironmentObject does, we can actually tell SwiftUI we want access
to only part of the theme, which means only that part of it will be a
dependency for this view:

struct CirclesView: View {


@Environment(\.theme.strokeWidth) var strokeWidth

var body: some View {


print("In CirclesView.body")

return ForEach(0..<3) { _ in
Circle()
.stroke(.red, lineWidth: strokeWidth)
}
}
}

And now SwiftUI will only reinvoke the body property when that one
specific value changes – we’re back to where we were originally in terms of
performance, while also benefiting from having the environment object
available elsewhere if needed.

This approach obviously takes more work, but it gives us the ability to
synchronize changes everywhere with some kind of theme control panel,
but also means we get the guaranteed safety of using environment keys
rather than environment objects.
OceanofPDF.com
Overriding the environment
As we’ve seen, SwiftUI’s environment flows down through our views,
which allows parents to set some data for their children that can then be
read out as needed.

For example, we might create a simple welcome view for our app:

struct WelcomeView: View {


var body: some View {
VStack {
Image(systemName: "sun.max")
Text("Welcome!")
}
}
}

We could then use it somewhere else, adjusting its font as needed:

struct ContentView: View {


var body: some View {
WelcomeView()
.font(.largeTitle)
}
}

That works well enough, but what if we wanted to customize the font for
the SF Symbols image we’re using? Maybe our designer wants that to be
much bolder, so the image stands out more clearly.
We could try adding a font() modifier to it, like this:

Image(systemName: "sun.max")
.font(.largeTitle.weight(.black))

But now we’ve introduce a problem: that font will override whatever comes
in from the environment, so if we later changed ContentView to use
.font(.headline) instead we’ll have a mismatch – our symbol will use a
large title, whereas the text below will use a headline font.

In this situation, the second font() modifier isn’t really what we meant – we
don’t want to force a wholly new font, we just want to bold up whatever
font we were asked to use. SwiftUI has a wholly separate modifier for this,
called transformEnvironment(): it is able to transform any one specific
environment key somehow, and passes us an inout reference of whatever
the current value is.

So, a much better solution is to write this:

Image(systemName: "sun.max")
.transformEnvironment(\.font) { font in
font = font?.weight(.black)
}

This approach is really similar to the transaction() modifier: any kind of


font applied to this image will automatically be transformed using our
custom closure.

OceanofPDF.com
Preferences
We’ve seen how SwiftUI’s environment flows downwards, but sometimes
you want information to flow upwards too – to send data from a child view
upwards to its ancestor views. In SwiftUI this is done using preferences,
and the canonical example of this in action is the navigationTitle()
modifier:

NavigationStack {
VStack {
Image(systemName: "sun.max")
Text("Welcome!")
}
.navigationTitle("MyApp")
}

In that code, the VStack describes its navigation title, but that data flows
upwards to the NavigationStack containing it. This makes sense from a UI
perspective, because of course the navigation stack can push and pop views
freely, and it needs to be able to update itself to show the titles of each of
those views.
Just like the environment, preferences flow upwards continuously rather
than just stopping at the first container. In our simple ContentView code,
this means we can put navigationTitle() on a view inside the VStack, if we
wanted:

NavigationStack {
VStack {
Image(systemName: "sun.max")
Text("Welcome!")
.navigationTitle("MyApp")
}
}

That will give exactly the same result – the title preference just flows
upwards until it’s used.

Of course, that might set off some alarm bells: what happens if we have
multiple navigation titles? We can find out easily:

NavigationStack {
VStack {
Image(systemName: "sun.max")
.navigationTitle("Image")

Text("Welcome!")
.navigationTitle("Text")
}
.navigationTitle("VStack")
}

As you’ll see when that code runs, the navigation view just picks the first
one it finds – it will show “Image” as its title.

This kind of preference system is open to us to use as needed, although you


should keep in mind that having data flowing freely both downwards and
upwards might result in spaghetti code.

Our own preferences work in a similar way to navigationTitle():

Any view can add them.


They flow upwards through our views.
We need to choose one value to use.

It’s not identical, because we get to decide how the single value is selected
– maybe we choose the first one like navigationTitle(), or maybe we
combine values together somehow.

Enough talk: let’s try and implement a preference key ourselves, which will
let a child view report its size upwards to containers.

The first step is to create a new struct that conforms to the PreferenceKey
protocol. This requires us to provide a default value for the preference, just
like EnvironmentKey, but now we also need to provide a reducer function
– code that chooses which value to use, when several come in.

In our case, we want to track the width of some view, but if we get multiple
widths coming in we’ll just track the last one:

struct WidthPreferenceKey: PreferenceKey {


static let defaultValue = 0.0

static func reduce(value: inout Double, nextValue: () ->


Double) {
value = nextValue()
}
}

Now we can make a view that sets a value for that preference, like this:

struct SizingView: View {


@State private var width = 50.0

var body: some View {


Color.red
.frame(width: width, height: 100)
.onTapGesture {
width = Double.random(in: 50...160)
}
.preference(key: WidthPreferenceKey.self, value:
width)
}
}

That changes its width whenever it’s tapped, which is helpful so you can
see what’s happening.

Finally, we need to place that view somehow, and watch for changes to its
preferences. This watching is done using the onPreferenceChange()
modifier, which runs code of our choosing whenever some specific
preference data changes, like this:

struct ContentView: View {


@State private var width = 0.0

var body: some View {


NavigationStack {
VStack {
SizingView()
}
.onPreferenceChange(WidthPreferenceKey.self) {
width = $0 }
.navigationTitle("Width: \(width)")
}
}
}

That code works great: you can run the project, then tap the red square to
see its width adjust and be reflected in the navigation view.
Of course, rather than just displaying the value, we can put it to use
somehow. For example, we could use the value to set the widths of other,
unrelated views, like this:

struct ContentView: View {


@State private var width = 0.0

var body: some View {


NavigationStack {
VStack {
SizingView()

Text("100%")
.frame(width: width)
.background(.red)

Text("150%")
.frame(width: width * 1.5)
.background(.green)

Text("200%")
.frame(width: width * 2)
.background(.blue)
}
.onPreferenceChange(WidthPreferenceKey.self) {
width = $0 }
.navigationTitle("Width: \(width)")
}
}
}

Or we can add multiple resizing views just fine, thanks to the reducer we
wrote:

VStack {
SizingView()
SizingView()
SizingView()
}
.onPreferenceChange(WidthPreferenceKey.self) { width = $0 }
.navigationTitle("Width: \(width)")

We made the reduce() method always use the final value it’s given, so
when that code runs only the third SizingView will have its width reflected
in the navigation title. Of course, it doesn’t have to be that way: we could
make our reducer sum the preferences instead, like this:

static func reduce(value: inout Double, nextValue: () ->


Double) {
value += nextValue()
}

Now the final value for our preference will be the total of all three widths,
and will automatically adapt when any of the three changes.
Even better, if you wanted to mimic the “first preference only” approach of
navigationTitle(), it takes literally zero code:

static func reduce(value: inout Double, nextValue: () ->


Double) {

Because that never calls nextValue(), it means “you give me the first value
and the next one, but I don’t care – do nothing with them.”

OceanofPDF.com
Anchor preferences
You’ve seen how preferences allow us to send data from a child view up to
its ancestors, and how it’s up to us to decide what to do – if anything – with
that information. Well, SwiftUI provides a handful of specialized preference
modifiers that are specifically aimed at making it easier to share sizing data
and make use of it easily.

To demonstrate this, we’re going to create a simplified copy of part of the


Airbnb app: at the top of the app’s Explore tab there are some options you
can select, and whichever one is selected shows a line underneath. Sure, we
could do this by giving every icon an underline that gets shown or hidden
depending on the selection status, but the Airbnb app uses just one line that
moves around and resizes based on the category selection.

Solving this problem will demonstrate not only how the more advanced
preferences work, but you’ll also see how preference data can be more
complex – it’s definitely a fun problem to tackle.
First, we can define what one category in our app looks like. We’ll provide
two properties here: one for the identifier, which will be things like
“Beach”, “Golfing”, or “Tropical”, and one for the SF Symbol that will be
used to add an icon. We’ll make this struct be Identifiable so we can loop
over arrays of them in SwiftUI, and also Equatable so we can compare one
category to another. Start with this code:

struct Category: Identifiable, Equatable {


let id: String
let symbol: String
}

Next will be the preference data we want to share. Previously this was a
simple number, but this time I want to share a custom struct containing two
pieces of information: the category that it refers to, and an anchor. Anchors
are opaque geometry stores, which means they can store a reference to
some position and size on the screen but we can’t read it out directly
because it wouldn’t be useful. Fortunately, SwiftUI knows how to read
them for us, and in doing so automatically resolves the anchor’s geometry
into coordinates that are useful for us.

If that sounds fuzzy, relax; it will make sense in a moment. For now, add
this second struct to store the category and an anchor storing its geometry
data:

struct CategoryPreference: Equatable {


let category: Category
let anchor: Anchor<CGRect>
}

Next we’re going to add a third struct that conforms to PreferenceKey.


This is similar to the preference key we wrote for SizingView, except now
we’re going to send back an array of data all at once – we’ll collect
whatever array of CategoryPreference values we’re given and add them
into a collection of all such values
struct CategoryPreferenceKey: PreferenceKey {
static let defaultValue = [CategoryPreference]()

static func reduce(value: inout [CategoryPreference],


nextValue: () -> [CategoryPreference]) {
value.append(contentsOf: nextValue())
}
}

That completes all the underlying data we need for our work, so now we
need two SwiftUI views to render it all: one to handle a single category
button on the screen, and one to render all the buttons plus an underline and
whatever else we want.

First, the category button. This will be given the category to show, along
with a binding that will store which category is currently selected. This
binding is important, as it allows our category button to adjust external state
– to say “my category was selected” when it is tapped.

It looks like this:

struct CategoryButton: View {


var category: Category
@Binding var selection: Category?

var body: some View {


Button {
withAnimation {
selection = category
}
} label: {
VStack {
Image(systemName: category.symbol)
Text(category.id)
}
}
.buttonStyle(.plain)
.accessibilityElement()
.accessibilityLabel(category.id)
}
}
We’ll come back to that in a moment, but first I want to create an initial
version of ContentView, which has the job of showing several category
buttons in a HStack. We’ll add more to this shortly, so I’ll also add a
VStack where we can add extra things later, but the important part for now
is that it has an array of categories and a single piece of state that stores the
selected category – that’s what gets passed into CategoryButton as a
binding.

Add this ContentView code now:

struct ContentView: View {


@State private var selectedCategory: Category?

let categories = [
Category(id: "Arctic", symbol: "snowflake"),
Category(id: "Beach", symbol: "beach.umbrella"),
Category(id: "Shared Homes", symbol: "house")
]

var body: some View {


VStack {
HStack(spacing: 20) {
ForEach(categories) { category in
CategoryButton(category: category,
selection: $selectedCategory)
}
}
}
}
}

At this point we have something fairly unimpressive: we’ve created the data
model for our preferences, and also the views to show categories, but we
haven’t actually linked them together. To do this means introducing two
new modifiers: one to send the value upwards as a preference, and one to
read the array of those values back out once they have been through our
reducer function.

Now, previously we used the preference() modifier for sending a


preference up to ancestors, but here we’re going to use a specialized
preference specifically for working with geometry. This modifier is called
anchorPreference() and takes three parameters:

1. The preference key you want to send. For us that will be


CategoryPreferenceKey.self, just like we’d have with a simple
preference.
2. What part of the geometry we want to send. Perhaps you might want to
send just the leading edge, for example, but here we’re going to
request .bounds to get the whole frame wrapped up.
3. A transformation function that accepts an Anchor containing our
bounds, and needs to convert that into whatever input your preference
key expects. If you remember, we made ours work with
CategoryPreference arrays, so we’ll convert the anchor into an array
containing one CategoryPreference instance.

All this is done by adding this single modifier to CategoryButton, below


accessibilityLabel():

.anchorPreference(key: CategoryPreferenceKey.self, value:


.bounds, transform: { [CategoryPreference(category: category,
anchor: $0)] })

So, we’re telling SwiftUI we want to express an anchor preference for the
CategoryPreferenceKey, that it needs to use the bounds of the button we
attached the preference to, and we want to receive that bounds as an anchor
and place it inside a CategoryPreference object.

That sets the category preference key for each button, but we still need to
add code to read the preference and act on it. This is where the second new
modifier comes in: rather than just using onPreferenceChange() to read
values coming in, we’re going to use overlayPreferenceValue(). This has
the job of reading preferences and converting them into an overlay – it’s
effectively a combination of onPreferenceChange() and overlay() in one
modifier, which helps make our code simpler.

This is where SwiftUI performs a beautiful trick. Remember earlier when I


said that anchors are opaque geometry stores? That means they contain
geometry data, but don’t let us read that data back out because it wouldn’t
have any meaning – just knowing X:35 Y:58 by itself doesn’t mean
anything unless you know exactly what coordinate space you’re coming
from and going to, for example.

SwiftUI solves this brilliantly using GeometryReader: if we use one of


these anywhere in our view layout, we can pass an anchor to its proxy
object and have it send back a relevant frame for us as a CGRect. That
means the GeometryProxy figures out how to convert the original frame
into whatever coordinate space our GeometryProxy is working in – it takes
away all the hassle of figuring out where the two are in relation to each
other, and just sends us back the bit we actually care about: a CGRect
telling us the frame we actually want to use to refer to the original bounds
we set with anchorPreference().

Enough chat, let’s put the code in place so you can see exactly how it
works. Please add this modifier to the VStack in ContentView:

.overlayPreferenceValue(CategoryPreferenceKey.self) {
preferences in
GeometryReader { proxy in
if let selected = preferences.first(where: {
$0.category == selectedCategory }) {
let frame = proxy[selected.anchor]

Rectangle()
.fill(.black)
.frame(width: frame.width, height: 2)
.position(x: frame.midX, y: frame.maxY)
}
}
}

I’m going to break that down and walk through every line, but first I want
you to build and run the code so you can see what it does – you should find
you can now tap on any button to see a line animate between them,
automatically moving and resizing so that it always underlines each button
correctly. It’s a great effect, although I should repeat that it’s from Airbnb
rather than something I came up with by myself!
Anyway, let’s break down the code:

1. We use overlayPreferenceValue() to specify we want to read in a


particular preference key and convert it into an overlay. As a reminder,
that means we want SwiftUI to place some kind of view over our
VStack, and we’ll use the preference keys to figure out what that view
should be.
2. Inside our overlay we use a GeometryReader so we can evaluate our
geometry somehow. This will automatically expand to fill all the
available space, which is fine as an overlay because it will naturally fit
the same space as the view it overlays.
3. We look through all the category preferences for whichever one
matches the selected category.
4. If we find a match, we pass the selected preference’s anchor into our
geometry proxy, which performs the conversion into a finished frame
representing where that anchor is in the coordinates of our current
GeometryReader.
5. Now we have a real frame, we draw a black rectangle using the width
of the frame so it matches the width of the thing we want to underline,
giving it a 2-point height so it looks like a thick line.
6. We want to position that rectangle so that its center lies at the middle
bottom of our frame. If you prefer offset() rather than position() you
should use frame.minX because we’re providing a relative movement
rather than trying to center the view in some exact location.

The real magic in that code is proxy[selected.anchor], which takes care of


all the geometry conversion for us – hopefully you can see now why
Anchor is opaque!
The real power of this approach is that the rest of our UI has no idea that
preferences are being used at all, which means if we added more to the
VStack it would Just Work™ without any special extra work from us.

For example, we could add a List below the buttons HStack, showing all
the categories and letting the user select them that way:

List(categories, id: \.id) { category in


HStack {
Button(category.id) {
withAnimation {
selectedCategory = category
}
}

if selectedCategory == category {
Spacer()

Image(systemName: "checkmark")
}
}
}

That adjusts selectedCategory when each row in the list is tapped, which
triggers body being reinvoked and will update the overlay preference. If
you try it out, you’ll see SwiftUI automatically moves the underline
rectangle around, just like we had when tapping the buttons directly.
We can also show the selected category by adding another view to the
VStack, below the List:

if let selectedCategory {
Text("Selected: \(selectedCategory.id)")
}

Again, that will automatically update no matter how the selected category
changes – we get both the underline and text updating smoothly, all thanks
to SwiftUI’s preferences system.
So, even though I think it takes a little understanding at first, I hope you can
appreciate the power being exposed here: we’re able to send complex data
to parent views, convert coordinate spaces, and more, all to quite elegantly
achieve a very specific result.

OceanofPDF.com
Chapter 4
Custom Layouts
OceanofPDF.com
Adaptive layouts
Switching between layouts can be a complicated beast in SwiftUI, because
as you saw earlier it’s easy to get stuck with view builder conditional
content views that toss away your state, destroy your platform views, and
screw up any animations. However, if done right it’s possible to move
smoothly from one container view to another, e.g. from a HStack to a
VStack – not only does it avoid the aforementioned problems, but SwiftUI
will even animate between the two layouts for us.

The key here is SwiftUI’s AnyLayout view, which is a type-erased wrapper


around a container capable of performing layout. You might see the “Any”
in the name and imagine this should be avoided much like AnyView, but
relax: this wrapper is specifically designed to let us dynamically switch
between different layouts, e.g. horizontal and vertical, without destroying
any state along the way.

You’re already familiar with HStack, VStack, and ZStack, but SwiftUI
provides special alternatives to those called HStackLayout, VStackLayout
and ZStackLayout – all of which are designed to work with AnyLayout,
so we can swap between them freely and have SwiftUI rearrange our views
automatically. These have different names not because Apple is changing
their mind or because they plan to remove the old names, but simply for
compiler reasons: if Apple made the original stack types work with
AnyLayout it would cause our code to build significantly slower as the
compiler tried to figure out which implementation we meant in our code.

Anyway, let’s get into some code, so you can see all this in practice. First, I
want to make a simple test view that we can use repeatedly, so create this
new SwiftUI view now:

struct ExampleView: View {


@State private var counter = 0
let color: Color

var body: some View {


Button {
counter += 1
} label: {
RoundedRectangle(cornerRadius: 10)
.fill(color)
.overlay(
Text(String(counter))
.foregroundColor(.white)
.font(.largeTitle)
)
}
.frame(width: 100, height: 100)
.rotationEffect(.degrees(.random(in: -20...20)))
}
}

Those views will provide enough variation that several of them will look
suitably different on the screen, but they also have their own individual
state – each view will have its own counter that increments when tapped.

What is more interesting is how we place a bunch of those on the screen at


the same time. I already mentioned that AnyLayout has the job of
wrapping another specific layout type, so in order to cycle through various
layouts we’re going to create an array of them and cycle through them to
use one at a time.

Start by adding this as a property to ContentView:

let layouts = [AnyLayout(VStackLayout()),


AnyLayout(HStackLayout()), AnyLayout(ZStackLayout())]

So, that contains three different ways of laying out views: vertical,
horizontal, and depth. Which one we use will depend on another property
that points to an index in that array, so add this next:

@State private var currentLayout = 0

And our final property will be responsible for returning one layer from the
array, based on the value of currentLayout:
var layout: AnyLayout {
layouts[currentLayout]
}

For the body of our view, we’re going to create a VStack with four things
inside:

1. A button that goes to the next layout when pressed.


2. Whatever layout is currently active, containing four instances of
ExampleView inside.
3. A spacer above the layout…
4. …and another one below the layout, so it’s centered neatly.

We’ll also make the VStack take up all available space, and have a dark
background color so our example views stand out clearly. Replace the
current body property of ContentView with this:

VStack {
Spacer()

layout {
ExampleView(color: .red)
ExampleView(color: .green)
ExampleView(color: .blue)
ExampleView(color: .orange)
}

Spacer()

Button("Change Layout") {
withAnimation {
currentLayout += 1

if currentLayout == layouts.count {
currentLayout = 0
}
}
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
That’s already enough to demonstrate the power of AnyLayout: when you
run the app now you’ll see our four example views rearrange themselves
smoothly every time the button is tapped. More importantly, as we switch
between layouts each of the views will retain their state – you can tap on
any of them to increase their counter, and those values will be preserved
between layout changes.

That’s neat, right? But SwiftUI goes two steps further. First, alongside
VStackLayout and others we can also use GridLayout to lay out views in
a grid. To try it out, first modify your layouts array to this:

let layouts = [AnyLayout(VStackLayout()),


AnyLayout(HStackLayout()), AnyLayout(ZStackLayout()),
AnyLayout(GridLayout())]

Second, modify your layout code to this:

layout {
GridRow {
ExampleView(color: .red)
ExampleView(color: .green)
}

GridRow {
ExampleView(color: .blue)
ExampleView(color: .orange)
}
}

That splits up our views into two rows, so if you run the app now you’ll see
we get a 2x2 grid layout alongside the other three.
If you pause to think about it, something neat is happening here: three of
our four layouts aren’t grids, and yet SwiftUI won’t bat an eyelid about
them containing GridRow. That particular view only does anything when
contained inside a grid, and for all other layouts it behaves identically to a
Group.

So now we’re cycling between four completely different layouts for our
views, with SwiftUI preserving animation, state, and platform views
throughout. But I said SwiftUI goes two steps further, so what’s the second
one?

Well, all four of these layout types conform to an underlying protocol


named simply Layout. Not only can we create our own types that conform
to Layout, but when doing so those custom layouts can be used with
AnyLayout too – you can move smoothly from the built-in layouts to ones
you built yourselves.

Let’s look at that next…

OceanofPDF.com
Implementing a radial layout
The first custom layout we’re going to build is by no coincidence also the
easiest, and is designed to place its views in a circle. To build this layout
you’ll need to meet the two most important methods in the Layout
protocol:

The sizeThatFits() method is given a proposed size for our layout,


along with all the subviews that are inside, and must return the actual
size our container wants to have. (Remember the three-step layout
process: step 1 is the parent proposing a size, step 2 is the child
deciding on its actual size, and step 3 is the parent placing the child
based on that size.)
The placeSubviews() method is given the actual CGRect the parent
has allocated for the child, which will match the size we returned from
sizeThatFits(). It will also be given the original proposed size,
because it’s possible the parent proposed multiple sizes before one was
finally chosen, and we’ll also get the subviews ready to place.

There’s one more thing both these methods accept, and we’ll be covering it
in a later chapter: a cache, so that you’re doing slow calculations to create
your layout you can skip doing the work more often than is necessary.

Anyway, let’s begin by creating a new struct with stubs for the two required
methods:

struct RadialLayout: Layout {


func sizeThatFits(proposal: ProposedViewSize, subviews:
Subviews, cache: inout Void) -> CGSize {

func placeSubviews(in bounds: CGRect, proposal:


ProposedViewSize, subviews: Subviews, cache: inout Void) {

}
}
Swift will complain because we aren’t returning something from
sizeThatFits(), but that’s okay: that entire method is just one line of code,
so we might as well fill it in now. Put this inside sizeThatFits():

proposal.replacingUnspecifiedDimensions()

Before I explain what it does, I should make one thing clear: although that
is the correct and only line of code for this radial layout, many if not most
of the custom layouts you will make in the future will need significantly
more logic to work. So, if you were thinking “wow, these custom layouts
are easy,” you shouldn’t get your hopes up too much!

SwiftUI calls sizeThatFits() with a proposed view size and all our
subviews. Very often you’ll want to query those subviews to ask how much
space they want before deciding how much the whole container wants, but
for a radial layout we don’t care – we just want to take all the space that
was offered to us.

The proposed view size we receive into sizeThatFits() comes from our
container’s parent, and it might call the method several times to get a full
understanding of what our layout is happy to use. Sometimes we’ll be
passed in a specific size the parent wants to give us, e.g. 300x200,
sometimes we’ll be passed only part of a size, e.g. we can have 300 points
horizontally and no vertical limit, and sometimes we’ll be passed one of
three special values:

Unspecified: “I don’t have a particular size in mind for you, so tell me


your ideal size.”
Infinity: “You can have as much space as you want, so what’s the most
you’ll take?”
Zero: “Space is really tight, so what’s the least you can work with?”

The point is that this proposal isn’t just a simple width and height, because
even without the infinite and zero values it’s still possible to get nil for
either or both width and height.
What replacingUnspecifiedDimensions() does is return a fully-formed
CGSize with no optional width or height – both values will have something
meaningful in there, with nil values being replaced by a default of 10. So,
our sizeThatFits() method effectively means “I’ll take all the space you
offered, but if you didn’t specify something I’ll ask for just 10 points.” Yes,
10 points isn’t really enough for a good layout, but there isn’t really a good
alternative here – we can’t really create a circular layout unless we know
the amount of space available to us.

This situation might seem familiar to you: back in the chapter on layout
neutrality I mentioned that Color.red inside a scroll view would be given a
nominal 10-point height because it wouldn’t make sense to use anything
else. Hopefully now you can see where that value of 10 comes from –
internally the Color is using replacingUnspecifiedDimensions() to
replace its nil inputs with 10.

That’s enough talk, so let’s move on to the placeSubviews() method. This


is more challenging, because it’s our job to figure out how to place all our
subviews in a circle. It takes a small amount of trigonometry, but hopefully
it won’t challenge you too much!

To begin with, we need to calculate the radius of the bounds we’re working
with, then divide 360 degrees by the number of subviews so we can see
how many degrees of our circle should be allocated to each view.

Start by adding these two lines to placeSubviews():

let radius = min(bounds.size.width, bounds.size.height) / 2


let angle = Angle.degrees(360 /
Double(subviews.count)).radians

Note: The Angle.degrees(…).radians part is intentional – it’s a little easier


to think about 360 degrees in a circle, but we need the final “angle per
subview” value as radians.

Now we know the radius of our circle and how many degrees of our circle
should be allocate to each view, we can start to place each view. This means
going over all the views in subviews, figuring out how much space it wants,
then placing it on our circle as appropriate.

Add this loop underneath the previous lines:

for (index, subview) in subviews.enumerated() {


// more code to come
}

If we place every view at the very edge of our circle, we’ll hit a problem
because a large part of each view will lie outside the circle’s perimeter. For
example, if the radial layout goes horizontally edge to edge on the screen,
the views on the left and right edge will both be hanging half way off the
screen, which is a poor experience.

Rather than placing our views at the very edge of our circle, we’ll instead
ask each view how much space it wants, then subtract half that from the
position it would otherwise have been given – rather than “edge of the
circle” it’s “edge of the circle minus half the view’s size” so it’s fully inside
the circle.

So, the first line we’ll add inside our loop will be to ask each subview for its
ideal size, like this:

let viewSize = subview.sizeThatFits(.unspecified)

Now for the trigonometry:

We know the angle that needs to be allocated to each view to split up


our circle fairly.
If we multiply that angle by our loop index, we’ll find the angle where
this particular view should be placed.
Calculating the cosine of that angle will tell us how much X movement
should happen to reach that position, in a range of -1 through +1.
We can then multiply that by our radius to get that X movement in the
range of -radius to +radius.
Calculating the sine of the angle will tell us how much Y movement
should happen to reach the view’s position, and again we will multiply
that by our radius to get the actual location.

There are two bonus complications here:

1. Like I said, we need to subtract half the width and height of the view
from the final X and Y positions.
2. SwiftUI considers 0 radians to be directly to the right, whereas users
will expect 0 to be directly up. We can fix this by subtracting half of pi
from our angle before putting it through sin() and cos().

Add these two lines to placeSubviews() below the previous code:

let xPos = cos(angle * Double(index) - .pi / 2) * (radius -


viewSize.width / 2)
let yPos = sin(angle * Double(index) - .pi / 2) * (radius -
viewSize.height / 2)

At this point we know where to place this view inside our container, but
there are still two more small complications.

First, we’ve calculated where the view should be by multiplying its angle
by our container’s radius, with a little extra logic in there for handling 0
degrees being up and ensuring views always lie inside the circle. What we
haven’t done is offset this position so that it’s relative to the center of our
container, which means right now those xPos and yPos values are offsets
from the top-left corner of our container.

To fix this, we’re going to convert our two position values into a CGPoint,
and while doing so add in the midX and midY of our bounds, so that our
circle is centered on the center of our container as you’d expect.

Add this line to the method next:

let point = CGPoint(x: bounds.midX + xPos, y: bounds.midY +


yPos)
The second small complication comes by asking a question: when placing
the subview at point, are we saying the top-leading of the subview should
be there? Or the center of the subview? Or something else? If we don’t
specify SwiftUI will assume we mean top leading, but we’ve actually
calculated the center position so we need to say that.

Attached to this is a chance to tell the subview exactly how much space we
have allocated to it. Remember, children choose their final sizes and parents
must respect that, so this space allocation is just another proposal – the
child can do what it likes. We don’t care how much size the view takes up,
so we’ll use .unspecified here.

Add this final line of code to end the loop:

subview.place(at: point, anchor: .center, proposal:


.unspecified)

So, that asks each view for its ideal size, then uses it to place it inside our
circle’s perimeter. The whole thing isn’t a lot of actual code, but it does take
quite a bit of explaining because there’s a lot of power here.

We’ll look into these layouts more in coming chapters, but first I want you
to try it out in a SwiftUI view – try replacing your ContentView struct with
this one:

struct ContentView: View {


@State private var count = 16

var body: some View {


RadialLayout {
ForEach(0..<count, id: \.self) { _ in
Circle()
.frame(width: 32, height: 32)
}
}
.safeAreaInset(edge: .bottom) {
Stepper("Count: \(count)", value:
$count.animation(), in: 0...36)
.padding()
}
}
}

That creates lots of circles in a radial layout, but adds a stepper to increment
or decrement the circle count using animation. Try running it now – it’s a
simple view, and again our radial layout code is pretty short, but I think
you’ll be impressed by how good the results are!

OceanofPDF.com
Implementing an equal width layout
The next custom layout we’re going to look at will create a HStack where
each view is allocated exactly the same width.

In the “Fixing view sizes” chapter I showed you how we could make two
views in the same HStack have the same height by using
.frame(maxHeight: .infinity) on the views and .fixedSize(horizontal:
false, vertical: true) on the HStack, but this is different: here we want all
the views to have the same width, which is trickier to solve.

Now, you might think one solution is to give each child view
.frame(maxWidth: .infinity), which will cause each view to resize freely
horizontally – the HStack will just divide its available space by the number
of views. However, the problem with this approach is that it makes all the
views take up more space than is needed, causing our HStack to grow.

Yes, we want all our views to have the same size, but ideally that size is
whatever is the largest of all the subviews – if subview A is 100 wide, B is
50 wide, and C is 150 wide, we want to give A, B, and C 150 points each,
because that’s the largest width of the subviews.

Let’s start writing some code – add this empty struct now:

struct EqualWidthHStack: Layout {


}

Before we write sizeThatFits() and placeSubviews(), we’re going to write


two helper methods that do work shared in both those other places. There
are extremely concise ways of writing both of these functionally, but
honestly the main focus here is understanding how the Layout protocol
works so I’m going to give you longer code that is much easier to
understand.

The first helper method has the job of going over all the subviews we’re
laying out and figure out the maximum size – the maximum width of all the
views, and the maximum height of all the views. This can be done by:

1. Assuming a CGSize.zero maximum size to begin with.


2. Looping over every view and asking its preferred size.
3. If that view’s width is greater than our current maximum width, make
that our new maximum width.
4. Repeat that, just for height.

So, we’re not saying the maximum width and height for any one view, but
instead the maximum height across all subviews and maximum width
across subviews.

Add this method to the struct now:

private func maximumSize(across subviews: Subviews) -> CGSize


{
var maximumSize = CGSize.zero

for view in subviews {


let size = view.sizeThatFits(.unspecified)

if size.width > maximumSize.width {


maximumSize.width = size.width
}

if size.height > maximumSize.height {


maximumSize.height = size.height
}
}

return maximumSize
}

The second helper is a more complex one, but it solves an important


problem that we can mostly ignore when working with SwiftUI: some
views like to have a certain amount of distance between themselves and
other views. I don’t mean because of padding; that’s part of the view’s size.
Instead, this is a really neat feature of SwiftUI that allows a Text view to
have more or less spacing depending on whether its neighbor is another
Text view or is an Image. Even better, this automatic spacing automatically
varies across platforms, so you’ll get different values on watchOS and tvOS
because of the space differences.

Anyway, this second helper method is going to create a Double array


containing spacing values, one for each subview. This can mostly be done
by asking SwiftUI how much distance should be placed between one view
and the next one, but the last subview is a special case because it doesn’t
have a neighbor afterwards – we’ll return 0 for that.

Add this second helper now:

private func spacing(for subviews: Subviews) -> [Double] {


var spacing = [Double]()

for index in subviews.indices {


if index == subviews.count - 1 {
spacing.append(0)
} else {
let distance =
subviews[index].spacing.distance(to: subviews[index +
1].spacing, along: .horizontal)
spacing.append(distance)
}
}

return spacing
}

That’s most of the hard work done now, so we can finally turn our eyes to
the sizeThatFits() method. Remember, this is given a proposed size and all
the subviews it needs to lay out, and should return a CGSize containing the
actual size it wants to use.

Thanks to our helper methods, implementing sizeThatFits() is


straightforward. We need to:

1. Call maximumSize() to find the largest width and height across all our
subviews.
2. Call spacing() to get an array of the spacing between all the views,
then sum those numbers into a single value.
3. Return our maximum width multiplied by how many subviews we
have, because each view will have the same size, then add to that the
total spacing value.
4. Return our maximum height. No further calculations are needed
because it’s a horizontal stack.

Go ahead and add this sizeThatFits() method now:

func sizeThatFits(proposal: ProposedViewSize, subviews:


Subviews, cache: inout Void) -> CGSize {
let maxSize = maximumSize(across: subviews)
let spacing = spacing(for: subviews)
let totalSpacing = spacing.reduce(0, +)

return CGSize(width: maxSize.width *


Double(subviews.count) + totalSpacing, height:
maxSize.height)
}

The placeSubviews() method is a little trickier, but it still leans heavily on


those two helper methods we wrote. So, start with this:

func placeSubviews(in bounds: CGRect, proposal:


ProposedViewSize, subviews: Subviews, cache: inout Void) {
let maxSize = maximumSize(across: subviews)
let spacing = spacing(for: subviews)

// more code to come


}

In our radial layout example we used an .unspecified proposal size for each
view because we didn’t care how much size each view was, but here we do
care: we want every view to have the same size, which means creating a
specific size proposal and giving it to the view. Again, that view might
ignore the proposed size, but it’s important we give it the chance to take it
into account.

The size proposal we’ll be sending every subview is simple: we already


calculated the maximum width and height across our subviews, so that
becomes our proposed size for every view. Add this line next:

let proposal = ProposedViewSize(width: maxSize.width, height:


maxSize.height)

The final step is to lay out the views. To make this happen, we’ll create an x
variable that represents the center X position of the next view we’re laying
out. When we’re just starting out this will have the position of our left edge,
plus half our maximum size – we’re centering the views, remember.

Please add this line now:

var x = bounds.minX + maxSize.width / 2

And now all that remains is to loop over the subviews, placing each one at
the x position and in the vertical center of our container. Remember, it’s
important we tell SwiftUI that we’re specifying the center of our views
rather than the top-leading edge or something else, and this time we are
going to use the proposal parameter because we’re going to ask each view
to fit itself into the shared ProposedViewSize we made a moment ago.

The key thing here is that every time we place a view we need to modify x
upwards by adding our maximum width value, but also by adding the
spacing for the view we just added so that this view has the correct amount
of space between it and the next view.

We can finish the method by adding this loop:

for index in subviews.indices {


subviews[index].place(at: CGPoint(x: x, y: bounds.midY),
anchor: .center, proposal: proposal)
x += maxSize.width + spacing[index]
}

That completes our layout, so now all that remains is to try it out in a
SwiftUI view:
struct ContentView: View {
var body: some View {
EqualWidthHStack {
Text("Short")
.background(.red)

Text("This is long")
.background(.green)

Text("This is longest")
.background(.blue)
}
}
}

When that runs you’ll see the views spread out across the screen, with “This
is long” dead in the center. This is exactly what we want: the views
themselves have retained the natural size (which is why the colored boxes
are small), but their container has allocated each of them equal space.

Even though you might think this effect is easier to achieve than a radial
layout, you’ll notice it actually took more code – and that’s even with
adding two helper methods. Now, to be fair we could dramatically reduce
the code length if we switched to a more condensed, functional approach:

private func maximumSize(across subviews: Subviews) -> CGSize


{
let sizes = subviews.map { $0.sizeThatFits(.unspecified)
}
return sizes.reduce(.zero) { largest, next in
CGSize(width: max(largest.width, next.width), height:
max(largest.height, next.height))
}
}

private func spacing(for subviews: Subviews) -> [Double] {


subviews.indices.map { index in
guard index < subviews.count - 1 else { return 0 }
return subviews[index].spacing.distance(to:
subviews[index + 1].spacing, along: .horizontal)
}
}

However, keep in mind that the radial layout didn’t even need those
methods in the first place: we always placed views at their natural size, and
didn’t care about any spacing requests they had because they were being
placed in a circle. Still, I wanted to show you this layout because sizing and
spacing are both important skills and I’m sure you’ll use them in your own
work!

If you’re looking for a challenge, how about you try implementing an


EqualHeightVStack?

OceanofPDF.com
Implementing a relative width layout
Have you ever wanted to make SwiftUI views take up a proportional
amount of space in a HStack? I certainly have – to be able to say “give this
view 20% of the space, this other view 30% of the space, and this final view
the remaining 50%” is something that appeared briefly in the very earliest
SwiftUI beta. Sadly it disappeared before SwiftUI 1.0 shipped and has yet
to return, but with the power of the Layout protocol we can bring it back.

This is made possible because of SwiftUI’s layoutPriority() modifier,


which controls how willing a view is to shrink or stretch. All views have a
default layout priority of 0, but if you give a higher value to something it
will grow to fill all the available space more readily.

This layout priority can be read through the Layout protocol, so we’re
going to hijack it here as a way of specifying how much relative space
should be allocated to a view. Rather than forcing developers to add
numbers up to 1 or similar, we will instead sum up all the priorities for the
views we’re trying to lay out, then calculate the relative size for a view
based on its priority compared to the total. So, the total might be 1.0, 100,
12, or any other number depending on what works at the call site.

We can start by creating a struct for our layout, giving it the one property
we care about: how much space we want to have between our views. This
will be a single fixed value provided by the user rather than querying each
subview for how much spacing it wants, because the rest of the space will
be allocated proportionally.

Add this now:

struct RelativeHStack: Layout {


var spacing = 0.0
}

All the hard work for this layout is contained in one helper method, which
has the job of calculating all the frames for all the views in one pass. This
will be called by sizeThatFits() so we know how much space we need, and
also called again by placeSubviews() so we can actually assign each view
its frame.

Because there’s a lot of code here, I’m going to break it down step by step
so I can explain as I go. Start by adding this method stub:

func frames(for subviews: Subviews, in totalWidth: Double) ->


[CGRect] {

Already you can see we’re passing in all the subviews, along with whatever
is the total width allocated to our container.

Our first job will be to figure out how much total spacing we’ll be using
across our layout. We already added a spacing property to control the
amount of space between individual views, so to find the total spacing we
just need to multiply spacing by 1 less than our column count. Why one
less? Well, if we have two columns, we need only one space – the one that’s
between the two columns. We don’t need to add space on either side of the
layout, because that’s something that will be decided by whomever is
placing the container.

So, start by adding this to the frames() method:

let totalSpacing = spacing * Double(subviews.count - 1)

Now we know how much space we have to allocate to each view, which is
our total width minus our total spacing:

let availableWidth = totalWidth - totalSpacing

The final constant I want to set up front is an important one: what is the
total of all the layout priorities of the views we’re working with? This might
add up to 1.0, but really it’s arbitrary – this is a relative layout, after all.
So, add this constant now:

let totalPriorities = subviews.reduce(0) { $0 + $1.priority }

Now it’s time to start calculating frames, which means creating two
variables:

An array of frames, storing where we’ll place every subview we have.


An X value, starting at 0, that represents the position we’ll place the
next subview. Every time we place a view we’ll add its width plus
spacing to our X value.

Add these lines now:

var viewFrames = [CGRect]()


var x = 0.0

And now we have the main chunk of this method, which needs to go over
all the subviews we have, figuring out how much space each one should be
allocated. Start with this:

for subview in subviews {


// more code to come
}

return viewFrames

Inside that loop comes the real work. Remember, this is calculated using the
layout priority assigned to the view rather than trying to take into account
how one view should be spaced from others.

We already know how much space we have to allocate to all views


(availableWidth), and we know the sum of all our subview priorities
(totalPriorities), so we can calculate the width for this subview by
multiplying the available width by the view’s priority, then dividing the
result by the total priorities, like this:
let subviewWidth = availableWidth * subview.priority /
totalPriorities

Now that we know how much space this particular subview can have, we
can put it forward as a proposal and see what comes back:

let proposal = ProposedViewSize(width: subviewWidth, height:


nil)
let size = subview.sizeThatFits(proposal)

Again, that size is decided solely by the child, but it will take into account
our request: we’re asking for a specific width, but saying it can grow as
high as it likes.

We can then combine our x value with the size we received into a CGRect,
and add it to our array:

let frame = CGRect(x: x, y: 0, width: size.width, height:


size.height)
viewFrames.append(frame)

And finally we need to add to x the width of the view we placed, plus the
spacing for the whole container, so the next view we lay out will go into the
correct location:

x += size.width + spacing

That completes our helper method – as you can see, it does all the hard
work of calculating frames for our views, which makes the rest of this
layout straightforward.

First, the sizeThatFits() method. This will:

1. Use replacingUnspecifiedDimensions() on the proposed container


size, to make sure we always have sensible values to work with.
2. Send the proposed width value into our frames() method to calculate
all the view frames.
3. Send back a CGSize containing our proposed width alongside the
maximum Y value we got back from the frames() method – the bottom
edge of the lowest view.

Add this method to the RelativeHStack struct now:

func sizeThatFits(proposal: ProposedViewSize, subviews:


Subviews, cache: inout Void) -> CGSize {
let width =
proposal.replacingUnspecifiedDimensions().width
let viewFrames = frames(for: subviews, in: width)
let height = viewFrames.max { $0.maxY < $1.maxY } ??
.zero
return CGSize(width: width, height: height.maxY)
}

To finish up we need to implement placeSubviews(). Because the frames()


method already does all the hard work of calculating every frame for every
view, this just needs to loop over all the subviews and assign the position
and size we calculated for each one. This time, though, we’re assigning the
leading edge of each view, so that all our views are positioned in the
vertical center of our container.

Add this last method to RelativeHStack:

func placeSubviews(in bounds: CGRect, proposal:


ProposedViewSize, subviews: Subviews, cache: inout Void) {
let viewFrames = frames(for: subviews, in: bounds.width)

for index in subviews.indices {


let frame = viewFrames[index]
let position = CGPoint(x: bounds.minX + frame.minX,
y: bounds.midY)
subviews[index].place(at: position, anchor: .leading,
proposal: ProposedViewSize(frame.size))
}
}
Note how we’re proposing a size to the view based on the return value from
the frames() method – if you remember, that’s the size we got back when
we called subview.sizeThatFits() for this view, so it should be acceptable.

That’s our layout complete! To give it a try, create some flexible views then
assign layout priorities however you want.

For example, we could create views with priorities 1, 2, and 3:

RelativeHStack(spacing: 50) {
Text("First")
.frame(maxWidth: .infinity)
.background(.red)
.layoutPriority(1)

Text("Second")
.frame(maxWidth: .infinity)
.background(.green)
.layoutPriority(2)

Text("Third")
.frame(maxWidth: .infinity)
.background(.blue)
.layoutPriority(3)
}

Or we could use 4, 4, 8 to get two views the same size then one that’s twice
as large, or use 30, 50, 20 if you prefer to think in percentages, or really
whatever numbers work best for your layout.

Three sets of colored text views with space unevenly divided


between them.

OceanofPDF.com
Implementing a masonry layout
Masonry layouts – sometimes called “waterfall” layouts – allow us to create
a grid that’s ragged, which means although there are distinct columns in
place there aren’t “rows” because each view is just slotted in wherever it
fits according to its aspect ratio. This is a really common layout on the web,
and in apps are mainly used for content walls – when the user is scrolling
through a category of pictures looking for something specific, like Pinterest.

Implementing this is actually not as hard as you might think, particularly


now that you’ve implemented three other layouts already. In fact, as you’ll
see our approach is almost identical to creating RelativeHStack – large
parts of the code are almost identical.

Start with a struct for the layout, giving it two properties: how many
columns we have, and how much spacing we want between each item in
our layout. The number of columns must be at least 1 otherwise the layout
makes no sense, so we’ll add a custom initializer that ensures the column
count is always at least 1.

Add this now:

struct MasonryLayout: Layout {


var columns: Int
var spacing: Double

init(columns: Int = 3, spacing: Double = 5) {


self.columns = max(1, columns)
self.spacing = spacing
}
}

Just like with RelativeHStack, all the real work for this layout is contained
in one helper method that calculates and returns frames for all the views.
Start by adding this method stub:
func frames(for subviews: Subviews, in totalWidth: Double) ->
[CGRect] {

Just like before, we can figure out much total spacing we have by
multiplying spacing by 1 less than our column count, like this:

let totalSpacing = spacing * Double(columns - 1)

Now we know how much spacing we have in total, we can calculate how
much space is left for the columns by subtracting totalSpacing from
totalWidth. If we then divide that number by our column count, we’ll have
how much space should be allocated to each column. Add this next:

let columnWidth = (totalWidth - totalSpacing) /


Double(columns)

There’s one last number I want to set up front, which is how much space we
need to allocate for one column including its spacing, because it makes our
code a little easier to read:

let columnWidthWithSpacing = columnWidth + spacing

At this point we know all the values we need to calculate our frames, so our
first job is to create a ProposedViewSize that we can present to each view.
This will be the same for all views: “you can use as much height as you
want, but I’d like your width to be the same as our column width.”

Add this line next:

let proposedSize = ProposedViewSize(width: columnWidth,


height: nil)
When it comes to calculating frames, masonry layouts assign views to
columns in a very specific way – we don’t do it randomly because that
would just cause a mess. Instead, our goal for any given view is to place it
into the shortest column so that we aim for some balance. That doesn’t
mean we’ll get balance because perhaps the final view we place in a column
is much longer than all the others, but we’re at least aiming for it.

This means we need two arrays: one storing all our view frames across all
columns, and one storing the heights of each column we have. Add these
lines now:

var viewFrames = [CGRect]()


var columnHeights = Array(repeating: 0.0, count: columns)

We can now loop over all our subviews, figure out which column is the
shortest, and place our view there. As we go through, we’ll also stash the
latest view frame away in our viewFrames array, which is the value that
will be returned from this method.

Please add this code now:

for subview in subviews {


// more code to come
}

return viewFrames

Like I said, the first step in that loop is to figure out which column is
shortest. If we assume that the shortest is the first one and set a gigantic
height for it, we can loop over all the other columns and check whether they
are shorter or not. If we find one that is shorter, we’ll make that our new
selected column and use its height for our shortest height.

Replace the // more code to come comment with this:

var selectedColumn = 0
var selectedHeight = Double.greatestFiniteMagnitude
for (columnIndex, height) in columnHeights.enumerated() {
if height < selectedHeight {
selectedColumn = columnIndex
selectedHeight = height
}
}

At this point we know exactly which column should be used to place our
subview, so we can figure out the X and Y coordinates for it. The X value is
simply the index of the column multiplied by the
columnWidthWithSpacing, and the Y value is the current height of
whichever column was shortest. Add this just after the previous loop:

let x = Double(selectedColumn) * columnWidthWithSpacing


let y = columnHeights[selectedColumn]

To calculate the view’s size, we just need to ask it: given the proposed size
we made earlier, how much space does it actually want? Add this next:

let size = subview.sizeThatFits(proposedSize)

Again, the view is free to ignore that proposal entirely, which would make
our layout a mess. That’s how SwiftUI works, though: the parent proposes a
size, but the child gets to make the final choice and the parent must respect
that.

At this point we know the full set of data for the frame of this view, so we
can create a CGRect from it:

let frame = CGRect(x: x, y: y, width: size.width, height:


size.height)

To end this loop we need two more things. First, we just added a subview to
a column, so we need to adjust the height of that column to include the
subview’s height plus our spacing property. That doesn’t mean we’re
always adding spacing below the finished, placed views; these column
heights are just used for calculating where views ought to go.
Add this now:

columnHeights[selectedColumn] += size.height + spacing

And the final part of the loop is to add the frame value we created to our
viewFrames array, which is being returned from the method:

viewFrames.append(frame)

That completes our frames() method – it’s not vast amounts of code, but it
is done very precisely to make sure we balance each column as best as we
can. The point is that eventually it returns the viewFrames array, which
contains all the frames for all subviews.

With that hefty helper method in place, we can now turn to sizeThatFits()
and placeSubviews(), both of which lean on it heavily. In fact, both these
two are almost identical to their equivalents from RelativeHStack, so we
can take a shortcut – copy and paste those two methods from your
RelativeHStack code into your MasonryLayout, like this:

func sizeThatFits(proposal: ProposedViewSize, subviews:


Subviews, cache: inout Void) -> CGSize {
let width =
proposal.replacingUnspecifiedDimensions().width
let viewFrames = frames(for: subviews, in: width)
let height = viewFrames.max { $0.maxY < $1.maxY } ??
.zero
return CGSize(width: width, height: height.maxY)
}

func placeSubviews(in bounds: CGRect, proposal:


ProposedViewSize, subviews: Subviews, cache: inout Void) {
let viewFrames = frames(for: subviews, in: bounds.width)

for index in subviews.indices {


let frame = viewFrames[index]
let position = CGPoint(x: bounds.minX + frame.minX,
y: bounds.midY)
subviews[index].place(at: position, anchor: .leading,
proposal: ProposedViewSize(frame.size))
}
}

The only actual change is in the place() method inside placeSubviews(),


because we want our views placed by their top-leading edge rather than just
their leading edge. This is the default behavior when placing views, which
means making two changes:

1. The Y position will be the top-left corner of our view’s frame,


remembering to add in the midX of our container so the views are
centered correctly.
2. Remove anchor: .leading from the code, so it aligns top-leading.

So, change the loop in your placeSubviews() method to this:

for index in subviews.indices {


let frame = viewFrames[index]
let position = CGPoint(x: bounds.minX + frame.minX, y:
bounds.minY + frame.minY)
subviews[index].place(at: position, proposal:
ProposedViewSize(frame.size))
}

And that’s our layout complete! Honestly, once you see how well it works I
think you’ll be really pleased because it’s an extremely effective algorithm.

To actually give this a meaningful test, we can create a trivial placeholder


view that displays a random color and its size. What matters is that this
view has a fixed aspect ratio, and the same is true if you want to use your
own custom pictures instead – make them resizable, but ensure they stay at
the correct aspect ratio so they can be placed into our columns well.

Here’s my placeholder view:

struct PlaceholderView: View {


let color: Color = [.blue, .cyan, .green, .indigo, .mint,
.orange, .pink, .purple, .red].randomElement()!
let size: CGSize
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(color)

Text("\(Int(size.width))x\(Int(size.height))")
.foregroundColor(.white)
.font(.headline)
}
.aspectRatio(size, contentMode: .fill)
}
}

Important: The size displayed in those views won’t match the actual size
used to place them in our grid, because they’ll get resized up or down as
needed. However, the aspect ratio will be the same between the view’s
requested and actual size, which is what matters.

To try this out we can now create a ContentView that creates 20 random
view sizes and places them into a MasonryLayout inside a ScrollView. To
make things more interesting, I’m going to make the column count
adjustable using a stepper:

struct ContentView: View {


@State private var columns = 3

@State private var views = (0..<20).map { _ in


CGSize(width: .random(in: 100...500), height:
.random(in: 100...500))
}

var body: some View {


ScrollView {
MasonryLayout(columns: columns) {
ForEach(0..<20) { i in
PlaceholderView(size: views[i])
}
}
.padding(.horizontal, 5)
}
.safeAreaInset(edge: .bottom) {
Stepper("Columns: \(columns)", value:
$columns.animation(), in: 1...5)
.padding()
.background(.regularMaterial)
}
}
}

That completes our third and final layout, and I think it really shows off just
how flexible SwiftUI is. Remember, all three layouts we’ve made work
great in the AnyLayout example from earlier – we can flip through grids,
ZStack, masonry layout, relative width layout, radial layout, etc, all without
changing any other part of our code.
Before we’re done, I’d like you to try one more thing. Modify your
ContentView code to this:

MasonryLayout(columns: columns) {
ForEach(0..<20) { i in
if i.isMultiple(of: 2) {
PlaceholderView(size: views[i])
} else {
Divider()
}
}
}
.padding(.horizontal, 5)

We’re now inserting dividers into half our views, and when you run the
code you’ll see they appear correctly. The question is: how? How does
SwiftUI know to make our divider horizontally rather than vertical?

Internally, SwiftUI asks our layout whether it defines one specific axis for
its views. If we don’t specify something SwiftUI assumes we have a
vertical layout, which works great for our masonry layout where a left-to-
right divider looks great, but you’ll see it causes problems for horizontal
layouts such as EqualWidthHStack and RelativeHStack – the dividers
will still run left to right even in a horizontal axis, which looks wrong.

If you need to customize the axis of your layout, add a computed property
called layoutProperties providing a specific value. For example, if we
wanted to specifically tell SwiftUI that our masonry layout was vertical, we
would add this:

static var layoutProperties: LayoutProperties {


var properties = LayoutProperties()
properties.stackOrientation = .vertical
return properties
}

That specifies our masonry layout works vertically, but you won’t see a
difference here – try upgrading one of our horizontal stack implementations
to have a horizontal axis, and you should see them behave better!

OceanofPDF.com
Layout caching
When we first looked at custom layouts I mentioned that both
sizeThatFits() and placeSubviews() take a cache parameter that allows us
to avoid repeating work by reusing calculations. For our radial and equal
width layouts this wasn’t an issue, but our masonry layout is more
complicated and is a place where we could consider caching.

Important: I said could consider there, not must use. Caching is something
you should implement once you have used Instruments to profile your app
and verified there’s a performance problem you need to address.

No, seriously: You shouldn’t add a cache to your layout unless your
profiling has shown conclusive proof that it’s needed, because bad caching
is a very common source of bugs. I’m adding a cache here only so you can
see how it works, not because one is desperately needed.

To make a layout cache you first need to make the easiest choice: what data
do you want to cache? In the case of our masonry layout we only really
have one piece of data, and it’s also the most complex to calculate: all the
frames for our views. So, at the very least we need to add a struct like this
one to our code:

struct Cache {
var frames: [CGRect]
}

Now, you can put that anywhere in your project, but I’m a big fan of nesting
types where they have limited applicability. In this case, that Cache type is
designed specifically for use by our MasonryLayout struct, so I’d place it
inside like this:

struct MasonryLayout: Layout {


struct Cache {
var frames: [CGRect]
}
// rest of the masonry code
}

Important: As soon as you add that nested Cache struct, SwiftUI will
attempt to use it for the layout. We aren’t ready for this quite yet, so you’ll
see compiler errors for the time being.

That cache is enough to store all the data we care about for performance
reasons, but before we solve our compiler errors there’s another property I
want to add and it’s in answer to a simple question: how can we know when
our cache is invalid?

Well, SwiftUI will automatically invalidate our cache when our layout or its
subviews change, but that only applies to the properties of our layout rather
than the amount of space it’s allocated on the screen. This means if we
adjust the size of our layout at runtime, either explicitly adjusting its frame
or because the device rotated, our cache is likely to be wrong.

To fix this problem, we need to give our cache a width property that will
store the amount of space we were laying out for. When we’re asked to
place our subviews, we can double check we’re still referring to the width
we used for our cache, and if not we’ll recreate the cache from scratch.

Add this property to the Cache struct now:

var width = 0.0

That’s our Cache type complete, so now in order to make Swift happy we
need to use it in three places. It won’t work yet, but at least we’ll be back to
our code compiling cleanly.

The first is a new method that Layout will call when it wants to create a
new cache for our layout. This is called simply makeCache(), and it should
return a new cache object ready to use. Add this to MasonryLayout now:

func makeCache(subviews: Subviews) -> Cache {


Cache(frames: [])
}

The second and third places we need to use Cache are in the signatures for
sizeThatFits() and placeSubviews(). Find this code in both the method
signatures:

cache: inout Void

And replace it with this:

cache: inout Cache

Note: If you used Xcode’s code completion for the signatures, you might
have cache: inout (), which is identical to cache: inout Void.

That will make our code compile cleanly again, although it won’t actually
utilize the cache yet. This part is surprisingly easy, though, because we just
need to copy the data we’re creating into our cache at the right times.

For sizeThatFits(), that means adding two lines of code directly before the
return statement:

cache.frames = viewFrames
cache.width = width

Remember, SwiftUI already created a Cache object for us, so we just need
to update it with the latest values we computed.

sizeThatFits() will be called when our layout is created or recreated, or


when its subviews change. It will also be called when the size we allocate to
a view changes, e.g. if we add padding at runtime. However, it will only be
called once per orientation otherwise, so the following can happen:

We launch our app in portrait. sizeThatFits() is called and sets up the


cache for our portrait size.
We rotate to landscape. sizeThatFits() is called and sets up the cache
for our landscape size.
We rotate back to portrait. sizeThatFits() won’t be called again
because it was already called for the portrait orientation, so our layout
will accidentally use our cache that was configured for landscape.

So, we need to be careful in placeSubviews(): if we find that our bounds


doesn’t match our cached size, we should update the cache by calling
frames again and resetting the width.

Put this at the start of the method, in place of the let viewFrames line:

if cache.width != bounds.width {
cache.frames = frames(for: subviews, in: bounds.width)
cache.width = bounds.width
}

That ensures the cache is always in a good state before we try and use it.

Speaking of using it, the last step in this process is to replace


viewFrames[index] a couple of lines lower, because we need to use our
cache instead – make that read cache.frames[index] instead, and now our
cache will be used.

You can see it in action if you put this at the very start of the frames()
method:

print("Recreating cache")

When you run the app you’ll see “Recreating cache” is printed only once,
whereas previously the frames() method would have been called
unconditionally in both sizeThatFits() and placeSubviews(). So, at the
very least we’ve halved the number of calculations we perform.

That doesn’t mean we can eliminate all the extra work – SwiftUI is free to
rebuild our cache whenever and as often as it wants, but that’s an
implementation detail and nothing I’d worry about.
OceanofPDF.com
Customizing layout animations
SwiftUI does a good job of animating some parts of our layouts
automatically. For example, you’ll find you can animate something like the
spacing in both our custom HStack just by changing the value, and as you
saw our masonry layout animates its columns flawlessly.

However, sometimes it’s not perfect, and the problem occurs when our
intermediate states matter: if you’re animating the spacing of a HStack, for
example, then SwiftUI can look at the spacing before the animation, look at
the spacing after, then interpolate between the two – it will call
sizeThatFits() and placeSubviews() once each, then handle the rest itself.
However, if you need to calculate your intermediate states for every step in
the animation – if you’re animating values used inside sizeThatFits(), for
example – then we need to give SwiftUI a little extra help.

To demonstrate this, I want to return to our radial layout. Rather than


placing our circles around a full circle, we could instead add a property to
control how much of the circle we use. Add this to your RadialLayout
struct now:

var rollOut = 0.0

We can then adjust our placeSubviews() method so that the angle value we
calculate for each view is multiplied by rollOut, like this:

let angle = Angle.degrees(360 /


Double(subviews.count)).radians * rollOut

So, if angle was originally 10% of a circle, when rollOut was only 0.5 then
the angle would be just 5% of a circle, and when it’s 0.0 then the angle
would 0% for all the views – the circles wouldn’t roll out at all.

We can then try that out by adjusting our ContentView code to track a
Boolean property of whether our circle should be rolled out or not, convert
that into a Double to use with the RadialLayout initializer, and finally add
a button to toggle the Boolean. Adjust your ContentView struct to this:

struct ContentView: SelfCreatingView {


@State private var count = 16
@State private var isExpanded = false

var body: some View {


RadialLayout(rollOut: isExpanded ? 1 : 0) {
ForEach(0..<count, id: \.self) { _ in
Circle()
.frame(width: 32, height: 32)
}
}
.safeAreaInset(edge: .bottom) {
VStack {
Stepper("Count: \(count)", value:
$count.animation(), in: 0...36)
.padding()

Button("Expand") {
withAnimation(.easeInOut(duration: 1)) {
isExpanded.toggle()
}
}
}
}
}
}

Go ahead and run that now and see what you think – you should find that as
you toggle isExpanded the circles move from one point directly out to their
final location.

It’s important you understand what’s happening here: SwiftUI knows the
initial position of the circles, and when isExpanded is toggled it will
calculate the destination position of the circles, so it simply animates from
the original to the new positions in one action.

This happens because SwiftUI doesn’t understand rollOut should be


animated. It knows the result of changing rollOut should be animated
because that’s baked right into the framework, which is why our circles
animate their position, but it doesn’t know that it should animate all the
rollOut values as it moves from 0 to 1.

We can do better.

You see, the Layout protocol inherits from Animatable, which means we
can ask SwiftUI to give us all the intermediate values by implementing
animatableData just like animating any other SwiftUI views.

Add this property to RadialLayout now:

var animatableData: Double {


get { rollOut }
set { rollOut = newValue }
}

With that tiny change, we’ve told SwiftUI we want to do something special
when the animating value changes – that rather jumping directly to the new
rollOut property, it should instead move from 0.0 to 0.05, 0.1, etc, and let
us do something with each intermediate value.

We’ll look at the impact of this more in a moment, but first please run the
result so you can see the difference. All being well you should see our
circles animate outwards along the circle’s perimeter rather than just sliding
directly to their destination – it’s much nicer, I think.

To understand what has changed internally – and if you haven’t figured it


out by now, I really think it’s important to think about these internals so you
know what’s really happening when SwiftUI works with our code – I want
you to comment out the animatableData property we just added, and
instead add print statements to both sizeThatFits() and placeSubviews(),
like this:

print("In sizeThatFits")

Or this:
print("In placeSubviews")

When you run the app you’ll see those messages printed out at various
times, but it’s not a lot – SwiftUI might call each one twice when changing
rollOut, for example, so it can calculate the positions of its subviews before
and after the animation.

Now uncomment the animatableData property so that it’s live code again,
while also leaving our little print() calls in, and this time you’ll see
something very different: our print() calls get executed a lot. In fact, both
sizeThatFits() and placeSubviews() get called twice each for every tiny
change of animatableData, so if it animates from 0.0, through 0.01, 0.02,
0.03, etc, all the way up to 1.0, those methods are getting called many,
many times.

This is what makes our improved animation work: rather than making
circles from in a straight line from their start to end position, SwiftUI calls
this line of code again and again to calculate a custom location for all the
intermediate positions of our subviews:

let angle = Angle.degrees(360 /


Double(subviews.count)).radians * rollOut

That means the latest value of rollOut is being read every time it changes,
causing the much nicer animation.

Obviously you can go ahead and remove the print() calls now, but I do
want you to keep in mind that animating layouts like this will trigger an
exponential rise in the number of times your layout methods are called, so
you should avoid doing anything too computationally expensive in there
unless you’re carefully profiling the animation on older devices.

Tip: If your animation really does manage to push the CPU hard – a
surprisingly hard thing to do! – this might be a good place to consider
introducing a cache to reduce the number of calculations you’re performing.

OceanofPDF.com
Chapter 5
Drawing and Effects
OceanofPDF.com
Drawing with Canvas
It’s no secret that I am obsessed with drawing using SwiftUI, and honestly
I’ve lost countless hours noodling around, experimenting, and overall
having fun creating beautiful effects with surprisingly little code. Over the
coming chapters I want to explore a handful of these techniques with you,
partly because it really is a lot of fun creating beautiful things, but partly
also because I hope it will inspire you to add a little extra surprise and
delight to your own apps.

We’re going to start with something nice and easy: a trivial particle system
that lets us draw in glowing lights by touching the screen. Particle systems
work by creating dozens, hundreds, or even thousands of very small
images, which can be colored and animated to create a variety of special
effects such as fire, smoke, fog, and rain.

In this initial foray into drawing, our particle system will be trivial: we’ll
constantly be creating and deleting particles, and the user will be able to
move their finger to reposition the place where we generate particles from.
This takes fewer than 50 lines of code, and that’s including whitespace and
lines that are just closing braces – it’s a great entry point into the world of
drawing with SwiftUI.

The first step is to create a Particle struct that will store the data one
particle needs to work. We’ll make more advanced particles later on, but for
this example our particle just needs two pieces of data: its position on the
screen, and the date it should be destroyed. We’ll pass the X and Y values in
from the particle system that generates it all, but we can automatically set
the destruction date to be the current time plus 1 second so that each
particle lasts that long before being destroyed.

Add this struct now:

struct Particle {
let position: CGPoint
let deathDate = Date.now.timeIntervalSinceReferenceDate +
1
}

Now that we have defined a single particle, the next step is to create the
particle system responsible for creating and managing all the particles. This
has six interesting things:

1. It will be a class rather than a struct, so we can mutate its values freely
without triggering SwiftUI updates.
2. It needs to store an array of all the particles that are currently alive.
3. It also needs to store the current position of the particle system, which
is used to create new particles.
4. We’ll give it one method, called update(). This will be called every
time we want to redraw our canvas, and will be provided with the
current time.
5. Inside there we’ll destroy any particles that are past their deathDate
property.
6. Finally, it will also create a new particle at the current position of the
particle system.

It takes much less code to implement all that than it does to explain it, so go
ahead and add this class now:

class ParticleSystem {
var particles = [Particle]()
var position = CGPoint.zero

func update(date: TimeInterval) {


particles = particles.filter { $0.deathDate > date }
particles.append(Particle(position: position))
}
}

That completes all our data model, so what remains is to create one of those
particle systems inside ContentView, then use its data to render particles
inside TimelineView and Canvas.

This class doesn’t conform to ObservableObject because it doesn’t need to


– it manages itself without needing to publish any changes. So, we’ll create
it using @State rather than @StateObject, which is enough to keep the
object alive through view recreations without also requiring
ObservableObject.

Add this property to ContentView now:

@State private var particleSystem = ParticleSystem()

Now for the view’s body. I’m going to tackle this in three parts to make it
easier to follow: we’ll create the SwiftUI views first, then add some
modifiers to make it look and work right, then finish up by adding the
actual drawing code.

First, the views. We need a TimelineView so that SwiftUI knows to redraw


our layout on a fixed schedule, which in our case will be .animation so it
redraws as often as necessary to get smooth animations. Inside that we’ll
place a Canvas, which is where SwiftUI gives us free rein to draw
whatever we need inside a drawing context with a fixed size.

Replace your current body property with this:

TimelineView(.animation) { timeline in
Canvas { ctx, size in
// drawing code here
}
}

We’re going to add three modifiers to that TimelineView to get exactly the
right effect:

1. A custom drag gesture so that we can update the particle system’s


position as the user moves their finger. If we give this a minimum
distance of 0 it means the gesture will start being triggered as soon as
the user moves their finger even the smallest amount.
2. We’ll the TimelineView to ignore the safe area, so the user can draw
to the very edges of the screen.
3. Finally, we’ll give it a black background color so that our finger
drawings stand out nice and bright.

Add these three modifiers to the TimelineView now:

.gesture(
DragGesture(minimumDistance: 0)
.onChanged { drag in
particleSystem.position = drag.location
}
)
.ignoresSafeArea()
.background(.black)

The last part of the puzzle is to fill in the drawing code. This needs to call
the particle system’s update() method with the current time from the
TimelineView, then loop over all the particles and render a circle at their
location. I chose a 32x32 size for my circles, but you can make the circle
whatever size you want – just make sure you subtract half the size from the
X and Y position so the circle is centered on the user’s finger.

Replace the // drawing code here comment with this:

let timelineDate =
timeline.date.timeIntervalSinceReferenceDate
particleSystem.update(date: timelineDate)

for particle in particleSystem.particles {


ctx.fill(Circle().path(in: CGRect(x: particle.position.x
- 16, y: particle.position.y - 16, width: 32, height: 32)),
with: .color(.cyan))
}

Go ahead and give it a try. All being well you should find you can drag your
finger on the screen to see blue circles follow you around, each of which
should disappear a second after they are created.
That’s a start, but it’s not what I’d call beautiful. We can do better! First,
rather than making circles simply disappear we can make them fade away
slowly by subtracting the current time from their deathDate property and
using that for the canvas opacity. So, if deathDate was 3.5 and the timeline
date was 3.0, we’d set opacity to 0.5.

Add this line of code directly before the ctx.fill() line:

ctx.opacity = particle.deathDate - timelineDate


That’s an improvement, but to get something much nicer I’d like you to add
these two lines of code directly after the call to particleSystem.update():

ctx.blendMode = .plusLighter
ctx.addFilter(.blur(radius: 10))

The first of those tells SwiftUI to blend the circles together so the colors get
lighter when they overlap, and the second will apply a Gaussian blur to all
the circles so they look more like a smooth glow than individual circles.

And with that we’re done with our first drawing example! Try it out and see
what you think – given how little code we’ve written I think the end result
is quite beautiful!
OceanofPDF.com
Falling snow
Now that you’ve got the hang of a basic particle system, let’s take it up a
notch by creating particles that move independently rather than always
staying where they were created. This means being able to constantly adjust
our particles after they have been created, which in turn means using a class
rather than a struct for the Particle type.

Add this class now:

class Particle {
var x: Double
var y: Double
let xSpeed: Double
let ySpeed: Double
let deathDate = Date.now.timeIntervalSinceReferenceDate +
2

init(x: Double, y: Double, xSpeed: Double, ySpeed:


Double) {
self.x = x
self.y = y
self.xSpeed = xSpeed
self.ySpeed = ySpeed
}
}

There are a few things that deserve extra explanation:

1. I’ve split x and y into two separate properties because it makes them
easier to work with.
2. The ySpeed will determine how fast this particle moves down the
screen, and the xSpeed is there to let us add a very small amount of
horizontal movement to make the particles look a bit more natural.
3. There’s a 2-second lifetime for each particle so they can fall most if
not all of the way down the screen before being destroyed.
4. It’s a class rather than a struct, so we need a custom initializer.
Next we need to write the ParticleSystem class. This is similarly enhanced
from our previous, simpler particle system, because now it needs some
extra features:

1. We need to move all particles that are still alive.


2. That movement needs to happen at a fixed speed regardless of how
fast the app is rendering.
3. We need to create new particles at random locations at the top of the
screen, so they fall down evenly from the left screen edge to the right.
4. Knowing where the right edge of the screen lies means sending in the
canvas size.

That second point is the most interesting from a code perspective, because
we need to make sure particles move at the same speed no matter whether
update() is called 60 times a second (the ideal for many devices), 120 times
a second (the ideal for devices that support ProMotion), or even just 30
frames a second if your app is busy doing a lot of other work.

This can be done using a technique called frame-independent movement,


which means we calculate how much time has passed since update() was
last called, then multiply our movement speed by that time difference. So, if
we want to move 60 points per second and a tenth of a second has elapsed
since the last update, we move by 6 points, but if only 1/60th of a second
elapsed then we move only 1 point.

Doing this means giving our ParticleSystem class an extra property that
stores when update() was last called. This can be the current date to begin
with, but it will be changed every time update() is called.

Start with this new class:

class ParticleSystem {
var particles = [Particle]()
var lastUpdate = Date.now.timeIntervalSinceReferenceDate
}

The update() method will accept the same time interval we used in the
previous particle system, but like I said we’re also going to make it accept
the size of the screen so we know where particles can be created. Each new
particle needs various values provided as part of its initializer:

The X coordinate will be any value between -32 and the screen width.
Our particles will be 32 points, just like in the previous particle
system, so using -32 means we’ll position some particles partly off the
left edge to ensure a full spread of positions.
The Y coordinate will always be exactly -32, which places new
particles just off the top edge of the view.
The X speed will be a random number between -50 and 50, so particles
move very gently left or right.
The Y speed will be a random number between 100 and 500, so
particles move swiftly down the screen. Having a lot of variation in
speed will create a pleasing sense of depth to the particles.

Note: It’s important we replace the value of lastUpdate with whatever is


the new timeline date, so the next time the method is called we move the
correct amount.

Add this method to ParticleSystem now:

func update(date: TimeInterval, size: CGSize) {


let delta = date - lastUpdate
lastUpdate = date

// update all particles here

let newParticle = Particle(x: .random(in:


-32...size.width), y: -32, xSpeed: .random(in: -50...50),
ySpeed: .random(in: 100...500))
particles.append(newParticle)
}

Obviously I left the most important part out, which is where we update each
particle to take into account its movement across the screen. We need to
remove particles from our array whenever they pass their death date, just
like we did with the previous particle system, so we might as well use the
same loop to perform our frame-independent movement.
Add this loop in place of the // update all particles here comment:

for (index, particle) in particles.enumerated() {


if particle.deathDate < date {
particles.remove(at: index)
} else {
particle.x += particle.xSpeed * delta
particle.y += particle.ySpeed * delta
}
}

That completes our data model, so now we can turn to the SwiftUI view to
render it all using TimelineView and Canvas. We’re going to write a few
versions of this, but our initial pass is identical to the ContentView struct
from our previous particle system apart from three changes:

1. We need to pass the canvas size into our particle system’s update()
method, so it knows the full range of space that can be used to create
particles.
2. We aren’t going to add a blend mode. You can add one if you want, but
it’s not the effect I’m looking for here.
3. We don’t need to offset the particle’s X and Y position by half the
circle width, because we don’t need to draw exactly under the user’s
finger any more.

Otherwise it’s exactly the same, so replace your existing ContentView


struct with this:

struct ContentView: View {


@State private var particleSystem = ParticleSystem()

var body: some View {


TimelineView(.animation) { timeline in
Canvas { ctx, size in
let timelineDate =
timeline.date.timeIntervalSinceReferenceDate
particleSystem.update(date: timelineDate,
size: size)
ctx.addFilter(.blur(radius: 10))

for particle in particleSystem.particles {


ctx.opacity = particle.deathDate -
timelineDate
ctx.fill(Circle().path(in: CGRect(x:
particle.x, y: particle.y, width: 32, height: 32)), with:
.color(.white))
}
}
}
.ignoresSafeArea()
.background(.black)
}
}

That already creates a neat snow effect, but it takes only a little extra work
to push this into much more interesting territory. For example, we could
make our snow blobs look like metaballs with only a little extra code.

Important: Before you decide to email me saying I wrote “metaballs”


when I meant to write “meatballs”, you should know that metaballs is the
correct spelling and is the term used for organic-looking shapes that appear
to meld together when they are in close proximity to each other.
If you haven’t seen metaballs in action before, you’re in for a real treat –
not least because it’s remarkable how little code it takes in SwiftUI.

The first step is to tell SwiftUI to render all our particles into a distinct
layer. This means all the particles will be rendering into a new transparent
context, which is then drawn onto our original context in one pass. So,
rather than seeing each particle as individual, the original context will just
be given all the particles as one finished drawing – perfect for blending into
metaballs.

To make this happen, add ctx.drawLayer { ctx in before the for loop in our
canvas, and add a closing brace after the loop ends, like this:

ctx.drawLayer { ctx in
for particle in particleSystem.particles {
ctx.opacity = particle.deathDate - timelineDate
ctx.fill(Circle().path(in: CGRect(x: particle.x, y:
particle.y, width: 32, height: 32)), with: .color(.white))
}
}

Important: That creates a new context inside drawLayer(). I’ve given it


the same ctx name as the outer layer because a) we can’t draw to the outer
context from inside the layer, and b) it means we don’t need to change the
opacity and fill() lines, but you’re welcome to name it inner or similar.

If you run the code now you’ll see nothing has changed, but behind the
scenes SwiftUI is now rendering all the particles into a new layer, then
drawing that layer into our main context. Although it looks the same to the
user, the behind the scenes part matters to us because if we add more filters
they get applied to the whole sublayer in one pass – overlapping circles are
treated like one contiguous shape rather than two separate ones.

In this case we’re going to add the alpha threshold filter, which tells
SwiftUI to replace all the pixels with a specific color if they fall within
alpha values of our choosing, or make them transparent otherwise. The
maximum alpha value is 1 by default, so if we specify 0.5 for the minimum
it means that all pixels between alpha 0.5 and 1.0 will be replaced with a
specific color, but pixels outside that alpha range will be invisible.

Think about it: we’re using a blur filter already, so already quite a lot of
each circle will fall outside that alpha threshold because only the central
parts will have enough opacity to pass the threshold. But when two circles
overlap, even partly, the parts where they overlap will have a higher alpha
value, and because we’re rendering all our circles into a sublayer it means
this filter will see the combined circle areas as a single pixel colors to
evaluate.

To see the result of all this, and the following code directly before the blur
filter:

ctx.addFilter(.alphaThreshold(min: 0.5, color: .white))

If you run the code now you should see the metaball effect in full swing: as
the particles overlap each other they will appear to merge together as if they
were water drops rolling down a window. Hopefully you can see what I
meant when I said they looked organic!
To create an even more interesting effect, try using your TimelineView as
the mask for something else, such as a linear gradient, like this:

LinearGradient(colors: [.red, .indigo], startPoint: .top,


endPoint: .bottom).mask {
// current TimelineView code
}
.ignoresSafeArea()
.background(.black)

Now the metaballs will appear to change color the lower they get on the
screen, creating an effect reminiscent of lava lamps. Can we make an actual
lava lamp effect? Certainly, but that takes quite a bit more thinking…

OceanofPDF.com
Creating a lava lamp
For a really advanced effect, we can use SwiftUI to create a lava lamp by
combining Canvas, metaballs, and a chunk of mathematics. To make things
easier to follow I’m going to adopt a simplified approach first that does
without the math, but if you have the patience I encourage you stick around
for the extended version – it’s worth it!

As with the previous two particle systems, the first step is to define what
one particle needs to know in order to work. The particles will move, which
means we need a class with X and Y coordinates, but this time there are a
few small changes:

We will need to be able to loop over particles in SwiftUI code, which


means making this class conform to Identifiable with a random
UUID.
Each particle will have a unique size, making our lava lamp more
varied.
The lava bubbles won’t move horizontally, so we need only a single
speed property to handle Y movement.
We need to track whether this bubble is currently moving up or down,
because we will never destroy the lava particles – we just keep reusing
the same initial batch, flipping their direction when they reach one end.

There is also one important change: we need to fill our lava lamp with lots
of particles when the app is launched, which means creating all our particles
up front rather than on a rolling basis. That in turn means we don’t actually
know the canvas’s dimensions when creating our particles, so rather than
storing absolute X and Y positions – e.g. X:50 Y:300 – we will instead store
relative positions between 0 and 1, where 0 is the top or left edge and 1 is
the bottom or right edge.

So, start by creating this new Particle class:

class Particle: Identifiable {


let id = UUID()
var size = Double.random(in: 100...250)
var x = Double.random(in: -0.1...1.1)
var y = Double.random(in: -0.25...1.25)
var isMovingDown = Bool.random()
var speed = Double.random(in: 0.01...0.1)
}

Note: That creates particles in positions that go slightly beyond the screen’s
bounds to make sure we get a full spread. The spread is wider for the Y axis
because we’ll be moving particles vertically, so they can safely go further
off the screen before coming back on.

Next we need to create a ParticleSystem class responsible for creating the


lava bubbles and moving them around. This starts similar to the previous
particle system, so add this now:

class ParticleSystem {
let particles: [Particle]
var lastUpdate = Date.now.timeIntervalSinceReferenceDate

func update(date: TimeInterval) {


let delta = date - lastUpdate
lastUpdate = date

for particle in particles {


// move the particles
}
}
}

Apart from the // move the particles comment, there is one important
difference here: rather than creating particles dynamically as the app runs,
we’re instead going to create them all up front. This is why particles can be
a constant array now, and also why it doesn’t have a default value – we
need to add an initializer to create all our particles up front.

We could make the number of particles fixed, but it’s hardly any extra work
to make that amount customizable. So, add this initializer to
ParticleSystem now:
init(count: Int) {
particles = (0..<count).map { _ in Particle() }
}

Now for the movement: if the particle is moving down, we’ll add to its Y
position using frame-independent movement, otherwise we’ll subtract from
it. Critically, we’ll add two extra checks to make sure particles flip their
movement when they are fully off the screen.

Replace the // move the particles comment with this:

if particle.isMovingDown {
particle.y += particle.speed * delta

if particle.y > 1.25 {


particle.isMovingDown = false
}
} else {
particle.y -= particle.speed * delta

if particle.y < -0.25 {


particle.isMovingDown = true
}
}

Now for the main event: rendering all this in SwiftUI. We’re going to take a
different approach here, partly because it demonstrates an important
Canvas technique, but honestly mostly because it makes it much easier to
switch over to the more advanced version if you decide to pursue it!

The different approach is this: rather than just filling circles in our Canvas
code, we are instead going to create our shapes as SwiftUI views and pass
them into the canvas as symbols. This is a real powerhouse Canvas
technique because it lets us place any kind of SwiftUI view directly into our
drawings. This will be really important in the more advanced lava lamp
effect, but for now we’ll just create SwiftUI circles in there.

Apart from that drawing change, there are two other thing we’re going to
do:
1. Rather than use fixed values for the blur and threshold filters, we’ll
make them local state you can adjust using sliders. This will give you a
much better idea of how the finished effect really works, because you’l
be able to noodle around with both sliders to get exactly the result you
want.
2. We’ll be using the same LinearGradient mask as before, but this time
we’ll give it a background color indigo. Why? Well, if lava lamps can’t
be disco, what can?

Go ahead and add this ContentView now:

struct ContentView: View {


@State private var particleSystem = ParticleSystem(count:
15)
@State private var threshold = 0.5
@State private var blur = 30.0

var body: some View {


VStack {
LinearGradient(colors: [.red, .orange],
startPoint: .top, endPoint: .bottom).mask {
TimelineView(.animation) { timeline in
Canvas { ctx, size in
particleSystem.update(date:
timeline.date.timeIntervalSinceReferenceDate)
ctx.addFilter(.alphaThreshold(min:
threshold))
ctx.addFilter(.blur(radius: blur))

ctx.drawLayer { ctx in
// draw particles here
}
} symbols: {
// create symbols here
}
}
}
.ignoresSafeArea()
.background(.indigo)

LabeledContent("Threshold") {
Slider(value: $threshold, in: 0.01...0.99)
}
.padding(.horizontal)
LabeledContent("Blur") {
Slider(value: $blur, in: 0...40)
}
.padding(.horizontal)
}
}
}

I’ve removed two key parts from that code, both relating to the way canvas
symbols work. You see, the way this works is that we get to pass in as many
SwiftUI views as we want, either statically written out in code or
dynamically using ForEach, but the main thing is that we give each view a
unique identifier. Our Particle class conforms to Identifiable, which means
SwiftUI will take care of that part for us.

So, for the case of our lava particles, I’d like you to add the following code
in place of the // create symbols here comment:

ForEach(particleSystem.particles) { particle in
Circle()
.frame(width: particle.size, height: particle.size)
}

That creates a whole bunch of SwiftUI Circle shapes, each the correct size
for their bubble. Again, SwiftUI will silently tag each view for us because
of the Identifiable conformance, and that matters because when it comes to
the rendering code – where the // draw particles here comment is – we
need to look up each circle using the same identifier.

This lookup is done using the resolveSymbol(id:) method, which takes an


identifier to look up in the list of symbols. If it’s found we’ll be sent back
the resolved symbol to use, which will be a SwiftUI view we can draw just
like any other shape.

Remember, this time we’re using relative positions for our particles, so we
need to multiply the particle’s X and Y positions by our canvas’s width and
height respectively.

Go ahead and replace the // draw particles here comment with this:

for particle in particleSystem.particles {


guard let shape = ctx.resolveSymbol(id: particle.id) else
{ continue }
ctx.draw(shape, at: CGPoint(x: particle.x * size.width,
y: particle.y * size.height))
}

That’s us done, at least with the simplified version – try it out and see what
you think! The code is broadly similar to our previous particle system, but I
think it looks remarkably good.
Wouldn’t it be lava-ly?
Can we do better? Yes! If we create irregular polygons rather than circles,
we can adjust the length of each of their sides using animation, making the
shape change constantly. Thanks to our blur filter the shapes will still look
nice and smooth even though they are pretty rough polygons

I’ll be honest, this does take a bit of math. However, I have tried to remove
as much code as I possibly can, so hopefully it’s understandable.

First, the good news: Particle and ParticleSystem don’t need to change at
all, and only one small line of ContentView will change – most of what
we’re writing is new.

Now for the first piece of mathematics: we’re going to add an extension on
Array so that it conforms to both VectorArithmetic and
AdditiveArithmetic when it contains Double as its element type. These
protocols are used to provide animating values to SwiftUI, which in our
case means the polygon points we generate will animate to be longer or
shorter over time.
Yes, I know that extending a type we don’t own to support protocols we
don’t own is frowned upon, but it avoids having to create a wholly separate
type – this code really is as simple as I can make it!

There are quite a few parts here, so start by adding this empty extension so
we can fill it in bit by bit:

extension Array: VectorArithmetic, AdditiveArithmetic where


Element == Double {
}

First, we need to tell SwiftUI what zero looks like, which for us means an
empty array with 0 in. Add this to the extension now:

public static var zero = [0.0]

Next we need to add operator overloads for += and -= so that SwiftUI can
add one array to another. I’m going to assume (again, this is simplified!)
that our lava polygons don’t change their side counts over time, so we can
safely enumerate over both arrays and add or subtract each item.

So, add these two new methods to the extension:

public static func +=(lhs: inout [Double], rhs: [Double]) {


for (index, item) in rhs.enumerated() {
lhs[index] += item
}
}

public static func -=(lhs: inout [Double], rhs: [Double]) {


for (index, item) in rhs.enumerated() {
lhs[index] -= item
}
}

Next, the protocols require that add a scale(by:) method that multiplies
each item in the array by another number, so add this:
public mutating func scale(by rhs: Double) {
for (index, item) in self.enumerated() {
self[index] = item * rhs
}
}

Finally, the protocols require we add a - operator overload and a


magnitudeSquared property. Although the protocols require that these
exist, they won’t actually be used by our lava lamp effect so we can just
write dummies like these two:

public static func -(lhs: [Double], rhs: [Double]) ->


[Double] { [] }
public var magnitudeSquared: Double { 0 }

Okay, that completes our extension, so now we’re going to create two
structs to represent each blob: one that creates the shape using whatever
points are provided, and one that wraps the shape in a view that animates
over time.

The view is straightforward, but the shape is where more mathematics


comes in, so let’s start there.

First, we can create a new struct that conforms to Shape, and has a property
to store an array of numbers that represent how much we should shrink or
extend each side of the polygon. In order for this to be animated we need to
either store these values in an animatableData property containing these
values, or store them in another property and provide a getter/setter combo
for animatableData.

We’re aiming for the simplest solution, so that means using a single
property. Add this now:

struct AnimatablePolygonShape: Shape {


var animatableData: [Double]

init(points: [Double]) {
animatableData = points
}
}

Now for the mathematics: we need to create a path(in:) method that creates
a path from our polygon’s points, taking into account the animatable data
array that describes how much to shrink or extend each side.

We’ll fill this in piece by piece, starting with this:

func path(in rect: CGRect) -> Path {


Path { path in
// more code to come
}
}

If we want to draw completely regular polygons – i.e., polygons where


every side has the same length, we can do so by following a simple
procedure:

1. Calculating the center of our drawing rectangle, by halving the width


and height.
2. Calculate the radius of the largest circle that can fit into our space. This
is as simple as choosing the smallest of the two numbers from our
center.
3. Count from 0 to the number of sides we want to create, and calculate
how far we are through the number of sides as a fraction between 0
and 1. So, if we’re placing five sides, the first side will be 0.0, the third
side will be 0.5, and the last will be 1.0.
4. Multiply that fraction by pi times 2 so we get an angle between 0 and
2π radians, or 0 through 360 degrees, meaning that we know the angle
we need to use to create each point.
5. Get the X coordinate for this point by calculating the cosine of the
angle we just made, multiplying it by our radius, then adding the result
to our center X value.
6. Get the Y coordinate by doing the same thing, except using sine rather
than cosine.
Again, that creates regular polygons. We want irregular polygons using the
animatable Double array, which will contain numbers between 0.8 and 1.2
– one for each point in our polygon. So, once we’ve figured out the correct
X/Y coordinates for our regular polygon, we can multiply those values by
the matching element in animatableData so that each side can be made any
length from 80% to 120% its original size.

That might seem awfully complicated, but I think the code is surprisingly
straightforward given how good the finished effect looks!

So, go ahead and replace the // more code to come with this:

let center = CGPoint(x: rect.width / 2, y: rect.height / 2)


let radius = min(center.x, center.y)

let lines = animatableData.enumerated().map { index, value in


let fraction = Double(index) /
Double(animatableData.count)
let xPos = center.x + radius * cos(fraction * .pi * 2)
let yPos = center.y + radius * sin(fraction * .pi * 2)
return CGPoint(x: xPos * value, y: yPos * value)
}

path.addLines(lines)

Again, if we removed the * value part of that code each of our blobs would
be regular n-sided polygons, and if it weren’t for the fact that we only know
the shape’s size when path(in:) gets called we could calculate the polygon’s
points once in the initializer.

That defines a single irregular polygon shape, but in order to make it move
on the screen we need to wrap it in a SwiftUI view that has an animation in
place. This is much simpler, but still has a few tricks up its sleeve:

1. When the view is created we will immediately fill a points array with
8 random numbers between 0.8 and 1.2. These will be used for the
polygon.
2. The view will have a timer firing once a second, and each time it fires
we’ll create the points for our polygon.
3. Because we’ll have multiple lava bubbles at the same time, we’ll add a
1-second tolerance to our timer so iOS can definitely coalesce them –
rather than firing one timer, then waiting a split second and firing
another, iOS will be able group them all together so they fire at the
same time, which is more efficient.
4. We’ll attach an ease-in-out animation to our polygon shape, but it will
have a 3-second duration.

Yes, the timer fires three times as fast as the animation takes to complete,
which is intentional – it would look strange if one animation completed
fully before the next one started, so this way SwiftUI will always be
interpolating our animations smoothly. It’s a great little trick, but really
effective as you’ll see!

Add this new view now:

struct AnimatingPolygon: View {


@State private var points = Self.makePoints()
@State private var timer = Timer.publish(every: 1,
tolerance: 1, on: .main, in: .common).autoconnect()

var body: some View {


AnimatablePolygonShape(points: points)
.animation(.easeInOut(duration: 3), value:
points)
.onReceive(timer) { date in
points = Self.makePoints()
}
}

static func makePoints() -> [Double] {


(0..<8).map { _ in .random(in: 0.8...1.2) }
}
}

And that’s it! That completes all the major code changes to make the more
advanced lava lamp effect work.

To see it in action, we need to change one tiny part of ContentView: in the


symbols for the Canvas view, change Circle() to AnimatingPolygon(),
like this:
ForEach(particleSystem.particles) { particle in
AnimatingPolygon()
.frame(width: particle.size, height: particle.size)
}

Now give it a try and see what you think – you should see our lava blobs
now gently change shape all by themselves, even without colliding with
other blobs.

If you want to see the effect more clearly, try reducing the particle system
count down to 5 or so, or dragging the blur slider down to 0 so you see our
irregular polygons rather than the smoothed out blobs.

Hopefully I managed to find the right balance between mathematical


accuracy and explanations that are easy to understand, but more importantly
I hope you appreciate the final effect!
OceanofPDF.com
Blurred backgrounds
We’ve looked at lots of custom drawing using Canvas, but you can create
some really effective results just with plain old shapes and animations. To
demonstrate this, I want to show you how to we can create a soothing
background animation without even coming close to needing Canvas.

To make this work we’re going to create a new SwiftUI view called
BackgroundBlob, which will have a random alignment on the screen and a
random color. Inside there we need several modifiers:

1. The frame will be random between 200 and 500 wide, and 200 and
500 high.
2. That frame will be wrapped in a second frame using the random
alignment, so each blob will be placed at a random screen corner.
3. We’ll then use offset() to push the blob randomly between -400 and
+400 points both horizontally and vertically.
4. We can then rotate the blob by some amount, using a looping
animation to make it happen smoothly and continuously.
5. As soon as the blob is shown, we’ll adjust its rotation amount so the
animation is triggered.

The key to this effect is rotating after the offset, which causes the view to
be rotated around its original location so that it moves in interesting ways.

Create a new SwiftUI view called BackgroundBlob, then give it this code:

struct BackgroundBlob: View {


@State private var rotationAmount = 0.0
let alignment: Alignment = [.topLeading, .topTrailing,
.bottomLeading, .bottomTrailing].randomElement()!
let color: Color = [.blue, .cyan, .indigo, .mint,
.purple, .teal].randomElement()!

var body: some View {


Ellipse()
.fill(color)
.frame(width: .random(in: 200...500), height:
.random(in: 200...500))
.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: alignment)
.offset(x: .random(in: -400...400), y:
.random(in: -400...400))
.rotationEffect(.degrees(rotationAmount))
.animation(.linear(duration: .random(in:
2...4)).repeatForever(), value: rotationAmount)
.onAppear {
rotationAmount = .random(in: -360...360)
}
}
}

Tip: I’ve given the animations a very fast duration between 2 and 4
seconds, and also thrown in a bright purple color that really stands out.
That’s intentional because I want to really drive home how the effect works,
but in practice you’ll want to use 20 and 40 to get something much more
sedate, and you might also want to tweak the color palette too.

To get the full effect, we need to layer many of those background blobs on
top of each other in a ZStack with a solid background color, so replace
your current ContentView code with this:

struct ContentView: View {


var body: some View {
ZStack {
ForEach(0..<15) { _ in
BackgroundBlob()
}
}
.background(.blue)
}
}

Now try running it and see what you think! You should see a whole bunch
of ellipses swirling around on the screen, and it won’t look anything at all
like the soothing background animation I promised.
Fortunately, you’re just one line of code away from the final result, because
we just need to add a blur to each of the circles to make the blend together
nicely.

So, add this below the onAppear() modifier in BackgroundBlob:

.blur(radius: 75)

Now you should find all the colors mix together in more interesting ways,
and in particular using blobs that are the same blue color as the background
causes our shapes to get cut out in interesting ways.
Again, I’ve specifically chosen a fast animation and a bright purple in order
to make the effect more obvious – at the very least you’ll probably want to
adjust the animation to a range between 20 and 40 seconds to get something
gentler. You might also want to adjust the color palette to be more
harmonious, or on the other hand you might want something outrageously
bright like this:

let color: Color = [.blue, .blue, .blue, .cyan, .indigo,


.mint, .orange, .pink, .purple, .red, .teal,
.yellow].randomElement()!

Regardless of which approach you take, I think it’s a delightfully simple


effect and shows just how much work SwiftUI can do for us!
OceanofPDF.com
Magic with SpriteKit
Some people will see SpriteKit in this chapter title and skip right by, which
is sad because we’re about to make something absolutely magical.

You see, SpriteKit and SceneKit are Apple frameworks that are backed by
Metal, which is Apple’s high-performance 3D rendering framework. This
means SpriteKit is able to use Metal to create some extraordinarily
advanced effects, and of course SwiftUI is able to embed SpriteKit scenes
right into our view hierarchies.

To demonstrate this, we’re going to build a water rippling effect: we’re


going to write code that makes any SwiftUI views have animated ripples,
like they are being viewed through water. This takes a bit of thinking
because it involves multiple parts:

1. We’ll have a fragment shader, which is the named used for a tiny
program able to manipulate what is effectively a single pixel in a
texture. Modern CPUs have hundreds or even thousands of tiny GPU
cores dedicated to running these shaders, which means a shader can
potentially run hundreds of millions of times every second.
2. We’ll run that program inside an SKScene subclass, which is the
SpriteKit equivalent to View.
3. That SKScene subclass will be rendered from inside a SwiftUI view,
which will make sure it’s configured correctly.
4. We’ll then render that view inside another, adding some controls so
you can experiment with the water effect.

That sounds like a lot of work because it is a lot of work, but trust me: this
effect is incredible, and most definitely worth it.

Tip: If you’re a coffee drinker, make a brew now – there’s quite a lot to
digest here.

To get started, add import SpriteKit to the top of your file. Now add the
following the following class:
class WaterScene: SKScene {
}

This scene needs to have three properties: one for the sprite it will display,
which will be our SwiftUI view with the water effect applied, and a second
the UIImage we want to place inside there. We’ll be making that image
dynamically, but we still need to store it somewhere.

Important: UIImage is available only iOS, watchOS, and macOS Catalyst.


If you intend to target macOS, use NSImage instead.

Add these two properties to the water scene now:

private let spriteNode = SKSpriteNode()


var image: UIImage?

I mentioned a third property, and this is the tricky one: we need to create
our shader, which means writing some fragment shader code. This is not
Swift code – it’s much, much lower-level than that, because of the need to
run literally millions of times every second.

SpriteKit lets us write shaders either in GLSL (the OpenGL Shading


Language) or MSL (the Metal shading language). Both these two get
compiled on the device when the app runs, so it can be fully optimized for
whatever hardware it’s running on.

Of the two options, GLSL is significantly simpler than MSL, and helpfully
the system will automatically convert GLSL to MSL when compiling our
shader. So, we’ll be using GLSL here – it’s just as fast, but way less code
for us.

Go ahead and add this property to the water scene:

let waterShader = SKShader(source: """


void main() {
// more code to come
}
""")

That creates a main() function in our shader, with a single comment inside.
We’re going to write that line by line, so I can explain what it all does.
Before I dive into the code, though, I want to summarize how the effect
works:

1. Imagine a grid of pixels.


2. Our shader will effectively be run on every pixel in that grid.
3. It will be given the full grid, along with the X/Y coordinates it’s
supposed to use.
4. Rather than returning pixel at the X/Y coordinate that was requested,
we will instead return a different pixel from a nearby coordinate –
we’re simulating the water refracting light.
5. Which nearby coordinate we choose depends on the control settings
the user provides.

All set?

Okay, the first step is to figure out how fast we want our effect to happen.
This can be done by reading two values that will be passed in externally:
u_time will tell us how much time has passed since the shader was created,
and u_speed will tell us how fast the user has requested for the water to
move. If we multiply those two together we are effectively making time
move faster, so the water will ripple more quickly.

Put this in your main() function now:

float speed = u_time * u_speed;

Next we need to decide which pixel we’re going to read. Here we need to
use several more external values: v_tex_coord will tell us which pixel we
were asked to read, u_frequency will contain how fast the user wants
ripples to be created, and u_strength will tell us how strong the user wants
the effect to be.
Figuring out which pixel to use involves a small amount of mathematics, so
let me break it down:

We already set speed to the current time multiplied by how fast the
user wants the ripples to happen.
If we take multiply that speed by the ripple frequency it means the
pixel we choose will change faster, creating more or fewer ripples.
We don’t want all the pixels to change uniformly: if they all moved up
by one or down by three it would look like the whole image was
moving up a bit rather than ripples.
So, we’ll add the pixel’s original X coordinate to speed first, then
multiply by frequency.
We’ll put the result of that operation through cos() to get a value
between -1 and 1 telling us which pixel direction we want to move in.
Finally, we’ll multiply that by the strength the user asked for.

Here’s a worked example, assuming our X coordinate is 22, our speed is 8,


our frequency is 4 and our strength is 2:

We add the X coordinate to our speed, making 30.


We multiply that by frequency, making 120.
We calculate the cosine of that to produce some long number that is
approximately 0.81.
We multiply that by strength to get 1.62.

So, we read 1.62 pixels to the left – yes it’s not a whole number, but Metal
will figure it out. Alternatively, if we were reading X:23 rather than X:22,
we would get cos((23 + 8) * 4) * 2, which is approximately -0.19, so the
pixel offset we read is slightly different, which creates the ripple effect.

Add this line of code next:

v_tex_coord.x += cos((v_tex_coord.x + speed) * u_frequency) *


u_strength;

Tip: Remember, v_tex_coord tells us which X/Y coordinates we were


asked to read.
To get the Y offset, we do the same thing as X except now using sin() rather
than cos():

v_tex_coord.y += sin((v_tex_coord.y + speed) * u_frequency) *


u_strength;

At this point we now know exactly which pixel we want to read, so we need
to read that value and send it back. GLSL provides three helpers here:

The texture we’re working with is provided as u_texture.


We can read a pixel from there by calling the built-in texture2D()
function, passing a texture and a pixel coordinate.
The return value from texture2D() will be the new color to use, and
we can assign that to the special value gl_FragColor – that’s what
SpriteKit will use for the finished pixel color for our shader.

Add this final line to the function now:

gl_FragColor = texture2D(u_texture, v_tex_coord);

Tip: If we try and read pixels from outside the bounds of the texture, Metal
will automatically wrap them around to the other side for us. As a result, it
usually works better if your SwiftUI views have a little padding around
them to avoid this wrapping.

I know it took a lot of explaining, but the entire function is really just this:

void main() {
float speed = u_time * u_speed;

v_tex_coord.x += cos((v_tex_coord.x + speed) *


u_frequency) * u_strength;
v_tex_coord.y += sin((v_tex_coord.y + speed) *
u_frequency) * u_strength;

gl_FragColor = texture2D(u_texture, v_tex_coord);


}
Fragment shaders are easy to write once you get the hang of them – this one
is taken from a huge library of them I wrote for my ShaderKit repository,
which you can find here: https://github.com/twostraws/ShaderKit/.

We’re done with the shader now, but we still have some SpriteKit work to
do. First, when our scene loads we need to do some quick set up work:

Give our scene a transparent background color


Make it resizable to whatever size SwiftUI wants,
Make sure our GLSL shader is assigned to the sprite node that will
contain our rendered SwiftUI view.
Add our sprite node to the scene, so it gets drawn.

This can all be done through the sceneDidLoad() method, which is the
SpriteKit equivalent to viewDidLoad() from UIKit. Add this method to the
class now:

override func sceneDidLoad() {


backgroundColor = .clear
scaleMode = .resizeFill

spriteNode.shader = waterShader
addChild(spriteNode)
}

Next we need to write an updateTexture() method, that will convert our


UIImage into a texture, place it into the sprite, and make sure it’s all sized
and positioned correctly.

Add this method now:

func updateTexture() {
guard view != nil else { return }
guard let image else { return }

let texture = SKTexture(image: image)


spriteNode.texture = texture
spriteNode.size = texture.size()
spriteNode.position.x = frame.midX
spriteNode.position.y = frame.midY
}

The very first line of updateTexture() checks whether our scene is


currently being shown in a view, because if it isn’t we silently exit to avoid
wasting time. When our scene actually is being moved into a real view,
SpriteKit will call a separate method named didMove(to:), and that’s our
chance to call updateTexture() so everything gets positioned correctly.

Add this now:

override func didMove(to view: SKView) {


updateTexture()
}

That finishes all our WaterScene code, so we’re half way there!

The next step is to create a SwiftUI view that’s responsible for creating and
managing our water scene. This is the bridge between SpriteKit and the rest
of our SwiftUI app – we want this thing to be neatly encapsulated so that
we don’t have to worry about SpriteKit every time we use it.

Start by creating a new SwiftUI view called WaterEffect, and give it this
code:

struct WaterEffect<Content: View>: View {


@State private var scene = WaterScene()

var speed: Double


var strength: Double
var frequency: Double
@ViewBuilder var content: () -> Content

var body: some View {


}
}

That doesn’t have a real body property yet, but before we add that I want to
explain what we have so far:
1. We have a WaterScene instance stored as state, meaning that SwiftUI
will keep it alive without watching it for changes.
2. We have properties to store the three user-configurable effect inputs:
speed, strength, and frequency.
3. Most importantly, we also have a view builder that will generate some
kind of SwiftUI content. This is what we’ll be using in our game
scene.

Tip: We need to make the struct generic so that it’s able to render any kind
of SwiftUI views.

Inside the body property comes the real work. You see, our content
property will contain a whole bunch of SwiftUI views, and we need to pass
those to the water scene to render. That thing expects a UIImage, not
SwiftUI views, so the first thing we need to do is render those SwiftUI
views as an image.

This can be done using the ImageRenderer struct, which accepts any views
as its input and has a uiImage property that contains the resulting image.

Add these two to your body property now:

let renderer = ImageRenderer(content: content())


let image = renderer.uiImage

Tip: If you’re using macOS, access the nsImage property rather than
uiImage.

Now that we have our view image ready to go, we can read its size or
replace it with .zero if there was a problem:

let size = image?.size ?? .zero

Next we need to send all those user values into the shader. We have speed,
strength, and frequency as properties of this view, and our shader expects
u_speed, u_strength, and u_frequency to be provided in order to work. All
those “u” names are there for a reason: these are called uniforms, which are
the name for fixed values we assign to a shader to customize it. These are
specified by name and value, with the names matching the names used
inside the shader code.

There is one small complication here, but it’s best explained after you see
the code. Add this to your body property next:

scene.waterShader.uniforms = [
SKUniform(name: "u_speed", float: Float(speed)),
SKUniform(name: "u_strength", float: Float(strength) /
20.0),
SKUniform(name: "u_frequency", float: Float(frequency))
]

Do you see the complication? Rather than sending our strength value in
directly, we divide it by 20 to make it a lot smaller – we only need very
small offsets to make this work, but it’s hard to work with very small
values.

Moving on, we can now assign our new image to the scene, and call its
updateTexture() method:

scene.image = image
scene.updateTexture()

Finally, we can send back a new SpriteView with that scene in place,
making sure to enable transparency on it and also give it a frame matching
our rendered view’s size:

return SpriteView(scene: scene, options: .allowsTransparency)


.frame(width: size.width, height: size.height)

You’ll be pleased to know we’re now effectively done with our water code
– almost everything from now on is just using it somehow.

Our WaterEffect view is capable of rendering any kind of SwiftUI view, so


we’re going to create a little sandbox that renders adds some user
customization points.

Replace your current ContentView with this:

struct ContentView: View {


@State private var text = "Hello"
@State private var speed = 0.5
@State private var strength = 0.5
@State private var frequency = 5.0

var body: some View {


VStack {
WaterEffect(speed: speed, strength: strength,
frequency: frequency) {
// Views to render here
}

TextField("Enter a message", text: $text)


.textFieldStyle(.roundedBorder)

LabeledContent("Speed") {
Slider(value: $speed)
}

LabeledContent("Strength") {
Slider(value: $strength)
}

LabeledContent("Frequency") {
Slider(value: $frequency, in: 5...25)
}
}
.padding()
}
}

What should go in place of that // Views to render here comment? It’s


down to you! Something like this is a good test of the effect:

Circle()
.fill(.red)
.frame(width: 150, height: 150)
.padding()
.overlay(Circle().stroke(.red, lineWidth: 4))
.overlay(Text(text).font(.title).foregroundColor(.white))
.padding()

Reminder: Having some padding around your view is a good idea, because
it avoids Metal wrapping pixel coordinates around at the edges.

Now try running the effect and see what you think! You’ll see you can type
into the text box to have the water effect update immediately, and of course
dragging the sliders around is also instant. You might also notice how low
the CPU usage is – Metal is fast!

There’s one last thing I want to do before we’re done, and it’s to fix a small
problem you might not have even noticed. You see, when using
ImageRenderer to convert views into images, SwiftUI will use an image
scale of 1.0, which means a 400x400 view will be rendered at 400x400
pixels. That might sound good, but keep in mind that all modern devices
have screen resolutions that are either 2x or 3x resolution, which means on
an iPhone 14 a 400x400 view should render into a 1200x1200 image.

To fix this, we need to add a property to the WaterEffect view that will
read the correct scale for the display:

@Environment(\.displayScale) var displayScale

Now we can assign that to the image renderer like this:

let renderer = ImageRenderer(content: content())


renderer.scale = displayScale

It’s a small difference, but it does mean the rendered SwiftUI views will
look pin sharp now, just as nature intended.

That’s our code all finished now, so go and experiment! And if you’re
feeling really brave, try adapting some of the other effects in
https://github.com/twostraws/ShaderKit – you’ll see a whole bunch of them
in the Shaders directory.

OceanofPDF.com
Chapter 6
Performance
OceanofPDF.com
Delaying work…
There are all sorts of tips to help you make your SwiftUI code as fast as
possible, but the best place to start is with this rule: the fastest code is the
code that never executes. In SwiftUI terms this means taking steps to skip
work where possible, or at least delaying work as much as possible.

For example, if you’re updating your user interface as the user interacts
with it, does it need to update for every single small change the user makes
– for every letter they types, or even pixel they drag a slider? If you’re
generating a QR code from their data, or perhaps even using the SpriteKit
water effect we made in an earlier chapter, re-rendering for every pixel of a
slider drag is clearly overkill.

For many operations, you’ll often find that introducing a small debounce
dramatically improves performance while sacrificing little to none of your
user experience. Debouncing is the practice of waiting for a certain amount
of time before taking an action. For example, if we’re debouncing a text
field for 1 second, the user needs to stop typing for a full second before we
update our app state – they can type as much as they want and no work will
happen until they finally stop.

Even a debounce as low as 0.1 – a tenth of a second! – can lead to huge


performance improvements, because the alternative might be updating
views as 120fps, which equates to just 0.008 of a second.

We can implement a simple generic debounce system using Combine,


which will:

Be generic over some kind of T, because we don’t care what kind of


data we’re trying to debounce.
Have two published properties: one for the input value, and one for the
output. The input property will always be the latest value, and the
output property will be the most recently debounced version.
Have a private AnyCancellable property that stores our actual
debouncing work. This comes from the Combine framework, and
allows us to cancel the work if needed.

Start with this:

import Combine

class Debouncer<T>: ObservableObject {


@Published var input: T
@Published var output: T

private var debounce: AnyCancellable?


}

We need to write an initializer that accepts two values from the user: the
initial value to use for both the input and output (because they will be the
same to start with), and also how long we want our debounce to wait for.

The way this works is simple: elsewhere in SwiftUI we’ll adjust the input
property directly, but we’ll attach to that Combine debounce() and sink()
operators so that after a period of time has passed we copy the value from
input into output. Because output uses @Published, changing it will
automatically cause any observing SwiftUI views to reinvoke their body
property.

Go ahead and add this initializer now:

init(initialValue: T, delay: Double = 1) {


self.input = initialValue
self.output = initialValue

debounce = $input
.debounce(for: .seconds(delay), scheduler:
DispatchQueue.main)
.sink { [weak self] in
self?.output = $0
}
}

And that’s it – that’s our entire Debouncer class done already. To try it out,
create instances of it using @StateObject, then bind your controls to input
and your readouts to output. For example, this debounces a text field and a
slider:

struct ContentView: View {


@StateObject private var text = Debouncer(initialValue:
"", delay: 0.5)
@StateObject private var slider = Debouncer(initialValue:
0.0, delay: 0.1)

var body: some View {


VStack {
TextField("Search for something", text:
$text.input)
.textFieldStyle(.roundedBorder)
Text(text.output)

Spacer().frame(height: 50)

Slider(value: $slider.input, in: 0...100)


Text(slider.output.formatted())
}
}
}

I encourage you to try that out, because you’ll see how our view waits until
the user pauses for a moment before reloading.

For work that takes place outside of bindings, use Swift’s Task to spin off
the work, sleep for some amount of time, then execute it after the delay
finishes. This works well because if you need to schedule the same work
while the sleep is happening, you can cancel the task and schedule it again –
it’s debouncing, just done without Combine.

As an example, this view model can do some work immediately, or


schedule some work to happen after a 3-second delay:

class ViewModel: ObservableObject {


private var refreshTask: Task<Void, Error>?
var workCounter = 0

func doWorkNow() {
workCounter += 1
print("Work done: \(workCounter)")
}

func scheduleWork() {
refreshTask?.cancel()

refreshTask = Task {
try await Task.sleep(until: .now + .seconds(3),
clock: .continuous)
doWorkNow()
}
}
}

Note: If you need to support older versions of Apple’s operating systems,


use Task.sleep(nanoseconds: 3_000_000_000) to get the same result.

That view model exposes both scheduleWork() and doWorkNow(), so you


can choose which you use depending on circumstances:

struct ContentView: View {


@StateObject private var viewModel = ViewModel()

var body: some View {


VStack {
Button("Do Work Soon", action:
viewModel.scheduleWork)
Button("Do Work Now", action:
viewModel.doWorkNow)
}
}
}

OceanofPDF.com
…or skipping it entirely
Delaying work is a good start, not least because it helps your apps load
faster and keeps your views responsive more of the time. Even better is
avoiding work entirely – looking for ways to skip work that isn’t necessary,
beyond things I’ve covered elsewhere such as preferring ternary
conditionals to if in a view builder.

For example, it’s common to write SwiftUI code that interacts with classes
that either don’t conform to ObservableObject, or do conform but we
don’t happen to care in this circumstance. In these places you should use
@State to store a reference to the class, as opposed to either let (which
would destroy and reallocate the class instance every time the view was
recreated) or @StateObject (which would monitor the object for changes).

You can see this in action in the following code, which uses @State to store
a Core Image context – something that is resource-intensive to create, and
so is best kept alive:

import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftUI

struct ContentView: View {


@State private var context = CIContext()
@State private var name = "Paul"

var body: some View {


VStack {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.padding()

Image(uiImage: generateQRCode(from: "\(name)"))


.resizable()
.interpolation(.none)
.frame(width: 200, height: 200)
}
}
func generateQRCode(from string: String) -> UIImage {
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(string.utf8)

if let output = filter.outputImage {


if let cgImage = context.createCGImage(output,
from: output.extent) {
return UIImage(cgImage: cgImage)
}
}

return UIImage(systemName: "xmark.circle") ??


UIImage()
}
}

Using @State like this is effectively using it as a cache – we’re storing


something persistently, but not trying to do any change tracking.

Another common place where wasted work happens is in onAppear(). First


I should say that using onAppear() is broadly a good thing: it’s much,
much better to take work out of view initializers and put it into onAppear()
where possible, because initializers are called far more frequently than
onAppear().

That being said, onAppear() is still called whenever a view is presented, so


if you’re putting work in there you might find it’s being repeated
pointlessly. You can see the problem in this example code:

struct ContentView: View {


var body: some View {
TabView {
ForEach(1..<6) { i in
ExampleView(number: i)
.tabItem { Label(String(i), systemImage:
"\(i).circle") }
}
}
}
}

struct ExampleView: View {


let number: Int
var body: some View {
Text("View \(number)")
.onAppear {
print("View \(number) appearing")
}
}
}

When that runs, you’ll see that moving between the tabs repeats the
onAppear() code, even when you’ve already visited a tab. Is that what you
want? If so, you have nothing to worry about, but if you were using
onAppear() as a way to delay initialization of values for this view, you
might find you only really want that work to happen once.

The good news is that we can do better: we can create an onFirstAppear()


modifier that runs its closure only when a view appears for the very first
time:

struct OnFirstAppearModifier: ViewModifier {


@State private var hasLoaded = false
var perform: () -> Void

func body(content: Content) -> some View {


content.onAppear {
guard hasLoaded == false else { return }
hasLoaded = true
perform()
}
}
}

extension View {
func onFirstAppear(perform: @escaping () -> Void) -> some
View {
modifier(OnFirstAppearModifier(perform: perform))
}
}

And now you have the option of using onAppear() when you want some
code to always run on appearance, or onFirstAppear() when you want to
do the work only once:
Text("View \(number)")
.onFirstAppear {
print("View \(number) appearing")
}

Finally, if you’re targeting multiple platforms, don’t be afraid to write a


handful of platform-specific modifiers that lock off certain code on a case-
by-case basis. For example, if you want some text to have zero padding on
watchOS, you can add a method like this to make such a change apply only
to watchOS:

public extension View {


func watchOS<Content: View>(_ modifier: @escaping (Self)
-> Content) -> some View {
#if os(watchOS)
modifier(self)
#else
self
#endif
}
}

That does nothing at all for all operating systems apart from watchOS, so
the Swift compiler will optimize it out at build time. Use it like this:

Text("Hello, world!")
.watchOS {
$0.padding(0)
}

OceanofPDF.com
Watching for changes
One of the most common SwiftUI performance sinks is recomputing views
as a result of change notifications.

When these happen as a result of @State changes it’s usually obvious: the
user typed into your text field or dragged your slider, and so your view’s
body property needs to be reinvoked. That’s unavoidable for the most part.
Where things get more complex is when some external object changes and
your view is recomputed – what changed and why?

There are a handful of techniques I use here, any or all of which might
prove useful to you depending on the circumstances.

First, here’s a simple set up that triggers external notifications on a regular


basis:

class AutorefreshingObject: ObservableObject {


var timer: Timer?

init() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1,
repeats: true) { _ in
self.objectWillChange.send()
}
}
}

struct ContentView: View {


@StateObject private var viewModel =
AutorefreshingObject()

var body: some View {


Text("Example View Here")
}
}

If you run that you’ll see it looks and works just like any other SwiftUI
view – there’s no indication it’s doing vast amounts of work thanks to the
change notifications coming in.

To address this, I want to introduce you to a neat trick I learned from Peter
Steinberger, which is to create a random color extension such as this one:

extension ShapeStyle where Self == Color {


static var random: Color {
Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}

Use it like this:

Text("Example View Here")


.background(.random)

With that in place it becomes obvious that are view is being recomputed,
because the text will literally flash through colors on the screen:

That doesn’t explain why something changed, though, and for that you need
the View._printChanges() method. This can be called from inside any
view’s body property, and will automatically print what triggered a change.

Because of the way Swift’s result builders work, to use _printChanges()


you either need to assign it to a value such as _ (underscore), or add an
explicit return to your view body. In the first case you’d write this:

var body: some View {


let _ = Self._printChanges()
Text("Example View Here")
}

And in the second this:


var body: some View {
Self._printChanges()
return Text("Example View Here")
}

Either way, you’ll see output such as ContentView: _viewModel changed,


which is telling us that the body property was reinvoked because that
particular object announced a change.

This kind of performance problem is particularly common when apps grow


over time. When you’re just starting out with a small app, it’s common to
create your view model right in your App struct, and post it into the
SwiftUI environment for all your views to share. But as your app grows and
more views depend on that data, you might come to find that a small change
in one view causes a cascade of refreshes to happen elsewhere.

Like I said when we looked at environment keys, every time you make a
view use @ObservableObject, you are effectively creating a dependency
on that data – you’re telling SwiftUI to refresh your view when that data
changes, even if your view doesn’t actually care about the precise thing that
changed.

This isn’t a theoretical problem, and in fact it’s something I hit with my
own Control Room app for macOS – it’s on GitHub here:
https://github.com/twostraws/ControlRoom. This project started out small
but grew extensively over time, and suddenly @AppStorage values that
were fine before were causing significant performance problems – typing
into a text field took a whole second for every character to appear!

Do you need to make your view dependent on an external object? If so,


does it need to be every property on that object, or just part of it? We looked
at how to solve this in the environment keys chapter, so if you find your
views are refreshing more often than you’d like that’s the best place to start.

If you’re stuck – if you have a view that’s changing and you’re not sure
why even with _printChanges() – then I recommend adding a handful of
View extensions that let us inject pieces of code directly into SwiftUI’s
views.
Here are the three I rely on the most:

extension View {
func debugPrint(_ value: @autoclosure () -> Any) -> some
View {
#if DEBUG
print(value())
#endif

return self
}

func debugExecute(_ function: () -> Void) -> some View {


#if DEBUG
function()
#endif

return self
}

func debugExecute(_ function: (Self) -> Void) -> some


View {
#if DEBUG
function(self)
#endif

return self
}
}

The first of those prints a message when the modifier is executed, the
second runs arbitrary code, and the third runs arbitrary code while also
giving access to the current view. All three use #if DEBUG to ensure the
diagnostic code never leaves Xcode – as soon as you ship these apps to the
App Store that code will be compiled out completely.

If you’ve tried background colors, checked _printChanges(), and even


tried using debugExecute() to check what’s happening, and you’re still not
sure where the problem is, there’s one last option: adding an assert()
modifier to View, so that you can check exactly what’s happening and
hopefully catch the problem.
It looks like this:

extension View {
public func assert(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) -> some View {
Swift.assert(condition(), message(), file: file,
line: line)
return self
}
}

That uses @autoclosure to avoid doing any work that isn’t needed, and
also calls down to Swift’s internal assert() function that gets compiled out
of App Store releases.

Use it like this:

struct ContentView: View {


@State private var counter = 0
let timer = Timer.publish(every: 0.1, on: .main, in:
.common).autoconnect()

var body: some View {


Text("Example View Here")
.onReceive(timer) { _ in
counter += 1
}
.assert(counter < 50, "Timer exceeded")
}
}

That adds an explicit, code-level check that counter must be less than 50 at
all times, and as soon as the view body is reinvoked when that isn’t true
Xcode will stop execution and print the message “Timer exceeded”.

OceanofPDF.com
The SwiftUI cycle of events
If there is one thing – just one thing – I would recommend everyone do in
order to better understand the sequence of events SwiftUI goes through
when working with all our views and modifiers, and in doing so get a much
better idea of what code is being run when our apps execute, it is this:
create a bunch of test structs that represent your app, some views,
modifiers, properties, initializers, etc, and make them all print out messages
explaining what’s going on. It takes only a few minutes to do, and yet most
people will be surprised what they see when it runs.

If you’re not sure where to start, something like this is a great beginning:

@main
struct MyApp: App {
@State private var property = ExampleProperty(location:
"App")

var body: some Scene {


print("In App.body")

return WindowGroup {
NavigationStack {
ContentView()
}
}
}

init() {
print("In App.init")
}
}

struct ExampleProperty {
init(location: String) {
print("Creating ExampleProperty from \(location)")
}
}

struct ExampleModifier: ViewModifier {


init(location: String) {
print("Creating ExampleModifier from \(location)")
}

func body(content: Content) -> some View {


print("In ExampleModifier.body()")
return content
}
}

struct ContentView: View {


@State private var property = ExampleProperty(location:
"ContentView")

var body: some View {


print("In ContentView.body")

return NavigationLink("Hello, world!") {


DetailView()
}
.modifier(ExampleModifier(location: "ContentView"))
.task { print("In first task") }
.task { print("In second task") }
.onAppear { print("In first onAppear") }
.onAppear { print("In second onAppear") }
}

init() {
print("In ContentView.init")
}
}

struct DetailView: View {


@State private var property = ExampleProperty(location:
"DetailView")

var body: some View {


print("In DetailView.body")

return Text("Hello, world!")


.modifier(ExampleModifier(location:
"DetailView"))
.task { print("In detail task") }
.onAppear { print("In detail onAppear") }
}

init() {
print("In DetailView.init")
}
}

When that runs you’ll see a whole lot of output, and while you use it some
more will come out. In particular, watch out for:

1. The App property being created before App.init() is called.


2. Swift running DetailView.init() immediately, long before we even
press the button to show it. Heck, it’s run twice!
3. The ContentView.body property being executed more than once.
4. The two onAppear() modifiers being executed in the order they
appear in the code, and the two task() modifiers being executed in the
order they appear in the code, but onAppear() running before task().
5. Both the onAppear() and task() modifiers in DetailView only being
run when the view is shown.

The point of this exercise is to remind you that SwiftUI can create our
views and reinvoke their body properties as often as it wants, whenever it
wants – even just launching our app run DetailView.init() twice, and it’s
not even visible. This is why it’s so absolutely critical to keep your
initializers as simple as possible: do no slow work in there, and under no
circumstances attempt to access network data from there!

Instead, push work back into onAppear() or task() where possible, so you
allow SwiftUI to call init() and body frequently without hiccups. These are
called when your views are added to the active view hierarchy – when they
are being placed onto the screen – and so you can be sure the work you’re
doing is actually useful.

OceanofPDF.com

You might also like