Tam A., Begbie C. - SwiftUI Apprentice (2nd Edition) - 2023
Tam A., Begbie C. - SwiftUI Apprentice (2nd Edition) - 2023
SwiftUI Apprentice
By Audrey Tam & Caroline Begbie
Notice of Rights
All rights reserved. No part of this book or corresponding materials (such as text,
images, or source code) may be reproduced or distributed by any means without
prior written permission of the copyright owner.
Notice of Liability
This book and all corresponding materials (such as source code) are provided on an
“as is” basis, without warranty of any kind, express or implied, including but not
limited to the warranties of merchantability, fitness for a particular purpose, and
noninfringement. In no event shall the authors or copyright holders be liable for any
claim, damages or other liability, whether in action of contract, tort or otherwise,
arising from, out of or in connection with the software or the use of other dealing in
the software.
Trademarks
All trademarks and registered trademarks appearing in this book are the property of
their own respective owners.
2
SwiftUI Apprentice
3
SwiftUI Apprentice
4
SwiftUI Apprentice
5
SwiftUI Apprentice
6
SwiftUI Apprentice
7
SwiftUI Apprentice
8
SwiftUI Apprentice
9
SwiftUI Apprentice
10
SwiftUI Apprentice
11
SwiftUI Apprentice
List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
NavigationStack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 614
Using the Internet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 617
navigationDestination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 622
Using Custom Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 630
One Last Thing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
Key Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 637
Chapter 23: Just Enough Web Stuff . . . . . . . . . . . . . . . . . . . . . . . . 638
Servers & Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 639
REST API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 641
Sending & Receiving HTTP Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 642
Exploring metmuseum.org . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 646
POST Request & Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 652
Key Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
Chapter 24: Downloading Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 657
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 658
URLSession . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 658
Creating a REST Request URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
Sending the Request With URLSession . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 665
Decoding JSON. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 666
Downloading Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 670
Downloading Data in Your App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 671
Showing a Progress View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683
Key Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 684
Chapter 25: Widgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 685
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 686
WidgetKit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 686
Adding a Widget Extension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 687
Creating Entries From Your App’s Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
Creating Widget Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
12
SwiftUI Apprentice
13
L Book License
• You are allowed to use and/or modify the source code in SwiftUI Apprentice in as
many apps as you want, with no attribution required.
• You are allowed to use and/or modify all art, images and designs that are included
in SwiftUI Apprentice in as many apps as you want, but must include this
attribution line somewhere inside your app: “Artwork/images/designs: from
SwiftUI Apprentice, available at www.kodeco.com”.
• The source code included in SwiftUI Apprentice is for your personal use only. You
are NOT allowed to distribute or sell the source code in SwiftUI Apprentice without
prior authorization.
• This book is for your personal use only. You are NOT allowed to reproduce or
transmit any part of this book by any means, electronic or mechanical, including
photocopying, recording, etc. without previous authorization. You may not sell
digital versions of this book or distribute them to friends, coworkers or students
without prior authorization. They need to purchase their own copies.
All materials provided with this book are provided on an “as is” basis, without
warranty of any kind, express or implied, including but not limited to the warranties
of merchantability, fitness for a particular purpose and noninfringement. In no event
shall the authors or copyright holders be liable for any claim, damages or other
liability, whether in an action of contract, tort or otherwise, arising from, out of or in
connection with the software or the use or other dealings in the software.
All trademarks and registered trademarks appearing in this guide are the properties
of their respective owners.
14
Before You Begin
This section tells you a few things you need to know before you get started, such as
what you’ll need for hardware and software, where to find the project files for this
book, and more.
15
i What You Need
• A Mac computer with an Intel or ARM processor. Any Mac that you’ve bought
in the last few years will do, even a Mac mini or MacBook Air.
• Xcode 14.2 or later. Xcode is the main development environment for building iOS
Apps. It includes the Swift compiler, the debugger and other development tools
you’ll need. You can download the latest version of Xcode for free from the Mac
App Store.
16
ii Book Source Code &
Forums
• https://github.com/kodecocodes/suia-materials/tree/editions/2.0
Forums
We’ve also set up an official forum for the book at https://forums.kodeco.com/c/
books/swiftui-apprentice. This is a great place to ask questions about the book or to
submit any errors you may find.
17
“To the kodeco.com community, who help create my happy
place.”
— Audrey Tam
— Caroline Begbie
18
SwiftUI Apprentice About the Team
Richard Critz did double duty as editor and final pass editor for
this book. He has been doing software professionally for over 40
years, working on products as diverse as CNC machinery, network
infrastructure, and operating systems. He discovered the joys of
working with iOS beginning with iOS 6. Yes, he dates back to punch
cards and paper tape. He’s a dinosaur; just ask his kids.
19
v How to Read This Book
This book is designed to take you from zero to hero! Each chapter builds on the code
and concepts from its predecessors, so you’ll want to work your way through them in
order. To help you navigate on your journey, here are some conventions we use:
• Filenames, text you enter into dialog boxes, items you look for on screen all appear
in bold.
• Names of things you find in your code — such as variables, properties, types,
protocols and method names — appear in a monospaced typeface.
• Deeper explanations of Swift language topics are marked with Swift Dive.
• Watch for Skills you’ll learn in this section to get a quick overview of specific
new things you’ll learn.
20
Section I: Your First App:
HIITFit
At WWDC 2019, Apple surprised and delighted the developer community with the
introduction of SwiftUI, a declarative way of building user interfaces. With SwiftUI,
you build your user interface by combining fundamental components such as colors,
buttons, text labels, lists and more into beautiful and functional views. Your views
react to changes in the data they display, updating automatically without any
intervention from you!
• Understand how data moves in a SwiftUI app and how to make it persist.
21
1 Chapter 1: Checking Your
Tools
By Audrey Tam
You’re eager to dive in and create your first iOS app. If you’ve never used Xcode
before, take some time to work through this chapter. You want to be sure your tools
are working and learn how to use them efficiently.
22
SwiftUI Apprentice Chapter 1: Checking Your Tools
Getting Started
To develop iOS apps, you need a Mac with Xcode installed. If you have an account on
GitHub or similar, you can connect to that from Xcode.
macOS
To use the SwiftUI canvas, you need a Mac running Catalina (V10.15) or later. To
install Xcode, your user account must have administrator status.
Xcode
To install Xcode, you need 23 GB free space on your Mac’s drive.
➤ Open the App Store app, then search for and GET Xcode. This is a large download
— almost 8GB — so it takes a while. Fix yourself a snack while you wait or, to stay in
the flow, browse Chapter 12, “Apple App Development Ecosystem”.
➤ When the installation finishes, OPEN it from the App Store page:
23
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: You probably have your favorite way to open a Mac application, and it
will work with Xcode, too. Double-click it in Applications. Or search for it in
Spotlight. Or double-click a project’s .xcodeproj file.
The first time you open Xcode after App Store installation, you’ll see this window:
24
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ iOS and macOS are built into the 23 GB. Click Install and enter your Mac login
password in the window that appears. This doesn’t take long, so don’t go away.
➤ When this installation process finishes, you’ll see this Welcome window:
➤ Click Create a new Xcode project. Or, if you want to do this without the
Welcome window, press Shift-Command-N or select File ▸ New ▸ Project… from
the menu.
25
SwiftUI Apprentice Chapter 1: Checking Your Tools
26
SwiftUI Apprentice Chapter 1: Checking Your Tools
• For Organization Identifier, type the reverse-DNS of your domain name. If you
don’t have a domain name, just type something that follows this pattern, like
org.audrey. The grayed-out Bundle Identifier changes to your-org-id.MyFirst.
When you submit your app to the App Store, this bundle identifier uniquely
identifies your app.
➤ Click Next. Here’s where you decide where to save your new project.
27
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: When a project is open, you can find its location by selecting File ▸
Show in Finder from the Xcode menu. If you forget where you saved a project,
try looking in the Xcode menu File ▸ Open Recent.
➤ If you’re saving this project to a location that is not currently under source
control, click the Source Control checkbox to create a local Git repository. Later in
this chapter, you’ll learn how to connect this to a remote repository.
28
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: If you don’t see the file extension .swift in the Project navigator, press
Command-, to open Settings… then, in the General tab, set File Extensions
to Show All:
29
SwiftUI Apprentice Chapter 1: Checking Your Tools
The Xcode window has three main panes: Navigator, Editor and Inspectors. When
the app is running, the Debug Area opens below the Editor. When you’re viewing a
SwiftUI View file in the Editor, you can view the preview canvas side-by-side with
the code.
The toolbar button just above the Navigator pane hides or shows it. The same is true
for the Inspectors pane. The debug area has a hide/show button in its own toolbar.
You can also hide any of these three panes by dragging its border to the edge of the
Xcode window.
Note: There’s a handy cheat sheet of Xcode keyboard shortcuts in the assets
folder for this chapter. It’s not a complete list, but it covers the ones people
use most.
Navigator
The Navigator has nine tabs. When the navigator pane is hidden, you can open it
directly in one of its tabs by pressing Command-1 to Command-9 (from left to
right):
Navigator bar
1. Project: Add, delete or group files. Open a file in the editor.
2. Source Control: View Git repository working copies, branches, commits, tags,
remotes and stashed changes.
30
SwiftUI Apprentice Chapter 1: Checking Your Tools
7. Debug: Information about CPU, memory, disk and network usage while your app
is running.
9. Report: View or export reports and logs generated when you build and run the
project.
The Filter field at the bottom is different for each tab. For example, the Project
Filter lets you show only the files you recently worked on. This is handy for projects
with a lot of files in a deeply nested hierarchy.
Editor
31
SwiftUI Apprentice Chapter 1: Checking Your Tools
The editor has browser features like tab and go back/forward. Keyboard shortcuts for
tabs are the same as for web browsers: Command-T to open a new tab, Shift-
Command-[ or -] to move to the previous or next tab, Command-W to close the tab
and Option-click a tab’s close button to close all the other tabs. The back/forward
button shows a list of previous/next files, but the keyboard shortcuts are Control-
Command-right or -left arrow.
Inspectors
The Inspectors pane has three, four or five tabs, depending on what’s selected in the
Project navigator. When this pane is hidden, you can open it directly in one of its
tabs by pressing Option-Command-1 to Option-Command-5:
Inspector bar
1. File: Name, Full Path, Target Membership.
All five tabs appear when you select a file in the Project navigator. If you select a
folder, you get only the first three tabs. If you select Assets.xcassets, you don’t get
the accessibility tab.
This quick tour just brushes the surface of what you can do in Xcode. Next, you’ll use
a few of its tools while you explore your new project.
Navigation Settings
In this book, you’ll use keyboard shortcuts to examine and structure your code.
Unlike the fixed keyboard shortcuts for opening navigator tabs or inspectors, you can
specify settings for which shortcut does what. To avoid confusion while working
through this book, you’ll adjust your settings to match the instructions you’ll see.
32
SwiftUI Apprentice Chapter 1: Checking Your Tools
ContentView.swift
The heart of your new project is in ContentView.swift, where your new project
opened. This is where you’ll lay out the initial view of your app.
The first several lines are comments that identify the file and you, the creator.
import
The first line of code is an import statement:
import SwiftUI
This works just like most programming languages: It allows your code to access
everything in the built-in SwiftUI module. See what happens if it’s missing.
33
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: All code on our site uses 2-space indentation to save space. Xcode
defaults to 4-space indentation. You can change this in Xcode Settings ▸ Text
Editing ▸ Indentation.
You commented out the import statement, so compiler errors appear, complaining
about View and PreviewProvider.
Below the import statement are two struct definitions. A structure is a named data
type that encapsulates properties and methods.
struct ContentView
The name of the first structure matches the name of the file. Nothing bad happens if
they’re different, but most developers follow and expect this convention.
34
SwiftUI Apprentice Chapter 1: Checking Your Tools
}
.padding()
}
}
Looking at ContentView: View, you might think ContentView inherits from View,
but Swift structures don’t have inheritance. View is a protocol, and ContentView
conforms to this protocol.
The required component of View is the computed property body, which returns a
View. In this case, it returns a VStack (vertical stack) that displays a globe image and
the usual “Hello, world!” text.
Swift Tip: A computed property returns the computed value. If there’s only a
single code statement, you don’t need to explicitly use the return keyword.
The VStack view has a padding modifier — an instance method of View — that adds
space around the stack. You can see it in this screenshot:
35
SwiftUI Apprentice Chapter 1: Checking Your Tools
This also shows the Accessibility inspector for the Image and Text subviews in
VStack — label, value, identifier and traits.
Image(systemName: "globe") and its modifiers display a globe symbol. You’ll learn
about the Image view in Chapter 3, “Prototyping the Main View”.
➤ Click the Selectable button below the preview canvas, then click the Text view in
the canvas and select the Quick Help inspector.
36
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Now, select the Attributes inspector. Click in the Add Modifier field and wait a
short while until the modifiers menu appears:
37
SwiftUI Apprentice Chapter 1: Checking Your Tools
This inspector is useful when you want to add several modifiers to a View. If you just
need to add one modifier, Control-Option-click the view in the code editor to
open the Attributes inspector pop-up window.
38
SwiftUI Apprentice Chapter 1: Checking Your Tools
struct ContentView_Previews
Below ContentView is a ContentView_Previews structure.
➤ Press Command-Z to undo or, if the five lines are still selected, press Command-/
to uncomment them.
You’ll sometimes want to give more space to the code editor, so your code doesn’t
have to wrap.
39
SwiftUI Apprentice Chapter 1: Checking Your Tools
For most apps, ContentView.swift is just the starting point. Often, ContentView
only defines the app’s organization, orchestrating several subviews. And usually,
you’ll define these subviews in separate files.
Xcode Tip: A new file appears in the Project navigator below the currently
selected file. If that’s not where you want it, drag it to where you want it to
appear in the Project navigator.
40
SwiftUI Apprentice Chapter 1: Checking Your Tools
The new file window displays a lot of options! The one you want is iOS ▸ User
Interface ▸ SwiftUI View. In Chapter 3, “Prototyping the Main View”, you’ll get to
create a Swift File.
41
SwiftUI Apprentice Chapter 1: Checking Your Tools
This window also lets you specify where in the project to create your new file. The
default location is usually correct — in this project, in this group (folder) and in this
target.
The template code for a SwiftUI view is simpler than the ContentView of a new
project.
import SwiftUI
The view’s body contains only Text("Hello, World!") — no Image, so you don’t
need a VStack, and no padding. Another subtle difference: The “Hello, World!”
string is a token. It’s just a placeholder. Clicking anywhere in it highlights the token
so you can type in a new value.
42
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Next, in ContentView.swift, in the code editor, delete the Text view, then type
bye. Xcode suggests some auto-completions:
Xcode Tip: Descriptive names for your types, properties and methods is good
programming practice, and auto-completion is one way Xcode helps you do
the right thing. You can also turn on spell-checking from the Xcode menu:
Edit ▸ Format ▸ Spelling and Grammar ▸ Check Spelling While Typing.
➤ Select ByeView() from the list, so the line looks like this:
ByeView()
43
SwiftUI Apprentice Chapter 1: Checking Your Tools
• MyFirstApp.swift: This file contains the code for your app’s entry point. This is
what actually launches your app.
@main
struct MyFirstApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
The @main attribute marks MyFirstApp as the app’s entry point. You might be
accustomed to writing a main() method to actually launch an app. The App protocol
takes care of this. The App protocol requires only a computed property named body
that returns a Scene. And a Scene is a container for the root view of a view hierarchy.
For an iOS app, the default setup is a WindowGroup scene containing ContentView()
as its root view. A common customization is to set different root views, depending on
whether the user has logged in.
In an iOS app, the view hierarchy fills the entire display. In a macOS or iPadOS app,
WindowGroup can manage multiple windows.
• Assets.xcassets: Store your app’s images and colors here. AppIcon is a special
image set for all the different sizes and resolutions of your app’s icon. Using Xcode
14, you can supply a single 1024x1024-point app icon image that Xcode resizes to
get all the icon sizes for iOS, iPadOS and watchOS. Simply select Single Size in the
Attributes inspector:
44
SwiftUI Apprentice Chapter 1: Checking Your Tools
• Preview Content: If your views need additional code and sample data or assets
while you’re developing your app, store them here. They won’t be included in the
final distribution build of your app.
In this list, the last item is a group. Groups in the Project navigator appear to be
folders, but they don’t necessarily match up with folders in Finder.
Note: Don’t rename or delete any of these files or groups. Xcode stores their
path names in the project’s build settings and will flag errors if it can’t find
them.
You’ll learn how to use these files in the rest of this book.
Xcode Settings
Xcode has a huge number of settings you can adjust to be more productive.
Themes
You’ll be spending a lot of time working in the code editor, so you want it to look
good and also help you distinguish the different components of your code. Xcode
provides several pre-configured font and color themes for you to choose from or
modify.
45
SwiftUI Apprentice Chapter 1: Checking Your Tools
46
SwiftUI Apprentice Chapter 1: Checking Your Tools
Matching Delimiters
SwiftUI code uses a lot of nested closures. It’s really easy to mismatch your braces
and parentheses. Xcode helps you find any mismatches and tries to prevent these
errors from happening.
Here’s a big hint that something’s wrong or you’re typing in the wrong place: You’re
expecting Xcode to suggest completions while you type, but nothing (useful)
appears. When this happens, it’s usually because you’re outside the closure you need
to be in.
47
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Now select the Text Editing ▸ Display tab. Check Code folding ribbon and, if
you like to see them, Line numbers:
➤ Hover your cursor over one — anywhere between VStack { and padding().
48
SwiftUI Apprentice Chapter 1: Checking Your Tools
• Option-hover over {, (, [ or a closing delimiter: Xcode highlights the start and end
delimiters.
➤ Now click the bar (ribbon) to collapse (fold) those lines of code:
Adding Accounts
You can access some Xcode features by adding login information for your Apple ID
and source control accounts.
Settings ▸ Accounts
➤ Click the + button in the lower left corner and add your Apple ID. If you have a
separate paid Apple Developer account, add that too.
49
SwiftUI Apprentice Chapter 1: Checking Your Tools
To add capabilities like push notifications or Apple Pay to your app, you need to set
Team to a Developer Program account. You’d do this in the Signing & Capabilities
tab of the target.
• If you have an account at Bitbucket, GitHub or GitLab, add it here if you want to
push your project’s local git repository to a remote repository.
➤ To set up a remote repository, open the Xcode Source Control menu and select
New Git Repositories…:
50
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Click Create:
51
SwiftUI Apprentice Chapter 1: Checking Your Tools
Create-remote options
52
SwiftUI Apprentice Chapter 1: Checking Your Tools
53
SwiftUI Apprentice Chapter 1: Checking Your Tools
So far, you’ve only used the buttons at either end of the toolbar, to show or hide the
navigator or inspector panes.
• Stop button appears when project is running: Stop (Command-.) the running
project.
• Source control button: Shows branches and lets you create a pull request.
• Scheme menu: This button’s label is the name of the app. Select, edit or manage
schemes. Each product has a scheme. MyFirst has only one product, so it has only
one scheme.
• Run destination menu: This menu defaults to iPhone 14 Pro. Select a connected
device or a simulated device to run the project.
• Activity view: A wide gray field that shows the project name, status messages and
warning or error indicators.
• Library button: This button’s label is a + sign. It opens the library of views,
modifiers, code snippets, media, colors stored in Assets and system symbols.
Option-click this button to keep the library open.
Now that you know where the controls are, it’s time to use some of them.
You don’t need a complete collection of iOS devices. Xcode has several Developer
Tools, and one of them is Simulator. The run destination menu lets you choose
from a list of simulated devices.
54
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ If it’s not already set, click the run destination button and select iPhone 14 Pro.
55
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: When you install Xcode 14, there might be only a few iPhone simulators
in the destination run menu — mine lists only the iPhone 14 sizes and the
iPhone SE 3rd generation. But Apple’s support page lists all the iPhone models
compatible with iOS 16 (https://apple.co/3l94uYm).
56
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Check your destination run menu: Is iPhone 8 listed? If not, select Add
Additional Simulators…, type in iPhone 8, then click Create.
Group is another SwiftUI container. It doesn’t do any layout. It’s just useful when you
need to wrap code that’s more complicated than a single view.
57
SwiftUI Apprentice Chapter 1: Checking Your Tools
If you haven’t used this simulator device before, Xcode starts it up, then displays it
on a second page:
The preview usually looks the same as your app running on a simulated or real
device, but not always. If you feel the preview doesn’t match what your code is laying
out, try running it on a simulator.
58
SwiftUI Apprentice Chapter 1: Checking Your Tools
The first time you run a project on a simulated device, it starts from an “off” state, so
you’ll see a loading indicator. Until you quit the Simulator app, this particular
simulated device is now “awake”, so you won’t get the startup delay even if you run a
different project on it.
After the simulated device starts up, the app’s launch screen appears. For MyFirst,
this is just a blank screen. You’ll learn how to set up your own launch screen in
Chapter 16, “Adding Assets to Your App”.
59
SwiftUI Apprentice Chapter 1: Checking Your Tools
Not Stopping
Here’s a trick that will make your Xcode life a little easier.
➤ Don’t click the stop button. Yes, it’s enabled. But trust me, you’ll like this. :]
The app loads with your new change. But that’s not what I want to show you.
60
SwiftUI Apprentice Chapter 1: Checking Your Tools
Key Points
• The Xcode window has Navigator, Editor and Inspectors panes, a Toolbar and a
Debug Area, plus a huge number of Settings.
• The template project defines an App that launches with ContentView, displaying a
globe symbol and “Hello, world!”.
• When you create a new SwiftUI view file, give it the same name as the View you’ll
create in it.
• You can choose one of Xcode’s font and color themes, modify one or create your
own.
• You can run your app on a simulated device or create previews of specific devices.
61
2 Chapter 2: Planning a
Paged App
By Audrey Tam
In Section 1 of this book, you’ll build an app to help you do high-intensity interval
training. Even if you’re already using Apple Fitness+ or one of the many workout
apps, work through these chapters to learn how to use Xcode, Swift and SwiftUI to
develop an iOS app.
In this chapter, you’ll plan your app, then set up the paging interface. You’ll start
using the SwiftUI Attributes inspector to add modifiers. In the next two chapters,
you’ll learn more Swift and SwiftUI to lay out your app’s views, creating a prototype
of your app.
62
SwiftUI Apprentice Chapter 2: Planning a Paged App
HIITFit screens
There’s a lot going on in these screens, especially the one with the exercise video.
You might feel overwhelmed, wondering where to start. Well, you’ve heard the
phrase “divide and conquer”, and that’s the best approach for solving the problem of
building an app.
First, you need an inventory of what you’re going to divide. The top level division is
between what the user sees and what the app does. Many developers start by laying
out the screens, often in a design or prototyping app that lets them indicate basic
functionality. For example, when the user taps this button, the app shows this screen.
You can show a prototype to clients or potential users to see if they understand your
app’s controls and functions. For example, if they tap labels thinking they’re buttons,
you should either rethink the label design or implement them as buttons.
• A title and page numbers are at the top of the Welcome screen and a History
button is at the bottom. These are also on the screen with the exercise video. The
page numbers indicate there are four numbered pages after this page. The waving
hand symbol is highlighted.
• The screen with the exercise video also has a timer, a Start/Done button and
rating symbols. One of the page numbers is highlighted.
63
SwiftUI Apprentice Chapter 2: Planning a Paged App
• The History screen shows the user’s exercise history as a list and as a bar chart. It
has a title but no page numbers and no History button.
• The High Five! screen has an image, some large text and some small gray text.
Like the History screen, it has no page numbers and no History button.
In this chapter and the next, you’ll lay out the basic elements of these screens. In
Chapter 9, “Refining Your App”, you’ll fine-tune the appearance to look like the
screenshots above.
• The History and High Five! screens are modal sheets that slide up over the
Welcome or Exercise screen. Each has a button the user taps to dismiss it, either a
circled “X” or a Continue button.
• On the Welcome and Exercise screens, the matching page number is white text or
outline on a black background. Tapping the History button displays the History
screen.
• The Welcome page Get Started button displays the next page.
• On an Exercise page, the user can tap the play button to play the video of the
exercise.
• On an Exercise page, tapping the Start Exercise button starts a countdown timer,
and the button label changes to Done. Ideally, the Done button is disabled until
the timer reaches 0. Tapping Done adds this exercise to the user’s history for the
current day.
• On an Exercise page, tapping one of the five rating symbols changes the color of
that symbol and all those preceding it.
• Tapping Done on the last exercise shows the High Five! screen.
• Nice to have: Tapping a page number goes to that page. Tapping Done on an
Exercise page goes to the next Exercise page. Dismissing the High Five! screen
returns to the Welcome page.
There’s also the overarching page-based structure of HIITFit. This is quite easy to
implement in SwiftUI, so you’ll do it first, before you create any screens.
64
SwiftUI Apprentice Chapter 2: Planning a Paged App
Creating Pages
Skills you’ll learn in this section: visual editing of SwiftUI views; using the
pop-up Attributes inspector; TabView styles
The main purpose of this section is to set up the page-based structure of HIITFit, but
you’ll also learn a lot about using Xcode, Swift and SwiftUI. The short list of Skills at
the start of each section helps you keep track of what’s where.
➤ Open the starter project for this chapter. Use the Xcode menu Source Control ▸
New Git Repositories… to add a repository.
And here’s your first SwiftUI vocabulary term: Everything you can see on the device
screen is a view, with larger views containing subviews.
Your next SwiftUI term is modifier: SwiftUI has an enormous number of methods you
can use to modify the appearance or behavior of a view.
65
SwiftUI Apprentice Chapter 2: Planning a Paged App
Note: A single click selects the Text view; double-click selects the content of
the view.
➤ Now type Welcome: The text changes in both the canvas and the code.
Note: Don’t press return after typing Welcome. If necessary, refresh the
preview.
A Text view simply displays a string of characters. It’s useful for listing the views you
plan to create, as a kind of outline. You’ll use multiple Text views now, to see how to
implement paging behavior.
➤ Still in the canvas, click anywhere outside the Text view to deselect “Welcome”,
then single-click to select the whole Text view. Press Command-D:
VStack {
Text("Welcome")
Text("Welcome")
}
Your two Text views are now embedded in a VStack! When you have more than one
view, you must specify how to arrange them on the canvas. Xcode knows this, so it
provided the default arrangement, which displays the two Text views in a vertical
stack.
66
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Change “V” to “H” to see the two views displayed in a horizontal stack:
As you can see, you can edit the view either in the code editor or in the canvas in
Selectable mode.
➤ In the code editor, select both Text("Welcome") lines, then press { and type
VStack in front of the {:
67
SwiftUI Apprentice Chapter 2: Planning a Paged App
And now you’re back to one preview page with two lines of text.
Xcode Tip: Delimiter auto-completion works for curly and square braces,
parentheses and quotation marks: Select the text you want to enclose, then
type the start delimiter character.
➤ Still in the code editor, change the second Welcome to Exercise 1. Then
duplicate Text("Exercise 1") and change the third string to Exercise 2.
Using TabView
Here’s how easy it is to create a TabView:
68
SwiftUI Apprentice Chapter 2: Planning a Paged App
Labeling Tabs
Here’s how you label the tabs. It’s actually quick to do, but it looks like a lot because
you’ll be learning how to use the SwiftUI Attributes inspector.
Xcode Tip: The show-inspectors button (upper right toolbar) opens the right-
hand panel. The Attributes inspector is the right-most tab in this panel. If
you’re working on a small screen and just want to edit one attribute, Control-
Option-click a view in the code editor to use the pop-up inspector. It uses less
space.
➤ Click in the Add Modifier field, then type tab and select Tab Item from the
menu:
Text("Welcome")
.tabItem { Item Label }
69
SwiftUI Apprentice Chapter 2: Planning a Paged App
.tabItem { Text("Welcome") }
TabView {
Text("Welcome")
.tabItem { Text("Welcome") }
Text("Exercise 1")
.tabItem { Text("Exercise 1") }
Text("Exercise 2")
.tabItem { Text("Exercise 2") }
}
70
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Click the Live Preview button, then tap an Exercise tab label to switch to that
tab:
.tabViewStyle(PageTabViewStyle())
The page style uses small index dots, but they’re white on white, so you can’t see
them.
.indexViewStyle(
PageIndexViewStyle(backgroundDisplayMode: .always))
71
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ In Live Preview, just swipe left or right and each page snaps into place.
TabView {
Text("Welcome")
Text("Exercise 1")
Text("Exercise 2")
}
OK, you’ve set up the paging behavior, but you want the pages to be actual Welcome
and Exercise views, not just text. To keep your code organized and easy to read,
you’ll create each view in its own file and group all the view files in a folder.
Grouping Files
Skills you’ll learn in this section: creating and grouping project files
72
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Select the three view files, then right-click and select New Group from
Selection:
73
SwiftUI Apprentice Chapter 2: Planning a Paged App
Group folders just help you organize all the files in your project. In Chapter 4,
“Prototyping Supplementary Views”, you’ll create a folder for your app’s data
models.
Passing Parameters
Skills you’ll learn in this section: default initializers; arrays; let, var, Int;
method parameters; Fix button in error messages; placeholders in auto-
completions
➤ Now, in ContentView, replace the first two Text placeholders with your new
views:
TabView {
WelcomeView() // was Text("Welcome")
ExerciseView() // was Text("Exercise 1")
Text("Exercise 2")
}
Now what? Your app will use ExerciseView to display the name and video for
several different exercises, so you need a way to index this data and pass each index
to ExerciseView.
74
SwiftUI Apprentice Chapter 2: Planning a Paged App
Actually, first you need some sample exercise data. In the Videos folder, you’ll find
four videos:
Note: If you prefer to use your own videos, drag them from Finder into the
Project navigator. Be sure to check the Add to targets check box.
75
SwiftUI Apprentice Chapter 2: Planning a Paged App
The video names match the names of the video files. The exercise names are visible
to your users, so you use title capitalization and spaces.
Swift Tip: Swift distinguishes between creating constants with let and
creating variables with var.
Xcode now complains about ExerciseView() in previews, because it’s missing the
index parameter.
➤ Now there’s a placeholder for the index value — a grayed-out Int. Click it to turn
it blue, then type 0. So now you have this line of code:
ExerciseView(index: 0)
Swift Tip: Like other languages descended from the C programming language,
Swift arrays start counting from 0, not 1.
Now use your index property to display the correct name for each exercise.
76
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Change the "Hello, World!" placeholder to the exercise name for this index
value.
Text(exerciseNames[index])
The canvas has been complaining it Failed to build the scheme “HIITFit” and,
back in ContentView.swift, Xcode is also complaining about the missing argument
for parameter index in the call to ExerciseView().
Now there’s a placeholder for the index value: What should you type there?
Looping
Skills you’ll learn in this section: ForEach, Range; developer
documentation; initializers with parameters; running apps on an iOS device
ExerciseView(index: 0)
Then copy-paste and edit to specify the other three exercises, but there’s a better
way. You’re probably itching to use a loop. Here’s how you scratch that itch. ;]
➤ Replace the second and third lines in the TabView closure with this code:
ForEach loops over the range 0 to 4 but, because of that < symbol, not including 4.
Each integer value 0, 1, 2 and 3 creates an ExerciseView with that index value.
77
SwiftUI Apprentice Chapter 2: Planning a Paged App
The local variable name index is up to you. You could write this code instead:
Developer Documentation
➤ This is a good opportunity to check out Xcode’s built in documentation. Hold
down the Option key, then click ForEach:
78
SwiftUI Apprentice Chapter 2: Planning a Paged App
You’re viewing Xcode’s pop-up Quick Help for the ForEach keyword. You can also
view this information in the Quick Help inspector.
➤ To see more detailed information, scroll down to the bottom of the Quick Help
text, click Open in Developer Documentation, then scroll down to the Topics
section:
➤ This ForEach initializer requires Range<Int>. Click this line to open the
init(_:content:) page, then click Range in its Declaration to open the Range
page:
➤ You won’t need the TabView index dots. Open ContentView.swift and change:
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(
PageIndexViewStyle(backgroundDisplayMode: .always))
to:
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
79
SwiftUI Apprentice Chapter 2: Planning a Paged App
Xcode Tip: This is a good place to commit the changes you’ve made to your
project into your local Git repository. Select Source Control ▸ Commit… or
press Option-Command-C. If asked, check all the changed files. Enter a
commit message like “Set up paging tab view”, then click Commit.
➤ You’re still in ContentView, so Live Preview your app. Swipe from one page to
the next to see the different exercise names.
HIITFit pages
Note: This book’s apps expect your iOS device is running iOS 16.
Live Preview is a convenient way to see what your app looks like and give you some
idea how it behaves. But some features don’t work in Live Preview, so then you need
to build and run your app on a simulator.
If your app doesn’t look or behave quite right on the simulated device, running it on
a real device is the final word. It might look just as you expect, or it might agree with
the preview and simulator that you’ve got more work to do.
Also, there are features like motion and camera that you can’t test in a simulator. For
these, you must install your app on a real device. Plus, it’s fun to have something on
your iPhone that you built yourself!
80
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Open the Settings app and tap Privacy & Security. Scroll down and tap
Developer Mode, then tap the switch to turn it on.
➤ Tap the alert’s Restart button. After your device restarts and you unlock it, you’ll
see an alert asking you to confirm that you want to turn on Developer Mode:
81
SwiftUI Apprentice Chapter 2: Planning a Paged App
Your device is now ready to install and run apps from Xcode. After enabling
Developer Mode, Xcode doesn’t ask again unless you disable Developer Mode by
turning off the switch in Privacy & Security and restarting your device.
➤ Connect your device to your Mac with a cable. Use an Apple cable, as other-brand
cables might not work for this purpose. Select your device from the run destination
menu: It appears near the top, above the simulators:
➤ In the project page, select the target. In the Signing & Capabilities tab, change
the organization name in the Bundle Identifier to something that’s uniquely yours,
like org.audrey for me:
82
SwiftUI Apprentice Chapter 2: Planning a Paged App
Note: The apps in this book have starter projects with com.kodeco as the
organization. If you want to run these apps on an iOS device, you need to
personalize the bundle identifier. This is because one of the authors has
already signed the app with the original bundle identifier, and you’re not a
member of our teams.
83
SwiftUI Apprentice Chapter 2: Planning a Paged App
Trusting Yourself
Note: If your account is a paid Apple Developer account, you won’t need to do
this step. Running your app on your device will just work. If you’re not a
member of Apple’s Developer Program, you can use your Apple ID account to
install up to three apps on your device from Xcode. The app works for seven
days after you install it. Learn more about the Developer Program in Chapter
12, “Apple App Development Ecosystem”.
If this is the first time you’re running an app on this device, Apple makes you
perform one more step to make sure nothing nasty installs itself on your device.
The app icon appears on the home screen of your device, but error messages appear
in Xcode and on your device:
84
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Open Settings to see what’s there. You’ll never guess where to look, so here’s
what you do: Tap General, then scroll down and tap VPN & Device Management.
On that page, tap Apple Development…:
Trust developer.
85
SwiftUI Apprentice Chapter 2: Planning a Paged App
You won’t need to do this again unless you delete all your apps from this device.
Note: If your device uses dark mode, the background and text will have a
different color. By default, SwiftUI respects the device configuration and uses
colors accordingly.
The app doesn’t actually look or behave any different to Live Preview, but you’re now
all set up to run your own projects on this device. When you really want to get
something running right away, you won’t have to stop and deal with any of this
Trust business.
➤ After you disconnect your device from your Mac, select a simulator device in the
run destination menu. Otherwise, if it’s stuck on Any iOS Device, you won’t see
anything in the preview canvas.
86
SwiftUI Apprentice Chapter 2: Planning a Paged App
Key Points
• Plan your app by listing what the user will see and what the app will do.
• Build your app with views and subviews, customized with modifiers.
• The canvas and code editor are always in sync: Changes you make in one also
appear in the other.
• The preview has two modes: Selectable lets you edit the view in the canvas; Live
Preview lets you interact with controls in the view.
• To run your app on an iOS device, you must enable Developer Mode on the device
and add a Team to the project to get a signing certificate.
• The first time you run your project on an iOS device, Apple requires you to
complete a “Trust this developer” step.
87
3 Chapter 3: Prototyping the
Main View
By Audrey Tam
Now for the fun part! In this chapter, you’ll start creating a prototype of your app,
which has four full-screen views:
• Welcome
• Exercise
• History
• Success
88
SwiftUI Apprentice Chapter 3: Prototyping the Main View
• A title and page numbers are at the top of the view and a History button is at the
bottom.
• The exercise view contains a video player, a timer, a Start/Done button and rating
symbols.
• Video player
• Timer
• Start/Done button
• Rating
• History button
You could sketch your screens in an app like Sketch or Figma before translating the
designs into SwiftUI views. But SwiftUI makes it easy to lay out views directly in your
project, so that’s what you’ll do.
The beauty of SwiftUI is it’s declarative: You simply declare the views you want to
display, in the order you want them to appear. If you’ve created web pages, it’s a
similar experience.
➤ Open ExerciseView.swift.
89
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ You’ll start by laying out the iPad version of HIITFit, so select an iPad simulator:
90
SwiftUI Apprentice Chapter 3: Prototyping the Main View
VStack {
Text(exerciseNames[index])
Text("Video player")
Text("Timer")
Text("Start/Done button")
Text("Rating")
Text("History button")
}
Xcode Tip: You could create the six Text views, then do the curly brace auto-
completion trick. Another way is to embed the single Text view in a VStack
— Command-click Text and select Embed in VStack from the menu — and
then duplicate and edit your views. Embed in … only works on a single line of
code and the preview canvas must be open.
The first Text view is the starting point for the Header view. You’ll add code to it,
here in ExerciseView, then you’ll extract this code as a subview and move it to its
own file.
91
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ To prepare for later extraction, use the Command-click menu to embed the first
Text view in a VStack. Now, this Text view is in a VStack, nested in the top-level
VStack:
VStack {
VStack {
Text(exerciseNames[index])
}
➤ Click in the Add Modifier field, type font, then select Font from the menu:
92
SwiftUI Apprentice Chapter 3: Prototyping the Main View
The font size of “Squat” changes in both the canvas and in code:
Text(exerciseNames[index])
.font(.title)
Note: Putting the modifier on its own line is a SwiftUI convention. A view
often has several modifiers, each on its own line. This makes it easy to move a
modifier up or down, because sometimes the order makes a difference.
➤ Xcode suggests the font size title, but this is only a placeholder. To accept this
value, click .title, then press Return.
Note: Xcode and SwiftUI auto-suggestions and default options are often what
you want.
➤ To see other options, Control-Option-click font or title. This opens the font
modifier’s pop-up Attributes inspector. In the Font section, click the selected Font
option Title to see the Font menu:
93
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Select Large Title from the menu: “Squat” is even bigger now!
➤ Once you’re familiar with SwiftUI modifiers, you might prefer to just type.
Delete .font(.largeTitle) and type .font. Press the right-arrow key to see the
second font(_ font:) method:
Swift Tip: The method signature func font(_ font: Font?) -> Text
indicates this method takes one parameter of type Font? and returns a Text
view. The “_” means there’s no external parameter name — you call it with
font(.title), not with font(font: .title).
94
SwiftUI Apprentice Chapter 3: Prototyping the Main View
You could just display Text("1"), Text("2") and so on, but Apple provides a wealth
of configurable icons as SF Symbols.
➤ The SF Symbols app is the best way to browse and explore the collection.
Download it from SF Symbols 4 (https://apple.co/3hWxn3G) and install it. Some
symbols must be used only for specific Apple products like FaceTime or AirPods. You
can check symbols for restrictions at sfsymbols.com.
➤ After installing the SF Symbols app, open it, select the Indices category, then
scroll more than halfway down to the numbers:
You can copy SF Symbol names from the app with Shift-Command-C. However,
there’s an easier way, if you know part of the symbol name you want.
95
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ In the code editor, add this line below the title Text view in the nested VStack:
Image(systemName: "")
Image is another built-in SwiftUI view, and it has an initializer that takes an SF
Symbol name as a String.
➤ Position the cursor between the quotation marks, then open the Library: Click the
+ button in the toolbar or press Shift-Command-L. Select the Symbols tab and
search for “1 circle”:
Note: By default, the Library disappears as soon as you click somewhere else.
If you want it to stay open, hold down the Option key while you open it.
➤ Now, double-click 1.circle. The symbol name appears at the cursor location:
Image(systemName: "1.circle")
96
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Before adding more numbers, embed this Image in an HStack, so the numbers will
appear side by side. Then duplicate and edit more Image views to create the other
three numbers:
HStack {
Image(systemName: "1.circle")
Image(systemName: "2.circle")
Image(systemName: "3.circle")
Image(systemName: "4.circle")
}
➤ You could add .font(.title2) to each Image, but it’s quicker and neater to add it
to the HStack container:
HStack {
Image(systemName: "1.circle")
Image(systemName: "2.circle")
Image(systemName: "3.circle")
Image(systemName: "4.circle")
}
.font(.title2)
97
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ You can modify an Image to override the HStack modifier. For example, modify
the first number to make it extra large:
Image(systemName: "1.circle")
.font(.largeTitle)
Your ExerciseView now has a header, which you’ll reuse in WelcomeView. So you’re
about to extract the header code to create a HeaderView.
Extracting a Subview
➤ Command-click the VStack containing the title Text and the page numbers
HStack, then select Extract Subview from the menu:
98
SwiftUI Apprentice Chapter 3: Prototyping the Main View
99
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Adding a Parameter
The error flag in HeaderView shows where you need a parameter. The index
property is local to ExerciseView, so you can’t use it in HeaderView. You could pass
index to HeaderView and ensure it can access the exerciseNames array. But it’s
always better to pass just enough information. This makes it easier to set up the
preview for HeaderView. Right now, HeaderView needs only the exercise name.
Text(exerciseName)
HeaderView(exerciseName: exerciseNames[index])
Your new file opens in the editor with two error flags:
➤ To fix the first, in ExerciseView.swift, select all 17 lines of your new HeaderView
structure and press Command-X to cut it — copy it to the clipboard and delete it from
ExerciseView.swift.
➤ To fix the second error, in previews, let Xcode add the missing parameter, then
enter any exercise name for the argument:
HeaderView(exerciseName: "Squat")
100
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Because you pass only the exercise name to HeaderView, the preview doesn’t need
access to the exerciseNames array.
Selecting Preview Layout from the Attributes inspector for Header view
➤ Select Preview Layout to add this modifier:
.previewLayout(.sizeThatFits)
➤ The placeholder value sizeThatFits is what you want, but you must accept it:
Click sizeThatFits, then press Return.
➤ Switch to Selectable mode and Zoom to 100% to see just the header:
101
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Variants
➤ Click the Variants button (next to Selectable) and select Color Scheme
Variants:
Dynamic Type Variants show how this view appears for different text size settings,
enabling you to adapt your layout so important elements remain readable.
102
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Orientation Variants
That’s how easy it is to see how your views appear on a device with these settings.
➤ Click the Live Preview or Selectable button to stop showing the variants.
103
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Currently, your app uses two arrays of strings for the exercise and video file names.
This simple approach helped you pass just enough data to the extracted HeaderView,
keeping its preview manageable. But, if you add more videos, you must manually
ensure the strings match up across the two arrays. It’s safer to encapsulate them as
properties of a named type.
First, you’ll create an Exercise structure with the properties you need. Then, you’ll
create an array of Exercise instances and loop over this array to create the
ExerciseView pages of the TabView.
Note: Up to now, you’ve created and used new SwiftUI views. This Exercise
structure models your app’s data, encapsulating the exerciseName and
videoName properties. It isn’t a view, so you create it in a new Swift file.
104
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Name the file Exercise.swift. Select the HIITFit group so it doesn’t go into the
Views group:
struct Exercise {
let exerciseName: String
let videoName: String
Swift Tip: A stored property is one you declare with a type and/or an initial
value, like let name: String or let name = "Audrey". You declare a
computed property with a type and a closure where you compute its value, like
var body: some View { ... }.
105
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Here, you create an enumeration for the four exercise names. The case names are
camelCase: If you start typing ExerciseEnum.sunSalute, Xcode will suggest the
auto-completion.
Because this enumeration has String type, you can specify a String as the raw value
of each case. Here, you specify the title-case version of the exercise name, like “Sun
Salute” for sunSalute. Then ExerciseEnum.sunSalute.rawValue is "Sun Salute".
extension Exercise {
static let exercises = [
Exercise(
exerciseName: ExerciseEnum.squat.rawValue,
videoName: "squat"),
Exercise(
exerciseName: ExerciseEnum.stepUp.rawValue,
videoName: "step-up"),
Exercise(
exerciseName: ExerciseEnum.burpee.rawValue,
videoName: "burpee"),
Exercise(
exerciseName: ExerciseEnum.sunSalute.rawValue,
videoName: "sun-salute")
]
}
exerciseName and videoName are instance properties: Each Exercise instance has
its own values for these properties. A type property belongs to the type, and you
declare it with the static keyword. The exercises array doesn’t belong to an
Exercise instance. There’s only one exercises no matter how many Exercise
instances you create. You access it with the type name: Exercise.exercises.
106
SwiftUI Apprentice Chapter 3: Prototyping the Main View
As the word suggests, an extension extends a named type. The starter project
includes two extensions: DateExtension.swift and ImageExtension.swift. Date
and Image are built-in SwiftUI types but, by creating an extension, you can add
custom methods and computed or type properties.
Exercise is your own custom type, so why do you have an extension? It’s
housekeeping: You’re keeping this task — initializing an array of Exercise values —
separate from the core definition of your structure — stored properties and any
custom initializers.
Developers also use extensions to encapsulate the requirements for protocols, one
for each protocol. Organizing code like this makes it easy to see where to add
features or look for bugs.
Instead of 0 ..< 4, you use the exercises array’s built-in range. Because the range
is no longer fixed, you must provide an id for each array element. \.self means
each element is its own unique identifier.
➤ Add this near the top of ExerciseView, below let index: Int:
HeaderView(exerciseName: exercise.exerciseName)
107
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Playing a Video
Skills you’ll learn in this section: AVPlayer and VideoPlayer; bundle files;
optional types; make conditional; GeometryReader; adding padding
import AVKit
Importing AVKit lets you use high-level types like AVPlayer to play videos with the
usual playback controls.
Xcode complains it “cannot find ‘url’ in scope”, so you’ll define this value next.
108
SwiftUI Apprentice Chapter 3: Prototyping the Main View
These files are in the project folder, which you can access as Bundle.main. Its
method url(forResource:withExtension:) gets you the URL of a file in the main
app bundle if it exists. Otherwise, it returns nil which means no value. The return
type of this method is an Optional type, URL?.
Swift Tip: Swift’s Optional type helps you avoid many hard-to-find bugs that
are common in other programming languages. It’s usually declared as a type
like Int or String followed by a question mark: Int? or String?. If you
declare var index: Int?, index can contain an Int or no value at all. If you
declare var index: Int — with no ? — index must always contain an Int.
Use if let index {...} to check whether an optional has a value. The
condition is true if index has a value. You can also check index != nil.
Note: You’ll learn more about the app bundle in Chapter 7, “Saving Settings”
and about optionals in Chapter 8, “Saving History Data”.
So you need to wrap an if let around the VideoPlayer. Yet another pair of braces!
It can be hard to keep track of them all. But Xcode is here to help. ;]
if true {
VideoPlayer(player: AVPlayer(url: url))
} else {
EmptyView()
}
Xcode Tip: Take advantage of features like Embed in HStack and Make
Conditional to let Xcode keep your braces matched. To adjust what’s included
in the closure, use Option-Command-[ or Option-Command-] to move the
closing brace up or down.
109
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ In Live Preview, click above or below the video to show the play button. Or just
click the video to start/stop it.
GeometryReader { geometry in
110
SwiftUI Apprentice Chapter 3: Prototyping the Main View
The video player now uses only 45% of the screen height:
Adding Padding
➤ The header looks a little squashed. Control-Option-click HeaderView to add
padding to its bottom:
111
SwiftUI Apprentice Chapter 3: Prototyping the Main View
This gives you a new modifier padding(.bottom) and now there’s space between the
header and the video:
Note: You could have added padding to the VStack in HeaderView.swift, but
HeaderView is a little more reusable without padding. You can choose whether
to add padding and how to customize it whenever you use HeaderView in
another view.
➤ Head back to ContentView.swift and Live Preview your app. Swipe from one
page to the next to see the different exercise videos.
HIITFit pages
112
SwiftUI Apprentice Chapter 3: Prototyping the Main View
These are high-intensity interval exercises, so the timer counts down from 30
seconds.
The default initializer Date() creates a value with the current date and time. The
Date method addingTimeInterval(_ timeInterval:) adds interval seconds to
this value.
➤ The Swift Date type has a lot of methods for manipulating date and time values.
Option-click Date and Open in Developer Documentation to scan what’s
available. You’ll dive a little deeper into Date when you create the History view.
Swift Tip: Swift is a strongly typed language. This means that you must use the
correct type. When using numbers, you can usually pass a value of a wrong
type to the initializer of the correct type. For example, Double(myIntValue)
creates a Double value from an Int and Int(myDoubleValue) truncates a
Double value to create an Int. If you write code in languages that allow
automatic conversion, it’s easy to create a bug that’s very hard to find. Swift
makes sure you, and people reading your code — including “future you”, know
that you’re converting one type to another.
You’re using the Text view’s (_:style:) initializer for displaying dates and times.
The timer and relative styles display the time interval between the current time
and the date value, formatted as “mm:ss” or “mm min ss sec”, respectively. These
two styles update the display every second.
You set the system font size to geometry.size.height * 0.07 to make a really big
timer — around 95 points for a 12.9” iPad and 47 points for the much smaller iPhone
8.
113
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Click Live Preview to watch the timer count down from 30 seconds:
Creating Buttons
Creating buttons is simple, so you’ll do both now.
Button("Start/Done") { }
.font(.title3)
.padding()
114
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Here, you gave the Button the label Start/Done and an empty action. You’ll add the
action in Chapter 6, “Observing Objects”. Then, you enlarged the font of its label and
added padding all around it.
Spacer()
Button("History") { }
.padding(.bottom)
The Spacer pushes the History button to the bottom of the screen. The padding
pushes it back up a little, so it doesn’t look squashed.
You’ll add this button’s action in Chapter 5, “Moving Data Between Views”.
.previewLayout(.sizeThatFits)
115
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Replace the boilerplate Text with this code, leaving the cursor between the double
quotation marks:
Image(systemName: "")
.foregroundColor(.gray)
A rating view is usually five stars or hearts, but the rating for an exercise should
reflect the user’s exertion. Something heart-related…
Image(systemName: "waveform.path.ecg")
.foregroundColor(.gray)
116
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ In the canvas or in the editor, Command-click the Image and select Repeat from
the menu:
In the canvas, you see five separate preview pages! Xcode should have embedded
them in a stack, like when you duplicated a view, but it didn’t.
HStack {
ForEach(0 ..< 5) { item in
Image(systemName: "waveform.path.ecg")
.foregroundColor(.gray)
}
}
That’s better! And the symbols are all in a row. But they’re very small.
➤ Remember, you can use font to specify the size of SF Symbols. So add this
modifier to the Image:
.font(.largeTitle)
Bigger is better!
117
SwiftUI Apprentice Chapter 3: Prototyping the Main View
One last detail: The code Xcode created for you contains an unused closure parameter
item:
➤ You don’t use item in the loop code, so replace item with _:
ForEach(0 ..< 5) { _ in
Swift Tip: It’s good programming practice to replace unused parameter names
with _. The alternative is to create a throwaway name, which takes a non-zero
amount of time and focus and will confuse you and other programmers
reading your code.
RatingView()
.padding()
118
SwiftUI Apprentice Chapter 3: Prototyping the Main View
In Chapter 5, “Moving Data Between Views”, you’ll add code to let the user set a
rating value and represent this value by setting the right number of symbols to red.
And, in Chapter 7, “Saving Settings”, you’ll save the rating values so they persist
across app launches.
Challenges
ExerciseView will be easier to understand if all its components are in separate view
files.
VideoPlayerView(videoName: exercise.videoName)
.frame(height: geometry.size.height * 0.45)
119
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Key Points
• Declare SwiftUI views in the order you want them to appear.
• Create separate views for your user interface elements. This makes your code
easier to read and maintain.
• Put each view modifier on its own line. This makes it easy to move or delete a
modifier.
• Xcode and SwiftUI auto-suggestions and default values are often what you want.
• Let Xcode help you avoid errors: Use the Command-menu to embed or extract
views.
• The SF Symbols app and Xcode’s Symbols Library provide icon images you can
configure like text.
• Preview variants make it easy to check your interface for different user settings.
• An enumeration is a named type, useful for grouping related values so the compiler
can help you avoid mistakes like misspelling a string.
120
4 Chapter 4: Prototyping
Supplementary Views
By Audrey Tam
• Welcome
• History
• Success
In the previous chapter, you laid out the Exercise view and created an Exercise
structure. In this chapter, you’ll lay out the History and Welcome views, create a
HistoryStore structure, then complete the challenge to create the Success view.
And your app’s prototype will be complete.
121
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
You’ll start with a mock-up of the list view. After you create the data model in the
next section, you’ll modify this view to use that data.
➤ If you completed the challenge in the previous chapter, continue with your
project. Or open the project in this chapter’s starter folder.
➤ In the Views group, create a new SwiftUI View file named HistoryView.swift.
For this mock-up, add some sample history data to HistoryView, above body:
VStack {
Text("History")
.font(.title)
.padding()
// Exercise history
}
You’ve created the title for this view with some padding around it.
Creating a Form
SwiftUI has a container view that automatically formats its contents to look
organized.
Form {
Section(
header:
Text(today.formatted(as: "MMM d"))
122
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
.font(.headline)) {
// Section content
}
Section(
header:
Text(yesterday.formatted(as: "MMM d"))
.font(.headline)) {
// Section content
}
}
Inside the Form container view, you create two sections. Each Section has a header
with the date, using headline font size.
This code takes yesterday and today’s date as the section headers, so your view will
have different dates from the one below:
Swift Tip: A Date object is just some number of seconds relative to January 1,
2001 00:00:00 UTC. To display it as a calendar date in a particular time zone,
you must use a DateFormatter. This class has a few built-in styles named
short, medium, long and full, described in links from the developer
documentation page for DateFormatter.Style. You can also specify your own
format as a String.
123
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
DateFormatter has only the default empty initializer. You create an instance, then
configure it by setting the properties you care about. This method uses its format
argument to set the dateFormat property.
In HistoryView, you pass "MMM d" as format. This specifies three characters for the
month — so you get SEP or OCT — and one character for the day — so you get a
number. If the number is a single digit, that’s what you see. If you specify "MM dd",
you get numbers for both month and day, with leading 0 if the number is single digit:
09 02 instead of SEP 2.
Once you’ve configured dateFormatter, its string(from:) method returns the date
string.
You don’t have to worry about time zones if you simply want the user’s current time
zone. That’s the default setting.
Swift Tip: You can add methods to extend any type, including those built into
the software development kit, like Image and Date. Then, you can use them
the same way you use the built-in methods.
124
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
This is a special kind of comment. It appears in Xcode’s Quick Help when you
Option-click the method name:
It’s good practice to document all the methods you write this way. See Apple’s
Formatting Quick Help (https://apple.co/33hohbk) documentation for more details.
To display the completed exercises for each day, you’ll use ForEach to loop over the
elements of exercises1 and exercises2.
125
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
In ContentView, you looped over a number range. Here, you’re using the third
ForEach initializer for Creating a collection from data:
exercises1 is the Data and \.self is the key path to each array element’s identifier.
\.self means each element of the array is its own unique identifier.
As the loop visits each array element, you assign it to the local variable exercise,
which you display in a Text view.
➤ In the second Section, replace // Section content with the almost identical
code:
126
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
In this section, you’ll create a data structure to store the user’s activity, to replace
the hard-coded dates and exercise lists in your mock-up.
Creating HistoryStore
➤ Outside the Views group, create a new Swift file and name it HistoryStore.swift.
Group it with Exercise.swift and name the group folder Model:
struct HistoryStore {
var exerciseDays: [ExerciseDay] = []
}
An ExerciseDay has properties for the date and a list of exercise names completed
by the user on that date.
127
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
When you loop over a collection with ForEach, it must have a way to uniquely
identify each of the collection’s elements. The easiest way is to make the element’s
type conform to Identifiable and include id: UUID as a property. UUID is a basic
Foundation type, and UUID() is the easiest way to create a unique identifier
whenever you create an ExerciseDay instance.
The only property in HistoryStore is the array of ExerciseDay values you’ll loop
over in HistoryView.
In the meantime, you need some sample history data and an initializer to create it.
extension HistoryStore {
mutating func createDevData() {
// Development data
exerciseDays = [
ExerciseDay(
date: Date().addingTimeInterval(-86400),
exercises: [
Exercise.exercises[0].exerciseName,
Exercise.exercises[1].exerciseName,
Exercise.exercises[2].exerciseName
]),
ExerciseDay(
date: Date().addingTimeInterval(-86400 * 2),
exercises: [
Exercise.exercises[1].exerciseName,
Exercise.exercises[0].exerciseName
])
]
}
}
128
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
The exercise lists are slightly different to the mock-up data in HistoryView, and
they’re stored in your new Exercise and ExerciseDay structures. In Chapter 6,
“Observing Objects”, you’ll add a new ExerciseDay item, so I’ve set the
development data to yesterday and the day before yesterday.
You create this sample data in a method named createDevData(). This method
changes, or mutates, exerciseDays, so you must mark it with the mutating
keyword. And you create this method in an extension because it’s not part of the
core definition. But there’s another reason, too — coming up soon!
➤ Now, in the main HistoryStore, create an initializer for HistoryStore that calls
createDevData():
init() {
#if DEBUG
createDevData()
#endif
}
You don’t want to call createDevData() in the release version of your app, so you
use a compiler directive to check whether the current Build Configuration is Debug:
Note: To see this window, click the Scheme menu button (next to the run
destination menu). Select Edit Scheme…, then select the Info tab.
129
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
HistoryStore now encapsulates all the information in the stored properties today,
yesterday and the exercises arrays.
The Form closure currently displays each day in a Section. Now that you have an
exerciseDays array, you should loop over it.
Form {
ForEach(history.exerciseDays) { day in
Section(
header:
Text(day.date.formatted(as: "MMM d"))
.font(.headline)) {
ForEach(day.exercises, id: \.self) { exercise in
130
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Text(exercise)
}
}
}
}
Instead of today and yesterday, you use day.date. And, instead of the named
exercises arrays, you use day.exercises.
The code you just replaced looped over exercises1 and exercises2, which were
arrays of String. The id: \.self argument told ForEach to use the instance itself
as the unique identifier. The exercises array also contains String instances, so you
still need to specify this id value.
Dismissing HistoryView
Skills you’ll learn in this section: layering views with ZStack; stack
alignment values
131
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
ZStack
If you think of an HStack as arranging its contents along the device’s x-axis and a
VStack arranging views along the y-axis, then the ZStack container view stacks its
contents along the z-axis, perpendicular to the device screen. Think of it as a depth
stack, displaying views in layers.
➤ Command-click VStack to embed it in a ZStack, then add this code at the top of
ZStack above the VStack:
Button(action: {}) {
Image(systemName: "xmark.circle")
}
➤ Switch the preview to Selectable mode to see the outline of the button:
The arrangement is a little counter-intuitive unless you think of it as placing the first
view down on a flat surface, then layering the next view on top of that, and so on. So
declaring the button as the first view places it on the bottom of the stack. If you want
the button in the top layer, declare it last in the ZStack.
It doesn’t matter in this case, because you’re about to move the button into the top
right corner of the view, where there’s nothing in the VStack to cover it.
132
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Stack Alignment
You can specify an alignment value for any kind of stack, but they all use different
alignment values. VStack alignment values are horizontal: leading, center or
trailing. HStack alignment values are vertical: top, center, bottom,
firstTextBaseline or lastTextBaseline.
To specify the alignment of a ZStack, you must set both horizontal and vertical
alignment values. You can either specify separate horizontal and vertical values, or a
combined value like topTrailing.
ZStack(alignment: .topTrailing) {
You set the ZStack alignment parameter to position the button in the top right
corner of the view. Other views in the ZStack have their own alignment values, so
the ZStack alignment value doesn’t affect them.
The button is now visible, but it’s small and a little too close to the corner edges.
➤ Add these modifiers to the Button to adjust its size and position:
.font(.title)
.padding(.trailing)
133
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ Open WelcomeView.swift.
WelcomeView is the first page in your app’s page-style TabView, so it should have the
same header as ExerciseView.
HeaderView(exerciseName: "Welcome")
You want the title of this page to be “Welcome”, so you pass this as the value of the
exerciseName parameter. HeaderView also displays the page numbers of the four
exercises:
Refactoring HeaderView
Using HeaderView here raises two issues:
The first issue is easy to resolve. The app has only one non-exercise page, so you just
need to add another page “number” in HeaderView.
134
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ In HeaderView.swift, duplicate the first Image, then change the now-first Image
to display a hand wave:
Image(systemName: "hand.wave")
Now you need to rename the exerciseName property. Its purpose is really to be the
title of the page, so titleText is a better name for it.
You could search for all occurrences of exerciseName in your app, then decide for
each whether to change it to titleText. In a more complex app, this approach
almost guarantees you’ll forget one or change one that shouldn’t change.
135
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Xcode displays the code statements in three files that need to change:
136
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ Click the Rename button in the upper right corner to confirm these changes, then
head back to WelcomeView.swift to see the results:
In HistoryView, you used a ZStack to position the dismiss button in the upper right
corner (topTrailing), without affecting the layout of the other content.
In this view, you’ll use a ZStack to put the header and History button in one layer, to
push them apart. Then you’ll create the main content in another layer, centered by
default.
ZStack {
VStack {
HeaderView(titleText: "Welcome")
}
}
Spacer()
Button("History") { }
.padding(.bottom)
137
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
You have the header and the History button in a VStack, with a Spacer to push
them apart and some padding so the button isn’t too close to the bottom edge:
VStack {
HStack {
VStack(alignment: .leading) {
Text("Get fit")
.font(.largeTitle)
Text("with high intensity interval training")
.font(.headline)
}
}
}
Note: You can add this VStack either above or below the existing VStack. It
doesn’t matter because there’s no overlapping content in the two layers.
138
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
This VStack is in an HStack because you’re going to place an Image to the right of
the text. And the HStack is in an outer VStack because you’ll add a Button below the
text and image.
Using an Image
➤ Look in Assets.xcassets for the step-up image:
139
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ To insert step-up in the correct place, it’s easiest to drag it into the code editor.
Hold onto it while nudging the code with the cursor, until a line opens, just below
the VStack of two Text views. Let go of the image, and it appears in your code:
HStack {
VStack(alignment: .leading) {
Text("Get fit")
.font(.largeTitle)
Text("with high intensity interval training")
.font(.headline)
}
Image("step-up") // your new code appears here
}
➤ You usually have to add several modifiers to an Image, so open the Attributes
inspector in the inspectors panel:
Note: If you don’t see Image with a value of step-up, select the image or
select another inspector then re-select Attributes.
Modifying an Image
➤ First, you must add a modifier that lets you resize the image. In the Add Modifier
field, type res then select Resizable.
140
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ When resizing an image, you usually want to preserve the aspect ratio. So search
for an aspect modifier and select Aspect Ratio:
➤ Now the image looks more normal, but it’s too big. In the Frame section, set the
Width and Height to 240:
141
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
HStack(alignment: .bottom)
142
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 240.0, height: 240.0)
It extends the Image view, so self is the Image you’re modifying with
resizedToFill(width:height:).
And the view looks the same, but there’s a little less code.
➤ In the center view VStack, below the HStack with the image, add this code:
Button(action: { }) {
Text("Get Started")
Image(systemName: "arrow.right.circle")
}
.font(.title2)
.padding()
143
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Both parameter values can be closures, so action can be more than one executable
statement, and label can be more than one view.
The buttons you’ve created so far use the simplest Button syntax: The button’s label
is simply a String, and the button’s action is in a trailing closure. For example:
Button("History") { }
Swift Tip: You can move the last closure argument of a function call outside
the parentheses into a trailing closure.
This simple Button syntax reverses the official signature, and it’s only for the case
where label is a string.
144
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
If you want more than a string in your label, its content must be in a closure. It’s
the last closure argument of this function call, so it can be a trailing closure:
Button(action: {} ) {
<Content>
}
This is the syntax used in the “Get Started” Button above, with the Text and Image
views in an implicit HStack.
Note: You can modify a Label with labelStyle to show only the text or only
the image.
145
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
The image is on the left side of the text. This looks wrong to me: An arrow pointing
right should appear after the text. Unfortunately for this particular Button, there’s
no way to make the image appear to the right of the text, unless you’re using a
language like Arabic that’s written right-to-left. Label is ideal for icon-text lists,
where you want the icons nicely aligned on the leading edge.
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 2))
146
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Challenge
When your users tap Done on the last exercise page, your app will show a modal
sheet to congratulate them on their success. Your challenge is to create this
SuccessView:
147
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
2. Replace its Text view with a VStack containing the hand.raised.fill symbol
and the text in the screenshot.
3. The symbol is in a 75 by 75 frame and colored purple. Hint: Use the custom
Image modifier.
4. For the large “High Five!” title, you can use the fontWeight modifier to
emphasize it more.
5. For the three small lines of text, you could use three Text views. Or refer to our
Swift Style Guide (https://bit.ly/30cHeeL) to see how to create a multi-line string.
Text has a multilineTextAlignment modifier. This text is colored gray.
148
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
Key Points
• The Date type has many built-in properties and methods. You need to configure a
DateFormatter to create meaningful text to show your users.
• Use the Form container view to quickly lay out table data.
• Use compiler directives to create development data only while you’re developing
and not in the release version of your app.
• Preview Content is a convenient place to store code and data you use only while
developing. Its contents won’t be included in the release version of your app.
• ZStack is useful for keeping views in one layer centered while pushing views in
another layer to the edges.
• You can specify vertical alignment values for HStack, horizontal alignment values
for VStack and combination alignment values for ZStack.
• Xcode helps you to refactor the name of a parameter quickly and safely.
• Image often needs the same three modifiers. You can create a custom modifier so
you Don’t Repeat Yourself.
• A Button has a label and an action. You can define a Button a few different ways.
149
5 Chapter 5: Moving Data
Between Views
By Audrey Tam
In the previous chapter, you structured your app’s data to be more efficient and less
error-prone. In this chapter, you’ll implement most of the functionality your users
expect when navigating and using your app. Now, you’ll need to manage your app’s
data so values flow smoothly through the views and subviews of your app.
150
SwiftUI Apprentice Chapter 5: Moving Data Between Views
• Single source of truth: Every piece of data that a view reads has a source of truth,
which is either owned by the view or external to the view. Regardless of where the
source of truth lies, you should always have a single source of truth.
151
SwiftUI Apprentice Chapter 5: Moving Data Between Views
• A @State property is a source of truth. One view owns it and passes either its value
or a reference, known as a binding, to its subviews.
You’ll learn more about these, and other, property wrappers in Chapter 11,
“Managing Data With Property Wrappers”.
Here’s your first feature: Set up TabView to use tag values. When a button changes
the value of selectedTab, TabView displays that tab.
➤ Continue with your project from the previous chapter or open the project in this
chapter’s starter folder.
152
SwiftUI Apprentice Chapter 5: Moving Data Between Views
Note: You almost always mark a State property private, to emphasize that
it’s owned and managed by this view specifically. Only this view’s code in this
file can access it directly. An exception is when the App needs to initialize
ContentView, so it needs to pass values to its State properties. Learn more
about access control in Swift Apprentice, Chapter 18, “Access Control, Code
Organization & Testing” (https://bit.ly/37EUQDk).
Other views will use the value of selectedTab, and some will change this value to
make TabView display another page. But, you won’t declare it as a @State property
in any other view.
The initial value of selectedTab is 9, which you’ll set as the tag value of the
welcome page.
➤ If the preview is paused, refresh it, then click the pin button to pin the preview of
ContentView:
➤ Next, replace the entire body closure of ContentView with the following code:
153
SwiftUI Apprentice Chapter 5: Moving Data Between Views
You’ll soon write code to make ExerciseView change the value of selectedTab, so
it can’t be a plain old var selectedTab. Views are structures, which means you
can’t change a property value unless you mark it with a property wrapper like @State
or @Binding.
ContentView owns the source of truth for selectedTab. You don’t declare @State
private var selectedTab here in ExerciseView because that would create a
duplicate source of truth, which you’d have to keep in sync with the selectedTab
value in ContentView. Instead, you declare @Binding var selectedTab — a
reference to the @State variable owned by ContentView.
You just want the preview to show the second exercise, but you can’t pass 1 as the
selectedTab value. You must pass a Binding, which is tricky in a standalone
situation like this, where you don’t have a @State property to bind to. Fortunately,
SwiftUI provides the Binding type method constant(_:) to create a Binding from a
constant value.
154
SwiftUI Apprentice Chapter 5: Moving Data Between Views
WelcomeView(selectedTab: .constant(9))
Now that you’ve fixed the errors, you can preview ContentView while you’re still in
WelcomeView.swift:
Button(action: { selectedTab = 0 }) {
You’ve used selectedTab to navigate from the welcome page to the first exercise!
155
SwiftUI Apprentice Chapter 5: Moving Data Between Views
Note: You can’t preview this action in the WelcomeView preview because it
doesn’t include ExerciseView. Tapping Get Started doesn’t go anywhere.
156
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ First, simplify your life by separating the Start and Done buttons in
ExerciseView. In ExerciseView.swift, replace Button("Start/Done") { } with
this HStack:
HStack(spacing: 150) {
Button("Start Exercise") { }
Button("Done") { }
}
Keep the font and padding modifiers on the HStack, so both buttons use title3
font size, and the padding surrounds the HStack.
➤ Fix the indentation: Select the font and padding modifiers, then press Control-I.
Now you’re ready to implement your time-saving action for the Done button:
Tapping Done goes to the next ExerciseView, and tapping Done in the last
ExerciseView goes to WelcomeView.
You created a computed property to check whether this is the last exercise.
Button("Done") {
selectedTab = lastExercise ? 9 : selectedTab + 1
}
Swift Tip: The ternary conditional operator tests the condition specified
before ?, then evaluates the first expression after ? if the condition is true.
Otherwise, it evaluates the expression after :.
Later in this chapter, you’ll show SuccessView when the user taps Done on the last
ExerciseView. Then dismissing SuccessView will progress to WelcomeView.
157
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ In body, replace the two Button views in the HStack with your new properties:
HStack(spacing: 150) {
startButton
doneButton
}
➤ Now, in the ContentView Live Preview, tap Get Started to load the first exercise.
Tap Done on each exercise page to progress to the next. Tap Done on the last
exercise to return to the welcome page.
158
SwiftUI Apprentice Chapter 5: Moving Data Between Views
2. The Welcome page doesn’t really need a page “number”, so you delete the
"hand.wave" symbol from the HStack.\
159
SwiftUI Apprentice Chapter 5: Moving Data Between Views
3. To accommodate any number of exercises, you create the HStack by looping over
the exercises array, just like in ContentView.
4. You create each symbol’s name by joining together a String representing the
integer index + 1, the text ".circle" and either ".fill" or the empty String,
depending on whether index matches selectedTab. You use a ternary
conditional expression to choose between ".fill" and "".
➤ Now previews needs this new parameter, so replace it with the following:
HeaderView(
selectedTab: $selectedTab,
titleText: Exercise.exercises[index].exerciseName)
➤ In the ContentView Live Preview, tap Get Started to load the first exercise. The
1 symbol is filled. Tap Done on each exercise page to progress to the next and see
the symbol for each page highlight.
160
SwiftUI Apprentice Chapter 5: Moving Data Between Views
Using onTapGesture
Making Page Numbers Tappable
Many users expect page numbers to respond to tapping by going to that page.
.onTapGesture {
selectedTab = index
}
This modifier reacts to the user tapping the Image by setting the value of
selectedTab.
➤ In the ContentView Live Preview, tap a page number to navigate to that exercise
page:
161
SwiftUI Apprentice Chapter 5: Moving Data Between Views
In Chapter 7, “Saving Settings”, you’ll save the rating value along with the
exerciseName, so ExerciseView needs this rating property. You use the property
wrapper @State because rating must be able to change, and ExerciseView owns
this property.
RatingView(rating: $rating)
You pass a binding to rating to RatingView because that’s where the actual value
change will happen.
RatingView(rating: .constant(3))
162
SwiftUI Apprentice Chapter 5: Moving Data Between Views
}
}
}
.font(.largeTitle)
}
2. Most apps use a 5-level rating system, but you can set a different value for
maximumRating.
4. In the HStack, you still loop over the symbols, but now you set the symbol’s
foregroundColor to offColor if its index is higher than rating.
5. When the user taps a symbol, you set rating to that index.
➤ In the ContentView Live Preview, tap a page number to navigate to that exercise
page. Tap different symbols to see the colors change:
Rating view
163
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ Navigate to other exercise pages and set their ratings, then navigate through the
pages to see the ratings are still the values you set.
HistoryView and SuccessView are modal sheets that slide up over WelcomeView or
ExerciseView. You dismiss the modal sheet by tapping its circled-x or Continue
button, or by dragging it down.
Button("History") {
showHistory.toggle()
}
.sheet(isPresented: $showHistory) {
HistoryView(showHistory: $showHistory)
}
Tapping the History button toggles the value of showHistory from false to true.
This causes the sheet modifier to present HistoryView. You pass a binding
$showHistory to HistoryView so it can change this value back to false when the
user dismisses HistoryView.
➤ You’ll edit HistoryView to do this soon. But first, repeat the steps above in
ExerciseView.swift.
164
SwiftUI Apprentice Chapter 5: Moving Data Between Views
HistoryView(showHistory: .constant(true))
Button(action: { showHistory.toggle() }) {
➤ Pin the WelcomeView preview, then go to HistoryView and fix the button
padding:
Button(action: { showHistory.toggle() }) {
Image(systemName: "xmark.circle")
}
.font(.title)
.padding() // delete .trailing
165
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ That’s better! Now, tap the dismiss button to hide it. You can also drag down on
HistoryView. Unpin the WelcomeView preview.
166
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ Then, in the doneButton closure, replace the Done button action with an if-else
statement:
Button("Done") {
if lastExercise {
showSuccess.toggle()
} else {
selectedTab += 1
}
}
.sheet(isPresented: $showSuccess) {
SuccessView()
}
Every view’s environment has properties like colorScheme, locale and the device’s
accessibility settings. Many of these are inherited from the app, but a view’s dismiss
is specific to the view. It’s the instance for the current Environment of the
DismissAction structure. This structure has a callAsFunction method, but you
don’t call it directly. Instead, you use “syntactic sugar”.
167
SwiftUI Apprentice Chapter 5: Moving Data Between Views
Button("Continue") {
dismiss()
}
You “call” the dismiss structure, and SwiftUI then calls its callAsFunction method.
This method isn’t a toggle. It dismisses the view if it’s currently presented. It does
nothing if the view isn’t currently presented.
To test showing and hiding SuccessView, you’ll preview the last exercise page.
168
SwiftUI Apprentice Chapter 5: Moving Data Between Views
.presentationDetents([.medium, .large])
You’re telling SwiftUI to support two sheet sizes — .medium (about half height)
and .large (full height). You can also specify .fraction or .height values.
169
SwiftUI Apprentice Chapter 5: Moving Data Between Views
SuccessView(selectedTab: .constant(3))
selectedTab = 9
Note: You can add it either above or below the dismiss call, but adding it
above feels more like the right order of things.
SuccessView(selectedTab: $selectedTab)
170
SwiftUI Apprentice Chapter 5: Moving Data Between Views
➤ And finally, back to ContentView.swift to see it work. Run live preview, tap the
page 4 button, tap Done, then tap Continue:
Note: If you don’t see the welcome page, press Command-B to rebuild the
app, then try again.
You’ve used a Boolean flag to show modal sheets. And you’ve used the Boolean flag
and the environment variable .\dismiss to dismiss the sheets.
In this chapter, you’ve used view values to navigate your app’s views and show modal
sheets. In the next chapter, you’ll observe objects: You’ll use a TimeLineView and
rework HistoryStore as an ObservableObject.
171
SwiftUI Apprentice Chapter 5: Moving Data Between Views
Key Points
• Declarative app development means you declare both how you want the views in
your UI to look and also what data they depend on. The SwiftUI framework takes
care of creating views when they should appear and updating them whenever
there’s a change to data they depend on.
• Single source of truth: Every piece of data has a source of truth, internal or
external. Regardless of where the source of truth lies, you should always have a
single source of truth.
• Use Boolean @State properties to show and hide modal sheets or subviews. Use
@Environment(\.dismiss) as another way to dismiss a modal sheet.
172
6 Chapter 6: Observing
Objects
By Audrey Tam
In the previous chapter, you managed the flow of values to implement most of the
functionality your users expect when navigating and using your app. In this chapter,
you’ll manage some of your app’s data objects. You’ll use a TimeLineView and give
some views access to HistoryStore as an EnvironmentObject.
173
SwiftUI Apprentice Chapter 6: Observing Objects
Using TimelineView
Your app currently uses a Text view with style: .timer. This counts down just
fine, but then it counts up and keeps going. You don’t have any control over it. You
can’t stop it. You can’t even check when it reaches zero.
Swift has a TimelineView container that redraws its content at scheduled times. It
comes with some built-in schedule types:
➤ Continue with your project from the previous chapter or open the project in this
chapter’s starter folder.
174
SwiftUI Apprentice Chapter 6: Observing Objects
Text("\(timeRemaining)") // 5
.font(.system(size: size, design: .rounded))
.padding()
.onChange(of: date) { _ in // 6
timeRemaining -= 1
}
}
}
1. timeRemaining is the number of seconds the timer runs for each exercise.
Normally, this is 30 seconds. But one of the features you’ll implement in this
section is disabling the Done button until the timer reaches zero. You set
timeRemaining very small so you won’t have to wait 30 seconds when you’re
testing this feature.
175
SwiftUI Apprentice Chapter 6: Observing Objects
You’ll pass $timerDone to TimerView, which will set it to true when the timer
reaches zero. You’ll use this to enable the Done button.
And, you’ll toggle showTimer just like you did with showHistory and showSuccess.
176
SwiftUI Apprentice Chapter 6: Observing Objects
➤ Replace this Text view and font modifier with the following code:
if showTimer {
TimerView(
timerDone: $timerDone,
size: geometry.size.height * 0.07
)
}
You include TimerView when showTimer is true, passing it a binding to the @State
property timerDone and the font size.
Button("Start Exercise") {
showTimer.toggle()
}
This is just like your other buttons that toggle a Boolean to show another view.
timerDone = false
showTimer.toggle()
If the Done button is enabled, timerDone is now true, so you reset it to false to
disable the Done button.
Also, TimerView is showing. This means showTimer is currently true, so you toggle
it back to false, to hide TimerView.
➤ Next, in the HStack of start and done buttons, add this modifier to doneButton,
above the sheet(isPresented:) modifier:
.disabled(!timerDone)
177
SwiftUI Apprentice Chapter 6: Observing Objects
178
SwiftUI Apprentice Chapter 6: Observing Objects
➤ Tap Start Exercise and wait while the timer counts down from 3:
➤ Tap Done.
179
SwiftUI Apprentice Chapter 6: Observing Objects
➤ Tap Continue.
Tweaking the UI
Tapping Start Exercise shows the timer and pushes the buttons and rating symbols
down the screen. Tapping Done moves them up again. So much movement is
probably not desirable, unless you believe it’s a suitable “feature” for an exercise
app.
To stop the buttons and ratings from doing squats, you’ll rearrange the UI elements.
HStack(spacing: 150) {
startButton
doneButton
180
SwiftUI Apprentice Chapter 6: Observing Objects
.disabled(!timerDone)
.sheet(isPresented: $showSuccess) {
SuccessView(selectedTab: $selectedTab)
.presentationDetents([.medium, .large])
}
}
.font(.title3)
.padding()
if showTimer {
TimerView(
timerDone: $timerDone,
size: geometry.size.height * 0.07
)
}
Spacer()
RatingView(rating: $rating) // Move RatingView below Spacer
.padding()
You move the buttons above the timer and RatingView(rating:) below Spacer().
This leaves a stable space to show and hide the timer.
➤ In Live Preview, tap Start Exercise, wait for the Done button, then tap it. The
timer appears then disappears. None of the other UI elements moves.
181
SwiftUI Apprentice Chapter 6: Observing Objects
Note: If you preview on a small iPhone, the timer view pushes the History
button off the screen. To prevent this, set the spacing of the top level VStack
to 0: VStack(spacing: 0).
There’s just one last feature to add to your app. It’s another job for the Done button.
Using an EnvironmentObject
Skills you’ll learn in this section: using @ObservableObject and
@EnvironmentObject to let subviews access data; class vs structure
This is the last feature: Tapping Done adds this exercise to the user’s history for the
current day. You’ll add the exercise to the exercises array of today’s ExerciseDay
object, or you’ll create a new ExerciseDay object and add the exercise to its array.
Examine your app to see which views need to access HistoryStore and what kind of
access each view needs:
182
SwiftUI Apprentice Chapter 6: Observing Objects
More than one view needs access to HistoryStore, so you need a single source of
truth. There’s more than one way to do this.
The last list item above is the least satisfactory. You’ll learn how to manage
HistoryStore so it doesn’t have to pass through WelcomeView.
➤ Make a copy of this project now and use it to start the challenge at the end of this
chapter.
Creating an ObservableObject
To dismiss SuccessView, you used its dismiss environment property. This is one of
the system’s predefined environment properties. You can define your own
environment object on a view, and it can be accessed by any subview of that view.
You don’t need to pass it as a parameter. Any subview that needs it simply declares it
as a property.
183
SwiftUI Apprentice Chapter 6: Observing Objects
You make HistoryStore a class instead of a structure, and you make it conform to
the ObservableObject protocol.
1. The date of the first element of exerciseDays is the user’s most recent exercise
day. If today is the same as this date, you append the current exerciseName to
the exercises array of this exerciseDay.
2. If today is a new day, you create a new ExerciseDay object and insert it at the
beginning of the exerciseDays array.
func createDevData() {
184
SwiftUI Apprentice Chapter 6: Observing Objects
You had to mark this method as mutating when HistoryStore was a structure. You
must not use mutating for methods defined in a class.
Swift Tip: Structures tend to be constant, so you must mark as mutating any
method that changes a property. If you mark a method in a class as mutating,
Xcode flags an error. See Chapter 15, “Structures, Classes & Protocols” for
further discussion of reference and value types.
.environmentObject(HistoryStore())
You don’t want to create another HistoryStore object here. Instead, HistoryView
can access history directly without needing it passed as a parameter.
.environmentObject(HistoryStore())
You must tell previews about this EnvironmentObject or it will crash with the
message Preview is missing environment object “HistoryStore”.
185
SwiftUI Apprentice Chapter 6: Observing Objects
➤ Now, in doneButton, add this line at the top of the button’s action closure:
history.addDoneExercise(Exercise.exercises[index].exerciseName)
History: before
186
SwiftUI Apprentice Chapter 6: Observing Objects
➤ Dismiss HistoryView, then tap Start Exercise. When Done is enabled, tap it.
Because you’re previewing ExerciseView, it won’t progress to the next exercise.
History: after
There’s your new ExerciseDay with this exercise!
Your app is working pretty well now, with all the expected navigation features. But
you still need to save the user’s ratings and history so they’re still there after
quitting and restarting your app. And then, you’ll finally get to make your app look
pretty.
187
SwiftUI Apprentice Chapter 6: Observing Objects
Challenge
To appreciate how well @EnvironmentObject works for this feature, implement it
using State and Binding.
Key Points
• Implement a countdown timer by creating a TimelineView with an
animation(minimumInterval:paused:) schedule to update CountdownView
every 1 second.
188
7 Chapter 7: Saving Settings
By Caroline Begbie
Whenever your app closes, all the data entered, such as any ratings you’ve set or any
history you’ve recorded, is lost. For most apps to be useful, they have to persist data
between app sessions. Data persistence is a fancy way of saying “saving data to
permanent storage”.
In this chapter, you’ll explore how to store simple data using AppStorage and
SceneStorage. You’ll save the exercise ratings and, if you get called away mid-
exercise, your app will remember which exercise you were on and start there, instead
of at the welcome screen.
You’ll also learn about how to store data in Swift dictionaries and realize that string
manipulation is complicated.
189
SwiftUI Apprentice Chapter 7: Saving Settings
Data Persistence
Depending on what type of data you’re saving, there are different ways of persisting
your data:
• UserDefaults: Use this for saving user preferences for an app. This would be a
good way to save the ratings.
• Property List file: A macOS and iOS settings file that stores serialized objects.
Serialization means translating objects into a format that can be stored. This
would be a good format to store the history data, and you’ll do just that in the
following chapter.
• JSON file: An open standard text file that stores serialized objects. You’ll use this
format in Section 2.
• Core Data: An object graph with a macOS and iOS framework to store objects. For
further information, check out our book Core Data by Tutorials (https://bit.ly/
3fiStNp).
UserDefaults is a class that enables storing and retrieving data in a property list
(plist) file held with your app’s sandboxed data. It’s called “defaults” because you
should only use UserDefaults for simple app-wide settings. You should never store
data such as your history, which will get larger as time goes on.
➤ Open the project in this chapter’s starter folder. This is the same as the previous
chapter’s final project.
So far you’ve used iPad in previews. Remember to test your app just as much using
iPhone as well. To test data persistence, you’ll need to run the app in Simulator so
that you can examine the actual data on disk.
190
SwiftUI Apprentice Chapter 7: Saving Settings
AppStorage
@AppStorage is a property wrapper, similar to @State and @Binding, that allows
interaction between UserDefaults and your SwiftUI views.
You set up a ratings view that allows the user to rate the exercise difficulty from one
to five. You’ll save this rating to UserDefaults so that your ratings don’t disappear
when you close the app.
The source of truth for rating is currently in ExerciseView.swift, where you set up
a state property for it.
➤ Build and run, and choose an exercise. Tap the ratings view to score a rating for
the exercise. UserDefaults now stores your rating.
191
SwiftUI Apprentice Chapter 7: Saving Settings
AppStorage only allows a few types: String, Int, Double, Data, Bool and URL. For
simple pieces of data, such as user-configurable app settings, storing data to
UserDefaults with AppStorage is incredibly easy.
➤ Stop the app in Simulator, by swiping up from the bottom. Then, in Xcode, run the
app again and go to the same exercise. Your rating persists between launches.
Note: When using @AppStorage and @SceneStorage, always make sure you
exit the app in Simulator or on your device before terminating the app in
Xcode. Your app may not save data until the system notifies it of a change in
state.
You’ve solved the data persistence problem, but caused another. Unfortunately, as
you only have one rating key for all ratings, you are only storing a single value in
UserDefaults. When you go to another exercise, it has the same rating as the first
one. If you set a new rating, all the other exercises have that same rating.
You really need to store an array of ratings, with an entry for each exercise. For
example, an array of [1, 4, 3, 2] would store individual rating values for exercises
1 to 4. Before fixing this problem, you’ll find out how Xcode stores app data.
Data Directories
Skills you’ll learn in this section: what’s in an app bundle; data directories;
property list files; Dictionary
When you run your app in Simulator, Xcode creates a sandboxed directory
containing a standard set of subdirectories. Sandboxing is a security measure so no
other app will be able to access your app’s files.
192
SwiftUI Apprentice Chapter 7: Saving Settings
Conversely, your app will only be able to read files that iOS allows, and you won’t be
able to read files from any other app.
➤ From the Xcode menu, choose Product ▸ Show Build Folder in Finder.
Note: If you have build problems, or if you need more disk space, you can clear
the DerivedData folder.
App in Finder
When you build your app, Xcode creates a product with the same name as the
project.
193
SwiftUI Apprentice Chapter 7: Saving Settings
➤ Control-click HIITFit and choose Show Package Contents to see the contents
of the app bundle.
• Any app assets not in Assets.xcassets. In this app, there are four exercise videos.
• HIITFit executable.
The app bundle is read-only. Once the device loads your app, you can’t change the
contents of any of these files inside the app. If you have some default data included
with your bundle that your user should be able to change, you would need to copy
the bundle data to the user data directories when your user runs the app after first
installation.
Note: This would be another good use of UserDefaults. When you run the
app, store a Boolean — or the string version number — to mark that the app
has run. You can then check this flag or version number to see whether your
app needs to do any internal updates.
194
SwiftUI Apprentice Chapter 7: Saving Settings
You’ve already used the bundle when loading your video files with
Bundle.main.url(forResource:withExtension:). Generally, you won’t need to
look at the bundle files on disk but, if your app fails to load a bundle file for some
reason, it’s useful to go to the actual files included in the app and do a sanity check.
It’s easy to forget to check Target Membership for a file in the File inspector, for
example. In that case, the file wouldn’t be included in the app bundle.
.onAppear {
print(URL.documentsDirectory)
}
You print the URL of the app’s Documents directory to the console. Your app will run
onAppear(perform:) every time the view appears.
As well as the Documents directory, there are other significant directories, such as
Home, Movies, Pictures. You can find these in the URLApple documentation for
(https://apple.co/3zylHyL) under Type Properties. Remember that your app is
sandboxed, and each app will have its own app directories.
➤ Build and run in Simulator. Your app will print its Documents directory path in
the debug console.
Show in Finder
195
SwiftUI Apprentice Chapter 7: Saving Settings
This will open a new Finder window showing Simulator’s user directories:
Simulator directories
➤ The parent directory — in this example, 47379…F3198 — contains the app’s
sandboxed user directories. You’ll see other directories also named with UUIDs that
belong to other apps you may have worked on. Select the parent directory and drag it
to your Favorites sidebar, so you have quick access to it.
• URL.libraryDirectory: Library/. The directory for files that you don’t want to
expose to the user.
iPhone and iPad backups will save Documents and Library, excluding Library/
Caches.
196
SwiftUI Apprentice Chapter 7: Saving Settings
For example, instead of distributing HIITFit’s exercises in an array, you could store
them in a property list file and read them into an array at the start of the app:
Xcode formats property lists files in a readable format. This is the text version of the
property list file above:
The root of this file is an array that contains two exercises. Each exercise is of type
Dictionary with key values for the exercise properties.
197
SwiftUI Apprentice Chapter 7: Saving Settings
For example, you might create a Dictionary that holds ratings for exercises:
This is a Dictionary of type [String : Integer], where burpee is the key and 4 is
the value.
You can initialize with multiple values and add new values:
Dictionary contents
This last image is from a Swift Playground and shows you that dictionaries, unlike
arrays, have no guaranteed sequential order. The order the playground shows is
different than the order of creation.
If you haven’t used Swift Playgrounds before, they are fun and useful for testing
snippets of code. You’ll use a playground in Chapter 24, “Downloading Data”.
198
SwiftUI Apprentice Chapter 7: Saving Settings
• Dictionary
• Array
• String
• Number
• Boolean
• Data
• Date
With all these types available, you can see that direct storage to property list files is
more flexible than @AppStorage, which doesn’t support dictionaries or arrays. You
could decide that, to store your ratings array, maybe @AppStorage isn’t the way to go
after all. But hold on — all you have to do is a little data manipulation. You could
store your integer ratings as an array of characters, also known as a String.
199
SwiftUI Apprentice Chapter 7: Saving Settings
You’ll initially store the ratings as a string of "0000". When you need, for example,
the first exercise, you’ll read the first character in the string. When you tap a new
rating, you store the new rating back to the first character.
This is extensible. If you add more exercises, you simply have a longer string.
Strings aren’t as simple as they may appear. To support the ever-growing demand for
emojis, a string is made up of extended grapheme clusters. These are a sequence of
Unicode values, shown as a single character, where the platform supports it. They’re
used for some language characters and also for various emoji tag sequences, for
example skin tones and flags.
The Welsh flag uses seven tag sequences to construct the single character ! . On
platforms where the tag sequence is not supported, the flag will show as a black
flag " .
A String is a collection of these characters, very similar to an Array. Each element
of the String is a Character, type-aliased as String.Element.
200
SwiftUI Apprentice Chapter 7: Saving Settings
Just as with an array, you can iterate through a string using a for loop:
Because of the complicated nature of strings, you can’t index directly into a String.
But, you can do subscript operations using indices.
As you will see shortly, you can also insert a String into another String at an index,
using String.insert(contentsOf:at:), and insert a Character into a String,
using String.insert(_:at:).
Note: You can do so much string manipulation that you’ll need Use Your
Loaf’s Swift String cheat sheet (https://bit.ly/3aGRjWp)
Saving Ratings
Now that you’re going to store ratings, RatingView is a better source of truth than
ExerciseView. Instead of storing ratings in ExerciseView, you’ll pass the current
exercise index to RatingView, which can then read and write the rating.
➤ Toward the end of body, where the compile error shows, change
RatingView(rating: $rating) to:
RatingView(exerciseIndex: index)
You pass the current exercise index to the rating view. You’ll get a compile error until
you fix RatingView.
201
SwiftUI Apprentice Chapter 7: Saving Settings
Here you hold rating locally and set up ratings to be a string of four zeros.
Preview holds its own version of @AppStorage, which can be hard to clear.
To remove a key from the Preview UserDefaults, you need to set it to a nil value.
Only optional types can hold nil, so you define ratings as String?, with the ?
marking the property as optional. You can then set the @AppStorage ratings to
have a nil value, ensuring that your Preview doesn’t load previous values. You’ll
take another look at optionals in the next chapter.
You pass in the exercise index from Preview, so your app will now compile.
// 1
.onAppear {
// 2
let index = ratings.index(
ratings.startIndex,
offsetBy: exerciseIndex)
// 3
let character = ratings[index]
// 4
rating = character.wholeNumberValue ?? 0
}
202
SwiftUI Apprentice Chapter 7: Saving Settings
Swift can be a remarkably succinct language, and there’s a lot to unpack in this short
piece of code:
3. You extract the correct character from the string using the String.Index.
4. Convert the character to an integer. If the character is not an integer, the result
of wholeNumberValue will be an optional value of nil. The two question marks
are known as the nil coalescing operator. If the result of wholeNumberValue is
nil, then use the value after the question marks — in this case, zero. You’ll learn
more about optionals in the next chapter.
➤ Preview the view. Your stored ratings are currently 0000, and you’re previewing
exercise zero.
Zero Rating
➤ Change @AppStorage("ratings") private var ratings = "0000" to:
➤ Resume the preview, and the rating for exercise zero changes to four.
Rating of four
203
SwiftUI Apprentice Chapter 7: Saving Settings
Here you create a String.Index using exerciseIndex, as you did before. You create
a RangeExpression with index...index and replace the range with the new rating.
Note: You can find more information about RangeExpressions in the official
Apple documentation (https://apple.co/3qNxD8R).
updateRating(index: index)
➤ Build and run and replace all your ratings for all your exercises. Each exercise now
has its individual rating.
204
SwiftUI Apprentice Chapter 7: Saving Settings
AppStorage
You can remove rating from the property list file, as you no longer need it. The
ratings stored in the above property list file are:
• Squat: 3
• Step Up: 1
• Burpee: 2
• Sun Salute: 4
You should always be thinking of ways your code can fail. If you try to retrieve an out
of range value from an array, your app will crash. It’s the same with strings. If you try
to access a string index that is out of range, your app is dead in the water. It’s a
catastrophic error, because there is no way that the user can ever input the correct
length string, so your app will keep failing. As you control the ratings string, it’s
unlikely this would occur, but bugs happen, and it’s always best to avoid catastrophic
errors.
You can ensure that the string has the correct length when initializing RatingView.
Custom Initializers
➤ Add a new initializer to RatingView:
// 1
init(exerciseIndex: Int) {
self.exerciseIndex = exerciseIndex
205
SwiftUI Apprentice Chapter 7: Saving Settings
// 2
let desiredLength = Exercise.exercises.count
if ratings.count < desiredLength {
// 3
ratings = ratings.padding(
toLength: desiredLength,
withPad: "0",
startingAt: 0)
}
}
1. If you don’t define init() yourself, Xcode creates a default initializer that sets
up all the necessary properties. However, if you create a custom initializer, you
must initialize them yourself. Here, exerciseIndex is a required property, so you
receive it as a parameter and store it to the RatingView instance.
3. If ratings is too short, then you pad out the string with zeros.
To test this, in Simulator, choose Device ▸ Erase All Content and Settings… to
completely delete the app and clear caches.
➤ Build and run and go to an exercise. Then locate your app in Finder. Erasing all
contents and settings creates a completely new app sandbox, so open the path
printed in the console.
Zero padding
206
SwiftUI Apprentice Chapter 7: Saving Settings
Multiple Scenes
Skills you’ll learn in this section: multiple iPad windows
Perhaps your partner, dog or cat would like to exercise at the same time. Or maybe
you’re just really excited about HIITFit, and you’d like to view two exercises on iPad
at the same time. In iPad Split View, you can have a second window open so you can
compare your Squat to your Burpee.
➤ Select the top HIITFit group in the Project navigator. Select the HIITFit target,
then the Info tab.
➤ Locate Application Scene Manifest in the Custom iOS Target Properties, and
open the disclosure indicator. Enable Multiple Windows has a value of YES. When
this value is NO or not present, you won’t be able to have two windows of either your
own app, or yours plus another app, side-by-side.
These custom target properties are held in a file Info.plist. They configure your app
to act in certain ways. You’ll add new keys to this file to configure the launch screen
and allow writing of photos in Section 2.
207
SwiftUI Apprentice Chapter 7: Saving Settings
Non-reactive rating
208
SwiftUI Apprentice Chapter 7: Saving Settings
With AppStorage, you hold one ratings value per app, no matter how many
windows are open. You change ratings and update rating in
onTapGesture(count:perform:). The second window holds its own rating
instance. When you change the rating in one window, the second window should
react to this change and update and redraw its rating view.
Outdated rating
If you were showing a view with the ratings string from AppStorage, not the
extracted integer rating, AppStorage would automatically invalidate the view and
redraw it. However, because you’re converting the string to an integer, you’ll need to
perform that code on change of ratings.
The code you have in onAppear(perform:) does what you need. However, you need
it to run whenever ratings changes. SwiftUI provides another modifier —
onChange(of:perform:) — which does exactly that. Instead of duplicating the code,
you’ll create a new method and call the method twice.
➤ In onAppear(perform:), highlight:
209
SwiftUI Apprentice Chapter 7: Saving Settings
Swift tip: Note the fileprivate access control modifier in the new method.
This modifier allows access to convertRating() only inside
RatingView.swift.
.onChange(of: ratings) { _ in
convertRating()
}
Here you set up a reactive method that will call convertRating() whenever
ratings changes. If you were using only one window, you wouldn’t notice the effect,
but multiple windows can now react to the property changing in another window.
➤ Build and run the app with two windows side by side. Return to Exercise 1 in both
windows and change the rating in one window. The rating view in the other window
immediately redraws when you change the rating.
You opened two independent sessions of HIITFit in Simulator. If this app were
running on macOS, users would expect to be able to open any number of HIITFit
windows. You’ll now take a look at how SwiftUI handles multiple windows.
@main
struct HIITFitApp: App {
var body: some Scene {
WindowGroup {
ContentView()
...
}
}
}
210
SwiftUI Apprentice Chapter 7: Saving Settings
This simple code controls execution of your app. The @main attribute indicates the
entry point for the app and expects the structure to conform to the App protocol.
• ContentView: Everything you see in a SwiftUI app is a View. Although the SwiftUI
template creates ContentView, it’s a placeholder name, and you can rename it.
211
SwiftUI Apprentice Chapter 7: Saving Settings
➤ Build and run your app in iPad Simulator, in two windows, and go to Exercise 3 in
the second window. Exit the app in Simulator by swiping up from the bottom. Then
stop the app in Xcode. Remember that your app may not save data unless the device
notifies it that state has changed.
➤ Rerun the app and, because the app is completely refreshed with new states, the
app doesn’t remember that you were doing Exercise 3 in one of the windows.
212
SwiftUI Apprentice Chapter 7: Saving Settings
➤ Open ContentView.swift.
➤ Build and run the app. In the first window, go to Exercise 1, and in the second
window, go to Exercise 3.
➤ Exit the app in Simulator by swiping up from the bottom. Then, stop the app in
Xcode.
➤ Build and run again, and this time, the app remembers that you were viewing both
Exercise 1 and Exercise 3 and goes straight there.
Note: To reset SceneStorage in Simulator, you will have to clear the cache. In
Simulator, choose Device ▸ Erase All Content and Settings… and then re-
run your app.
Although you won’t realize this until the next chapter, introducing SceneStorage
has caused a problem with the way you’re initializing HistoryStore. Currently you
create HistoryStore in ContentView.swift as an environment object modifier on
TabView. SceneStorage reinitializes TabView when it stores selectedTab, so each
time you change the tab, you reinitialize HistoryStore. If you do an exercise your
history doesn’t save. You’ll fix this in the following chapter.
213
SwiftUI Apprentice Chapter 7: Saving Settings
Key Points
• You have several choices of where to store data. You should use @AppStorage and
@SceneStorage for lightweight data, and property lists, JSON or Core Data for
main app data that increases over time.
• Your app is sandboxed so that no other app can access its data. You are not able to
access the data from any other app either. Your app executable is held in the read-
only app bundle directory, with all your app’s assets.
• Property lists store serialized objects. If you want to store custom types in a
property list file, you must first convert them to a data type recognized by a
property list file, such as String or Boolean or Data.
• String manipulation can be quite complex, but Swift provides many supporting
methods to extract part of a string or append a string on another string.
• Manage scenes with @SceneStorage. Your app holds data per scene. iPads and
macOS can have multiple scenes, but an app run on iPhone only has one.
214
8 Chapter 8: Saving History
Data
By Caroline Begbie
@AppStorage is excellent for storing lightweight data such as settings and other app
initialization. You can store other app data in property list files, in a database such as
SQLite or Realm, or in Core Data. Since you’ve learned so much about property list
files already, you’ll save the history data to one in this chapter.
The saving and loading code itself is quite brief, but when dealing with data, you
should always be aware that errors might occur. As you would expect, Swift has
comprehensive error handling so that, if anything goes wrong, your app can recover
gracefully.
In this chapter, you’ll learn about error checking techniques as well as saving and
loading from a property list file. Specifically, you’ll learn about:
• Optionals: nil values are not allowed in Swift unless you define the property type
as Optional.
• Debugging: You’ll fix a bug by stepping through the code using breakpoints.
• Error Handling: You’ll throw and catch some errors, which is just as much fun as
it sounds. You’ll also alert the user when there is a problem.
• Closures: These are blocks of code that you can pass as parameters or use as
completion handlers.
• Serialization: Last but not least, you’ll translate your history data into a format
that can be stored.
215
SwiftUI Apprentice Chapter 8: Saving History Data
➤ Build and run your app. Start an exercise and tap Done to save the history. Your
app performs addDoneExercise(_:) and crashes with Fatal error: Index out of
range.
if today.isSameDay(as: exerciseDays[0].date) {
This line assumes that exerciseDays is never empty. If it’s empty, then trying to
access an array element at index zero is out of range. When users start the app for
the first time, their history will always be empty. A better way is to use optional
checking.
Using Optionals
Skills you’ll learn in this section: optionals; unwrapping; forced
unwrapping; filtering the debug console
216
SwiftUI Apprentice Chapter 8: Saving History Data
You may have learned that Booleans can be either true or false. But an optional
Boolean can hold nil, giving you a third alternative.
Checking for nil can be useful to prevent errors. At compile time, Xcode prevents
Swift properties from containing nil unless you’ve defined them as optional. At run
time, you can check that exerciseDays is not empty by checking the value of the
optional first:
if exerciseDays.first != nil {
if today.isSameDay(as: exerciseDays[0].date) {
...
}
}
When first is nil, the array is empty, but if first is not nil, then it’s safe to access
index 0 in the array. This is true, because exerciseDays doesn’t accept nil values.
You can have arrays with nil values by declaring them like this:
217
SwiftUI Apprentice Chapter 8: Saving History Data
Note: When the test is a simple one, you can shorten the test and assignment
from if let varName = varName {...} to if let varName {...}.
if let tells the compiler that whatever follows could result in nil. The property
first? with the added ? means that first is an optional and can contain nil.
If exerciseDays is empty, then first? will be nil and your app won’t perform the
conditional block, otherwise firstDate will contain the unwrapped first element in
exerciseDays.
• errorDay causes a compile error because you are trying to put an optional which
could contain nil into a property that can’t contain nil.
Unless you’re really certain that the value will never contain nil, don’t use
exclamation marks to force-unwrap it!
Multiple Conditionals
When checking whether you should add or insert the exercise into exerciseDays,
you also need a second conditional to check whether today is the same day as the
first date in the array.
218
SwiftUI Apprentice Chapter 8: Saving History Data
You can stack up conditionals, separating them with a comma. Your second
conditional evaluates the Boolean condition. If firstDate is not nil, and today is
the same day as firstDate, then the code block executes.
This will print the contents of exerciseDays to the debug console after adding or
inserting history.
Your app doesn’t crash, and your completed exercise prints out in the console.
Note: If you have trouble finding your print commands in the console, you can
filter on words that you expect to see, such as in this case History.
Debugging HistoryStore
Skills you’ll learn in this section: breakpoints
219
SwiftUI Apprentice Chapter 8: Saving History Data
The first and often most difficult debugging step is to find where the bug occurs and
be able to reproduce it consistently. Start from the beginning and proceed patiently.
Document what should happen and what actually happens.
➤ Build and run, complete an exercise and tap Done. The contents of exerciseDays
print out correctly in the debug console. Tap History and the view is empty, when it
should show the contents of exerciseDays. This error happens every time, so you
can be confident at being able to reproduce it.
Error Reproduction
An Introduction to Breakpoints
When you place breakpoints in your app, Xcode pauses execution and allows you to
examine the state of variables and, then, step through code.
➤ Still running the app, with the first exercise done, in Xcode click the line number
to the left of let today = Date() in addDoneExercise(_:). This adds a breakpoint
at that line.
Breakpoint
➤ Without stopping your app, complete a second exercise and tap Done.
220
SwiftUI Apprentice Chapter 8: Saving History Data
Execution paused
Above the debug console, you have icons to control execution:
3. Step over: If the next line to execute includes a method call, stop again after that
method completes.
4. Step into/out: If your code calls a method, you can step into the method and
continue stepping through it. If you step over a method, it will still be executed,
but execution won’t be paused after every instruction.
221
SwiftUI Apprentice Chapter 8: Saving History Data
➤ Click Step over to step over to the next instruction. today is now instantiated and
contains a value.
➤ In the debug console, remove any filters, and at the (lldb) prompt, enter:
po today
po exerciseDays
po prints out in the debug console the contents of today and exerciseDays:
Even though exerciseDays should have data from the previous exercise, it now
contains zero elements. Somewhere between tapping Done on two exercises,
exerciseDays is getting reset.
➤ Step over each instruction and examine the variables to make sure they make
sense to you. When you’ve finished, drag the breakpoint out of the gutter to remove
it.
The next step in your debugging operation is to find the source of truth for
exerciseDays and when that source of truth gets initialized. You don’t have to look
very far in this case, as exerciseDays is owned by HistoryStore.
print("Initializing HistoryStore")
222
SwiftUI Apprentice Chapter 8: Saving History Data
➤ Build and run, and reproduce your error by performing an exercise and tapping
Done. In the debug console, filter on History.
Initializing HistoryStore
Now you can see why exerciseDays is empty after performing an exercise.
Something is reinitializing HistoryStore!
You may remember from the end of the previous chapter that @SceneStorage
reinitializes TabView when it stores selectedTab. The redraw re-executes
environmentObject(HistoryStore()) and incorrectly initializes HistoryStore
with all its data.
You’ve now successfully debugged why your history data is empty. All you have to do
now is decide what to do about it.
This first step to fix this is to move the initialization of HistoryStore up a level in
the view hierarchy.
@StateObject
@State, being so transient, is incompatible with reference objects and, as
HistoryStore is a class, @StateObject is the right choice here. @StateObject is a
read-only property wrapper. You get one chance to initialize it, and you can’t change
the property once you set it.
223
SwiftUI Apprentice Chapter 8: Saving History Data
.environmentObject(historyStore)
You place the store into the environment. In case you’re confused about all the
property wrappers you’ve used so far, you’ll review them in Chapter 11, “Managing
Data With Property Wrappers”.
➤ Build and run, perform all four exercises, tapping Done after each, and check your
history:
Now you can continue on and save your history so that it doesn’t reset every time
you restart your app.
224
SwiftUI Apprentice Chapter 8: Saving History Data
Saving and loading data is serious business, and if any errors occur, you’ll need to
know about them. There isn’t a lot you can do about file system errors, but you can
let your users know that there has been an error, and they need to take some action.
To create a method that raises an error, you mark it with throws and add a throw
statement.
Here, you’ll read the history data from a file on disk. Currently, this method always
raises an error, but you’ll come back to it later when you add the loading code. When
you throw an error, the method returns immediately and doesn’t execute any
following code. It’s the caller that should handle the error, not the throwing method.
do {
try methodThatThrows()
} catch {
// take action on error
}
225
SwiftUI Apprentice Chapter 8: Saving History Data
If you don’t need to handle any errors specifically, instead of enclosing the try in a
do..catch statement, you can call the method with try?. For example, try?
load(), converts an error result to nil and execution continues.
If you have several errors, you can add a pattern to catch. For example:
do {
try load()
} catch FileError.loadFailure {
// load failed
} catch {
// any other error
}
do {
try load()
} catch {
print("Error:", error)
}
load() throws, so you embed try in a do...catch. If there’s an error, the catch
block executes.
➤ Build and run. Currently load() always throws, so in the debug console, you’ll see
your printed error: Error: loadFailure. (Remember to clear the debug console Filter
if you don’t see the error.)
Alerts
Skills you’ll learn in this section: Alert view
If loading the history data fails, you could either report a catastrophic error and
crash the app or, preferably, you could report an error and continue with no history.
226
SwiftUI Apprentice Chapter 8: Saving History Data
When you release your app, your users won’t be able to see print statements, so
you’ll have to provide them with more visible communication. When you want to
give the user a choice of actions, you can use an ActionSheet but, for simple
notifications, an Alert is perfect. An Alert pops up with a title and a message and
pauses app execution until the user taps OK.
An alert
➤ Open HistoryStore.swift and add a new property to HistoryStore:
loadingError = true
.alert(isPresented: $historyStore.loadingError) {
Alert(
title: Text("History"),
message: Text(
"""
Unfortunately we can't load your past history.
Email support:
[email protected]
"""))
}
When loadingError is true, you show an Alert view with the supplied Text title
and message. Surround the string with three """ to format your string on multiple
lines.
227
SwiftUI Apprentice Chapter 8: Saving History Data
➤ Build and run. Instead of seeing the failure message in the console, you see an
alert.
History Alert
➤ Tap OK. Alert resets historyStore.loadingError and your app continues with
empty history data.
➤ Now that you’ve tested error checking, open HistoryStore.swift and remove
throw FileError.loadFailure from load().
Note: You can find out more about error handling in our Swift Apprentice
book (https://www.kodeco.com/books/swift-apprentice), which has an entire
chapter on the subject.
228
SwiftUI Apprentice Chapter 8: Saving History Data
Saving History
Skills you’ll learn in this section: closures; map(_:); transforming arrays
You need to have some history data stored in order to load it. So, you’re now going to
implement saving data and then come back and complete load().
You add the file name to the documents path. This gives you the full URL of the file
to which you’ll write the history data.
You’ll save the history data to a property list (plist) file. As mentioned in the
previous chapter, the root of a property list file can be a dictionary or an array.
Dictionaries are useful when you have a number of discrete values that you can
reference by key. But in the case of history, you have an array of ExerciseDay to
store, so your root will be an array.
Property list files can only store a few standard types, and ExerciseDay, being a
custom type, is not one of them. In Chapter 19, “Saving Files”, you’ll learn about
Codable and how to save custom types to files but, for now, the easy way is to
separate out each ExerciseDay element into an array of Any and append this to the
array that you will save to disk.
229
SwiftUI Apprentice Chapter 8: Saving History Data
For each element in the loop, you construct an array with a String, a Date and a
[String]. You can’t store multiple types in an Array, so you create an array of type
[Any] and append this element to plistData.
plistData is a type [[Any]]. This is a two dimensional array, which is an array that
contains an array. After saving two elements, plistData will look like this:
A closure is simply a block of code between two curly braces. Closures can look
complicated, but if you recognize how to put a closure together, you’ll find that you
use them often, just as SwiftUI does. Notice a closure’s similarity to a function:
Functions are closures — blocks of code — with names.
A closure
The closure is the part between the two curly braces {...}. In the example above,
you assign the closure to a variable addition.
The signature of addition is (Int, Int) -> Int and declares that you will pass in
two integers and return one integer.
230
SwiftUI Apprentice Chapter 8: Saving History Data
It’s important to recognize that when you assign a closure to a variable, the closure
code doesn’t execute. The variable addition contains the code return a + b, not
the actual result.
Closure result
You pass in 1 and 2 as the two integer parameters and receive back an integer:
Closure signature
Another example:
This is the closure that would perform this conversion for a single ExerciseDay
element:
231
SwiftUI Apprentice Chapter 8: Saving History Data
You could send result to map which returns an array of the results:
map(_:) takes the closure result, executes it for every element in exerciseDays
and returns an array of the results.
Rather than separating out into a closure variable, it’s more common to declare the
map operation together with the closure.
func map<T>(
_ transform: (Self.Element) throws -> T) rethrows -> [T]
• T is a generic type. You’ll discover more about generics in Section 2, but here T is
equivalent to [Any].
232
SwiftUI Apprentice Chapter 8: Saving History Data
Deconstructing map(_:)
This code gives exactly the same result as the previous for loop. Option-click
plistData, and you’ll see that its type is [[Any]], just as before.
Type of plistData
One advantage of using map(_:) rather than dynamically appending to an array in a
for loop, is that you declare plistData as a constant with let. This is some extra
safety, so that you know that you won’t accidentally change plistData further down
the line.
233
SwiftUI Apprentice Chapter 8: Saving History Data
An Alternative Construct
When you have a simple transformation, and you don’t need to spell out all the
parameters in full, you can use $0, $1, $2, $... as replacements for multiple
parameter names.
Here you have one input parameter, which you can replace with $0. When using $0,
you don’t specify the parameter name after the first curly brace {.
Again, this code gives exactly the same result. Option-click plistData, and you’ll
see that its type is still [[Any]].
Type of plistData
do {
// 1
234
SwiftUI Apprentice Chapter 8: Saving History Data
1. You convert your history data to a serialized property list format. The result is a
Data type, which is a buffer of bytes.
3. The conversion and writing may throw errors, which you catch by throwing an
error.
do {
try save()
} catch {
fatalError(error.localizedDescription)
}
If there’s an error in saving, you crash the app, printing out the string description of
your error. This isn’t a great way to ship your app, and you may want to change it
later.
➤ Build and run and do an exercise. Tap Done and your history file will save.
235
SwiftUI Apprentice Chapter 8: Saving History Data
See how the property list file matches with your data:
• Root: The property list array you saved in plistData. This is an array of type
[[Any]].
• Item 2: The array of exercises that you have performed and tapped Done to save.
In this example, the user has exercised on one day with two exercises: Sun Salute
and Burpee.
Loading is very similar to saving, but with some type checking to ensure that your
data conforms to the types you are expecting.
236
SwiftUI Apprentice Chapter 8: Saving History Data
1. Read the data file into a byte buffer. This buffer is in the property list format. If
history.plist doesn’t exist on disk, Data(contentsOf:) will throw an error.
Throwing an error is not correct in this case, as there will be no history when
your user first launches your app. You’ll fix this error at the end of this chapter.
2. Convert the property list format into a format that your app can read.
3. When you serialize from a property list, the result is always of type Any. To cast
to another type, you use the type cast operator as?. This will return nil if the
type cast fails. Because you wrote history.plist yourself, you can be pretty sure
about the contents, and you can cast plistData from type Any to the [[Any]]
type that you serialized out to file. If for some reason history.plist isn’t of type
[[Any]], you provide a fall-back of an empty array using the nil coalescing
operator ??.
➤ Build and run, and tap History. The history you saved out to your property list file
will load in the modal.
Saved history
237
SwiftUI Apprentice Chapter 8: Saving History Data
Because the file doesn’t exist any more, your loading error appears because load()
fails on try Data(contentsOf:).
Load error
Before throwing an error, you should first check whether history.plist exists. On first
run of your app, the plist file will never exist, so you should ignore the loading error.
try? returns nil if the operation fails. Using guard, you can jump out of a method if
a condition is not met. guard let is similar to if let in that you assign an optional
to a non-optional variable and check it isn’t nil. You always provide an else branch
with guard, where you specify what to do when the guard conditional test fails.
Generally you return from the method, but you could instead use
fatalError(_:file:line:) to crash the app.
238
SwiftUI Apprentice Chapter 8: Saving History Data
➤ Build and run the app. The loading error has gone and you can start recording
your exercises.
Final result
239
SwiftUI Apprentice Chapter 8: Saving History Data
Key Points
• Optionals are properties that can contain nil. Optionals make your code more
secure, as the compiler won’t allow you to assign nil to non-optional properties.
You can use guard let to unwrap an optional or exit the current method if the
optional contains nil.
• Use breakpoints to halt execution and step through code to confirm that it’s
working correctly and that variables contain the values you expect.
• Use @StateObject to hold your data store. Your app will only initialize a state
object once.
• Closures are chunks of code that you can pass around just as you would any other
object. You can assign them to variables or provide them as parameters to
methods. Array has a number of methods requiring closures to transform its
elements into a new array.
240
9 Chapter 9: Refining Your
App
By Caroline Begbie
While you’ve been toiling making your app functional, your designer has been busy
coming up with a stunning eye-catching design. One of the strengths of SwiftUI is
that, as long as you’ve been encapsulating views and separating them out along the
way, it’s easy to restyle the UI without upsetting the main functionality.
In this chapter, you’ll style some of the views for iPhone, making sure that they work
on all iPhone devices.
App design
241
SwiftUI Apprentice Chapter 9: Refining Your App
Creating individual reusable elements is a good place to start. Looking at the design,
you’ll have to style:
2. An embossed button for History and the exercise rating. The History button is a
capsule shape, while the rating is round.
The starter app contains the colors and images that you’ll need in the asset catalog.
There’s also some code for creating the welcome image and text in
WelcomeImages.swift.
Neumorphism
Skills you’ll learn in this section: neumorphism
The style of design used in HIITFit, where the background and controls are one
single color, is called neumorphism. You achieve the look with shading rather than
with colors.
In the old days, peak iPhone design had skeuomorphic interfaces with realistic
surfaces, so you had wood and fabric textures with dials that looked real throughout
your UI. iOS 7 went in the opposite direction with minimalistic flat design. The name
Neumorphism comes from New + Skeuomorphism and refers to minimalism
combined with realistic shadows.
Neumorphism
Essentially, you choose a theme color. You then choose a lighter tint and a darker
shade of that theme color for the highlight and shadow. You can define colors with
either red, green, blue (RGB) or hue, saturation and lightness (HSL). When shifting
tones within one color, HSL is the easier model to use as you keep the same hue. The
base color in the picture above is Hue: 166, Saturation: 54, Lightness: 59. The lighter
highlight color has the same Hue and Saturation, but a Lightness: 71. Similarly, the
darker shadow color has a Lightness: 30.
242
SwiftUI Apprentice Chapter 9: Refining Your App
Here you create a plain vanilla button with a preview sized to fit the button.
Assets.xcassets holds the background color “background”.
Plain button
243
SwiftUI Apprentice Chapter 9: Refining Your App
The text style on both the raised buttons in your app have the same design.
extension Text {
func raisedButtonTextStyle() -> some View {
self
.font(.body)
.fontWeight(.bold)
}
}
.raisedButtonTextStyle()
Abstracting the style into a modifier makes your app more robust. If you want to
change the text style of the buttons, simply change raisedButtonTextStyle() and
the changes will reflect wherever you used this style.
Styled text
Styles
Skills you’ll learn in this section: view styles; button style; shadows
Apple knows that you often want to style objects, so it created a range of style
protocols for you to customize (https://apple.co/3kzvD2e). You’ve already used one
of these styles, the built-in PageTabViewStyle, on your TabView. Styling text is not
on that list, which is why you created your own view modifier.
244
SwiftUI Apprentice Chapter 9: Refining Your App
Here you make a simple style giving the button text a red background. ButtonStyle
has one required method: makeBody(configuration:). The configuration gives you
the button’s label text and a Boolean isPressed telling you whether the button is
currently depressed.
Swift Tip: If you want to customize how the button action triggers with
gestures, you can use PrimitiveButtonStyle instead of ButtonStyle.
This makes using the button style more Swift-y. Instead of adding a button style:
buttonStyle(RaisedButtonStyle()), you can instead use:
buttonStyle(.raised).
You can use this button style to change all your buttons in a view hierarchy.
.buttonStyle(.raised)
You tell ContentView that whenever there’s a button in the hierarchy, it should use
your custom style.
245
SwiftUI Apprentice Chapter 9: Refining Your App
➤ The buttons in your app won’t all use the same style so
remove .buttonStyle(.raised) from HIITFitApp.
.buttonStyle(.raised)
You can now preview your button style as you change it.
246
SwiftUI Apprentice Chapter 9: Refining Your App
When you set frame(maxWidth:) to .infinity, you ask the view to take up as much
width as its parent gives it. Add some padding around the label text at top and
bottom. For the background, use a Capsule shape.
Initial button
When you use Shapes, such as Rectangle, Circle and Capsule, the default fill color
is black, so you’ll change that in your neumorphic style to match the background
color.
Shadows
You have two choices when adding shadows. You can choose a simple all round
shadow, with a radius. The radius is how many pixels to blur out to. A default shadow
with radius of zero places a faint gray line around the object, which can be attractive.
The other alternative is to specify the color, the amount of blur radius, and the offset
of the shadow from the center.
Shadows
247
SwiftUI Apprentice Chapter 9: Refining Your App
.foregroundColor(Color("background"))
.shadow(color: Color("drop-shadow"), radius: 4, x: 6, y: 6)
.shadow(color: Color("drop-highlight"), radius: 4, x: -6, y: -6)
Watch the button preview change as you add these modifiers. Your darker shadow is
offset by six pixels to the right and down, whereas the highlight is offset by six pixels
to the left and up. When you add the highlight, the button really pops off the screen.
Button styling
The buttons work in Dark Mode too, because each color in the asset catalog has a
value for both Light Mode and Dark Mode. You’ll learn more about the asset catalog
in Chapter 16, “Adding Assets to Your App”.
Your button is finished, so you can now replace your three buttons in your app with
this one.
➤ Open WelcomeView.swift and locate the button code for Get Started. Replace
the button code and all the button modifiers with:
Button(action: { selectedTab = 0 }) {
Text("Get Started")
.raisedButtonTextStyle()
}
.buttonStyle(.raised)
.padding()
248
SwiftUI Apprentice Chapter 9: Refining Your App
Here you use your new text and button styles to create your new button. In Live
Preview, even though you haven’t yet changed the background color, it looks great.
You pass in the button text and an action closure. The action closure of type () ->
Void takes no parameters and returns nothing. Inside Button’s action closure, you
perform action().
➤ In the preview where you have a compile error, change RaisedButton() to:
RaisedButton(
buttonText: "Get Started",
action: {
print("Hello World")
})
249
SwiftUI Apprentice Chapter 9: Refining Your App
When the user taps the button marked Get Started, your app prints Hello World in
the console. (Of course a preview doesn’t print anything, so nothing will show.)
When a closure is the method’s last parameter, the preferred way of calling it is to
use special trailing closure syntax.
With trailing closure syntax, you remove the action label and take the closure out of
the method’s calling parentheses.
Open WelcomeView.swift and create a new property for the Get Started button:
➤ In body, change your previous Get Started button code, including modifiers, to:
getStartedButton
That code is a lot more succinct but still descriptive and has the same functionality
as before.
At the end of the next chapter, the challenge project moves the Done button to a
new modal view, so you don’t need to change it here.
250
SwiftUI Apprentice Chapter 9: Refining Your App
The History button will have an embossed border in the shape of a capsule. If you
remember from the start of the chapter, the rating view will also have an embossed
border. Your new button needs to be able to contain any content, not just text. For
this reason, you’ll create a new button style and not a new button structure.
251
SwiftUI Apprentice Chapter 9: Refining Your App
Here you use stroke(_:linewidth:) to outline the capsule instead of filling it with
color. You’ll learn more about shapes and fills in Chapter 18, “Paths & Custom
Shapes”. You offset the capsule outline by half the width of the stroke, which centers
the content.
Your capsule-shaped button is now ready for use in your app. However, looking back
at the design at the beginning of the chapter, the designer has placed the ratings in a
circular embossed button. You can make your button more useful by allowing
different shapes.
enum EmbossedButtonShape {
case round, capsule
}
252
SwiftUI Apprentice Chapter 9: Refining Your App
shape()
Here you return the desired shape. Unfortunately, you get a compile error. You’ll look
at this problem in more depth in Section 2, but for now, you just need to understand
that the compiler expects some View to be one type of view. You’re returning either a
Circle or a Capsule, determined at run time, so the compiler doesn’t know which
type some View should be at compile time.
253
SwiftUI Apprentice Chapter 9: Refining Your App
@ViewBuilder
Skills you’ll learn in this section: view builder attribute
There are several ways of dealing with this problem. One way is to return a Group
from shape() and place switch inside Group.
Another way is to use the function builder @ViewBuilder. Various built-in views,
such as HStack and VStack can display various types of views, and they achieve this
by using @ViewBuilder. Shortly, you’ll create your own container view where you
can stack up other views just as VStack does.
@ViewBuilder
Internally, @ViewBuilder takes in up to ten views and combines them into one
TupleView. A tuple is a loosely formed type made up of several items.
buildBlock(...)
The other nine buildBlock(...) methods are the same except for the different
number of views passed in.
254
SwiftUI Apprentice Chapter 9: Refining Your App
.buttonStyle(EmbossedButtonStyle(buttonShape: .round))
.background(
GeometryReader { geometry in
shape(size: geometry.size)
.foregroundColor(Color("background"))
.shadow(color: shadow, radius: 1, x: 2, y: 2)
.shadow(color: highlight, radius: 1, x: -2, y: -2)
.offset(x: -1, y: -1)
})
255
SwiftUI Apprentice Chapter 9: Refining Your App
You’re now passing to shape(size:) the size of the contents of the button, so you
can determine the larger of width or height.
.frame(
width: max(size.width, size.height),
height: max(size.width, size.height))
Here you set the frame to the larger of the width or height.
In the selectable preview, you can see that the circle takes the correct diameter of the
width of the button contents, but starts at the top.
.offset(x: -1)
.offset(y: -max(size.width, size.height) / 2 +
min(size.width, size.height) / 2)
You offset the circle in the x direction by half of the width of the stroke. In the y
direction, you offset the circle by half the diameter plus the smaller of half the width
or height.
256
SwiftUI Apprentice Chapter 9: Refining Your App
Here you format a new History button and use the default capsule shape for the
button style.
➤ In body, replace:
Button("History") {
showHistory.toggle()
}
.sheet(isPresented: $showHistory) {
HistoryView(showHistory: $showHistory)
}
.padding(.bottom)
with:
historyButton
.sheet(isPresented: $showHistory) {
HistoryView(showHistory: $showHistory)
}
➤ Copy the var historyButton code, open ExerciseView.swift and paste the code
into ExerciseView.
➤ In body, replace:
Button("History") {
showHistory.toggle()
}
with:
historyButton
257
SwiftUI Apprentice Chapter 9: Refining Your App
Notice as you replace body’s button code with properties describing the views, the
code becomes a lot more readable.
Button(action: {
updateRating(index: index)
}, label: {
Image(systemName: "waveform.path.ecg")
.foregroundColor(
index > rating ? offColor : onColor)
.font(.body)
})
.buttonStyle(EmbossedButtonStyle(buttonShape: .round))
.onChange(of: ratings) { _ in
convertRating()
}
.onAppear {
convertRating()
}
You embed Image inside the new embossed button as the label, and this time, you
use the round embossed style.
New buttons
258
SwiftUI Apprentice Chapter 9: Refining Your App
Looking at the design at the beginning of the chapter, the tab views have a purple/
blue gradient background for the header and a gray background with round corners
for the rest of the view.
You can make this gray background into a container view and embed WelcomeView
and ExerciseView inside it. The container view will be a @ViewBuilder. It will take
in any kind of view content as a parameter and add its own formatting to the view
stack. This is how HStack and VStack work.
Content is a generic. Generics make Swift very flexible and let you create methods
that work on multiple types without compile errors. Here, Content takes on the type
with which you initialize the view. You’ll learn more about generics in Chapter 15,
“Structures, Classes & Protocols”.
You’ll recognize the argument of the initializer as a closure. It’s a closure that takes
in no parameters and returns a generic value Content. In the initializer, you run the
closure and place the result of the closure in ContainerView’s local storage.
You mark the closure method with the @ViewBuilder attribute, allowing it to return
multiple child views of any type.
259
SwiftUI Apprentice Chapter 9: Refining Your App
The view here is the result of the content closure that the initializer performed.
You create a VStack of two buttons. You send ContainerView the VStack as the
content closure parameter. ContainerView then shows the result of running the
closure content.
Preview of ContainerView
In this example, ContainerView merely returns the content, which is a VStack. Your
container view will format the background on which the content resides. You can
then present any content and the background will be the same.
260
SwiftUI Apprentice Chapter 9: Refining Your App
.foregroundColor(Color("background"))
VStack {
Spacer()
Rectangle()
.frame(height: 25)
.foregroundColor(Color("background"))
}
content
}
}
Here you create a rounded rectangle using the background color from the asset
catalog. You don’t want the bottom corners to be rounded, so you add a rectangle
with sharp corners at the bottom to cover up the corners.
Finished ContainerView
Your container view is now finished. You can construct any views and present them
with the same background. It’s a good idea not to add unnecessary padding to the
actual container view, as that reduces the flexibility. Here the preview provides the
padding, but shortly you’ll make the container view go right to the edges.
261
SwiftUI Apprentice Chapter 9: Refining Your App
Designing WelcomeView
Skills you’ll learn in this section: refactoring with view properties; the safe
area
262
SwiftUI Apprentice Chapter 9: Refining Your App
}
}
}
Here you use the images and text from WelcomeImages.swift. Wherever you can
refactor your code into smaller chunks, you should. This code is much clearer and
easier to read. You embed the top VStack in GeometryReader so that you’ll be able
to determine the size available for the container view.
➤ Embed the second VStack — the one containing the images and text — in your
ContainerView and add the modifier to determine its height:
// container view
ContainerView {
VStack {
...
}
}
.frame(height: geometry.size.height * 0.8)
Using the size given by GeometryReader, the container view will take up 80% of the
available space. You’ll take a further look at GeometryReader in Chapter 20,
“Delightful UX - Layout”.
This will show the text at various accessibility levels. You want to make sure your
app looks great in all circumstances.
263
SwiftUI Apprentice Chapter 9: Refining Your App
The text fits horizontally, and you ask SwiftUI to fix an ideal vertical size.
ViewThatFits
Using ViewThatFits, you can present alternative layouts. Work out what is
important for interaction with your app. For the larger size text variants, you could
dispense with the images.
ViewThatFits {
VStack {
WelcomeView.images
WelcomeView.welcomeText
getStartedButton
Spacer()
historyButton
}
VStack {
WelcomeView.welcomeText
getStartedButton
264
SwiftUI Apprentice Chapter 9: Refining Your App
Spacer()
historyButton
}
}
Your app will use the first VStack wherever it can, but when space is tight, it will use
the alternative one.
ViewThatFits
The images won’t show on small devices with large text. Always remember to
preview your app on multiple devices with all the variants.
➤ Unpin ContentView and change your run destination back to iPhone 14 Pro.
Gradients
Skills you’ll learn in this section: gradient views
SwiftUI makes using gradients really easy. You simply define the gradient colors in
an array. As a background behind the header view, you’re going to use a lovely purple
to blue gradient, using the predefined colors in the asset catalog.
➤ Create a new SwiftUI View file called GradientBackground.swift and add a new
property to GradientBackground:
265
SwiftUI Apprentice Chapter 9: Refining Your App
Color("gradient-bottom")
])
}
You start the gradient at the top and continue down to the bottom. If you want the
gradient to be diagonal, you can use .topLeading as the start point
and .bottomTrailing as the end point.
Initial gradient
266
SwiftUI Apprentice Chapter 9: Refining Your App
ZStack {
GradientBackground()
TabView(selection: $selectedTab) {
...
}
...
}
Gradient background
In Live Preview, your gradient shows behind the header view, but doesn’t cover the
dynamic island or the bottom of the screen.
267
SwiftUI Apprentice Chapter 9: Refining Your App
Devices without a physical home button have a safe area at the bottom of the screen
where you swipe up to leave the app.
.ignoresSafeArea()
268
SwiftUI Apprentice Chapter 9: Refining Your App
Gradient(colors: [
Color("gradient-top"),
Color("gradient-bottom"),
Color("background")
])
Introducing this third color means that the gradient is now divided equally between
the three colors and gives a less pleasing purple to blue gradient.
Here you use purple to blue for 90% of the gradient. At the 90% mark, you switch to
the background color for the rest of the gradient. As you have two stops right next to
each other, you get a sharp line across instead of a gradient.
269
SwiftUI Apprentice Chapter 9: Refining Your App
If you want a striped background, you can achieve this using color stops in this way.
Multiple devices
You could come up with better layouts for iPad, and you now have all the tools at
your disposal to do that.
270
SwiftUI Apprentice Chapter 9: Refining Your App
Challenge
Your challenge is to continue styling. With ContentView pinned, style HeaderView.
Finished HeaderView
Functionality will remain the same, but instead of numbers, you’ll have circles. A
faded circle behind the circle indicates the current page. You can achieve
transparency with the modifier opacity(:), where opacity is between zero and one.
ExerciseView doesn’t look so hot with the gradient background, so embed this in
ContainerView just as you did in WelcomeView. Also, match the rating color with the
design color using the supplied color “ratings”.
271
SwiftUI Apprentice Chapter 9: Refining Your App
Key Points
• It’s not always possible to spend money on hiring a designer, but you should
definitely spend time making your app as attractive and friendly as possible. Try
various designs out and offer them to your testers for their opinions.
• Neumorphism is a simple style that works well. Keep up with designer trends at
https://dribbble.com.
• Style protocols allow you to customize various view types to fit in with your
desired design.
• Using @ViewBuilder, you can return varying types of views from methods and
properties. It’s easy to create custom container views that have added styling or
functionality.
• You can layer background colors in the safe area, but don’t place any of your user
interface there.
• Gradients are an easy way to create a stand-out design. You can find interesting
gradients at https://uigradients.com.
272
10 Chapter 10: Working With
Datasets
By Caroline Begbie
Now that you know how to collect and store user history, you’ll want to present the
data in a user-friendly format. In this chapter, you’ll learn how to deal with sets of
data.
First, you’ll allow the user to modify and delete the history data. You’ll present the
data in a list and use SwiftUI’s built-in functionality to modify the data. Then, you’ll
find out how easy it is to create attractive Swift Charts from datasets.
273
SwiftUI Apprentice Chapter 10: Working With Datasets
This project is the almost same as the previous chapter’s challenge project with
these changes:
• On first run of the project on Simulator, when there is no history, the app will run
HistoryStore.copyHistoryTestData(), in HistoryStoreDevData.swift. This
method copies a sample history.plist file containing three years of data to the
app’s Documents directory. Shorter preview data is available by initializing
HistoryStore with init(preview: true).
➤ In Simulator, choose Device ▸ Erase All Contents and Settings…. Erase all the
contents to ensure that you start with no history data.
274
SwiftUI Apprentice Chapter 10: Working With Datasets
➤ Build and run the app, and in the console, you’ll see Sample History data copied
to Documents directory, followed by your Documents URL. Tap the History button
to see the sample data.
Sample data
In the console, you’ll see error messages: ForEach<Array, String, Text>: the ID
Burpee occurs multiple times within the collection, this will give undefined
results!. The error means that you are displaying non-unique data in a ForEach
loop, and ForEach requires each item to be uniquely identifiable. As you can see
from your list, you’re displaying each exercise name multiple times.
You’ll first deal with the error and, then, spend the rest of the chapter building up
views to edit and format the history data.
275
SwiftUI Apprentice Chapter 10: Working With Datasets
Accumulating Data
Skills you’ll learn in this section: Sets; badges
Instead of showing all the exercises on each line, you’ll show a list of dates, with the
number of times you’ve performed the exercises accumulated within those dates.
Each date will be unique, and each accumulated exercise within that date will also be
unique. The ForEach loops will then show unique data with no errors.
For example, a Set created from this Array of exercises for July 16th:
Here you take the array of exercises, create a set of unique instances, and then return
an array created from that set, sorted alphabetically.
276
SwiftUI Apprentice Chapter 10: Working With Datasets
day.uniqueExercises
Instead of listing all the exercises, you list only the unique instances.
➤ Build and run the app, and tap the History button.
You no longer get an error printed in the console as all the values listed are unique.
Unique values
Unfortunately the seventeen squats you might have achieved in a day have all been
reduced to one listing. Time to add the accumulated number.
277
SwiftUI Apprentice Chapter 10: Working With Datasets
Here you pass in an exercise name and retrieve all the instances of that exercise from
the exercises array. You return the count of those instances.
.badge(day.countExercise(exercise: exercise))
A badge provides supplementary information in a list. This badge will show the
number of times you performed an exercise.
➤ Preview the view. Instead of repeating the listings, each exercise now shows the
number of times you’ve performed it.
Unique values
278
SwiftUI Apprentice Chapter 10: Working With Datasets
Lists
Skills you’ll learn in this section: Listing data; deleting items from lists;
collapsing hierarchical data; the Edit button
A List is a container that shows elements from a collection of data. Each element is
presented on a row. This is similar to the Form you’ve been using, but your current
usage is much more a listing of data than creating a form where you might ask for
input from the user.
Editable Lists
Being able to edit lists of data is a common requirement. In this app, you may want
to reset your daily exercise. Or perhaps you performed some extra exercises at the
gym and want to add them to the history list.
The Apple standard way of editing rows in lists is to have both an Edit button in the
navigation bar and a swipe action to delete. You’ll first add the swipe action and then
add the button. Most of the behavior is built-in.
With your data, each item in the file has a unique date, and the exercises for that
date are held in a single array.
279
SwiftUI Apprentice Chapter 10: Working With Datasets
Listing dates
➤ In body, replace Form and its contents with:
When you have a List with a ForEach following, you can compress them into one.
The $ binding syntax makes the data mutable so that you can delete it. The edit
action here is delete. Another edit action is move, which allows you to move rows
around in the list. You can try this out, but it doesn’t make sense to reorder the dates
here.
➤ In Live Preview, swipe a date to the left. You’ll first see the Delete button, and you
can either continue the swipe or tap the button to delete the row.
280
SwiftUI Apprentice Chapter 10: Working With Datasets
You’ll save the history data when the user closes the History view.
.onDisappear {
try? history.save()
}
When the view disappears, save the data. However, if the user doesn’t close the
History view, but instead leaves the app by swiping up from the bottom, the app may
be closed by the system without saving the data. You’ll find out how to overcome this
by checking scene phases in Chapter 19, “Saving Files”.
➤ Build and run the app and tap the History button to test that your deletion works.
To save the data permanently, make sure you close the History view in the app before
exiting the app.
Date deletion
281
SwiftUI Apprentice Chapter 10: Working With Datasets
Another way of showing hierarchical data is to use a disclosure group. This view
collapses its contents and you expand them using a disclosure indicator.
Here you embed your Text view in a DisclosureGroup. The disclosure group, when
expanded will reveal exerciseView(day:).
➤ Live Preview the view and tap each date to see your accumulated exercises for that
date.
Disclosure groups
282
SwiftUI Apprentice Chapter 10: Working With Datasets
the same type as Parent, you can take advantage of SwiftUI’s automatic
disclosure by initializing the list with the format List(parents, children:
\.children) { parent in ... }. The list will automatically list all parents,
with disclosure groups for the children.
Disappearing date
On each delete action, your List is deleting the top level of data, which in your case
is the date.
.deleteDisabled(true)
Now, you will still be able to delete the date with all the exercises, but you won’t be
able to delete a single exercise
283
SwiftUI Apprentice Chapter 10: Working With Datasets
Note: You can of course do anything with your data in Swift. If you had the
requirement of deleting a single exercise, you might set up your data
differently, so that the top level of a list would be by exercise, rather than by
date. Alternatively, instead of using the built in editActions of a list, you can
use the onDelete(perform:) modifier for deletion and write the deletion
code yourself.
EditButton()
Edit button
➤ In Live Preview, tap the Edit button to go into edit mode:
Edit mode
When you’ve finished deleting items, tap Done to return to the list.
284
SwiftUI Apprentice Chapter 10: Working With Datasets
You can now delete out-dated information, but how about adding in those exercises
that you perform away from your iPhone? This isn’t as easy as adding list deletion.
You’ll create an Add button that loads a calendar view from which you can select a
date. You’ll set up a button for each exercise and each time you tap a button, your
app will add an exercise to that date.
Button {
addMode = true
} label: {
Image(systemName: "plus")
}
.padding(.trailing)
Here you create the add button, which sets addMode to true.
Add button
In the History Views group, create a new SwiftUI View file called
AddHistoryView.swift. This is where you’ll create the calendar view.
285
SwiftUI Apprentice Chapter 10: Working With Datasets
AddHistoryView(addMode: .constant(true))
You’ll dismiss the calendar view from AddHistoryView, which entails changing
addMode. You’ll also need to update the exercise date.
Apple provides a date picker that you can configure in various ways. You can
customize the style and control which date components you show.
VStack {
DatePicker(
// 1
"Choose Date",
// 2
selection: $exerciseDate,
// 3
in: ...Date(),
// 4
displayedComponents: .date)
// 5
.datePickerStyle(.graphical)
}
.padding()
1. The label of the control. This may not display, depending on the style of the date
picker.
3. An optional closed range. In this case, you don’t want to let the user select a
future date, so you constrain the date range up to today.
286
SwiftUI Apprentice Chapter 10: Working With Datasets
5. The style of the picker. This can be a wheel, a compact drop-down or, in this case,
a full calendar month view.
DatePicker
➤ Add this as the first item at the top of the VStack:
ZStack {
Text("Add Exercise")
.font(.title)
Button("Done") {
addMode = false
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
You add the text heading for the view and the button to dismiss the view. By giving
the button an infinitely wide frame with trailing alignment, the text is centered, and
the button is in the correct position
if addMode {
AddHistoryView(addMode: $addMode)
}
287
SwiftUI Apprentice Chapter 10: Working With Datasets
➤ Try it out in Live Preview. Tap the + button to see the calendar and tap Done to
make the DatePicker disappear.
AddExerciseView
To navigate through the calendar, tap the forward and back arrows. To change the
month and year via a wheel picker, tap the disclosure indicator next to the month/
year. Tap the disclosure indicator again when you’ve selected the month and year.
Group {
if addMode {
Text("History")
288
SwiftUI Apprentice Chapter 10: Working With Datasets
.font(.title)
} else {
headerView
}
}
Now when you’re in add mode, the buttons in the navigation bar disappear. You
embed the conditional in a group to keep the same padding on both views.
Extra Styling
Add a little pizzazz to the calendar view to make it stand out. If you add a shadow to
AddHistoryView as a modifier, all the subviews will get a shadow, which isn’t the
result you want. Instead, you’ll add a background color to the view, and add a shadow
to that.
.background(Color.primary.colorInvert()
.shadow(color: .primary.opacity(0.5), radius: 7))
Here you change the date picker’s background color to the system’s primary color,
and invert it. If the system is in Light Mode, the primary color is black. When you
invert black, you get white. This matches the original color of the date picker. You
add to the background view a primary colored drop shadow with a 50% opacity.
289
SwiftUI Apprentice Chapter 10: Working With Datasets
Here you create a view that shows a button for each exercise, using the embossed
button style from the previous chapter.
ButtonsView(date: $exerciseDate)
You show the buttons and pass the currently selected date to ButtonsView
290
SwiftUI Apprentice Chapter 10: Working With Datasets
You’ll scale the button up temporarily, just while the user is tapping the button.
When the user is pressing the button, the button will scale up to the supplied value.
At all other times, the button’s scale won’t change.
.buttonStyle(EmbossedButtonStyle(buttonScale: 1.5))
The buttons will now scale up when you tap them, giving you feedback on your
action.
291
SwiftUI Apprentice Chapter 10: Working With Datasets
➤ Open HistoryStore.swift.
addDoneExercise(_:) will add or insert exercises. However, as you can now insert
historical dates, you’ll need a new method that inserts the date in the correct
position in the array.
1. You find the first index in the exerciseDays array where the date is less than or
equal to the passed-in date. The where part is a comparison closure that returns
true when the criterion is matched. The index of the first true comparison is then
passed back to index. Here, the conditional will fail if the passed-in date is
earlier than the array dates. You want to compare the dates on a daily basis, so
you use yearMonthDay from DateExtension.swift, to exclude the time.
2. If you find a date in the array that’s the same as the passed-in date, you append
the exercise name to the already existing array element.
292
SwiftUI Apprentice Chapter 10: Working With Datasets
3. If the date doesn’t already exist in the array, then insert it at the appropriate
position.
4. If the date is earlier than all the dates in the array, or the array is empty, then
append the date to the array.
You call the new method with the currently selected date and the name on the
tapped button.
.environmentObject(HistoryStore(preview: true))
➤ Open HistoryView.swift and try out adding new exercises in Live Preview. Then,
try your app in Simulator to make sure that it all gets saved correctly there too.
293
SwiftUI Apprentice Chapter 10: Working With Datasets
Charts
Skills you’ll learn in this section: Bar charts; organizing data for charts; line
charts; stacked charts
Using your history data, Swift Charts give you an opportunity to graphically
summarize how you’re performing. You can show which exercise you perform the
most, how often you exercise or how many exercises you perform per day or any
selected time period. Discover trends so you can analyze why you sometimes have
periods where you’re less enthusiastic about exercising. With just a few lines of code,
you can draw beautiful charts.
A chart consists of marks which represent the data. These marks could be points,
lines, areas or rectangular bars. The data is categorical, meaning that it can be
separated out into categories, or in this case, exercises.
Charts have two axes. One axis plots the individual categories, and the other axis
plots the numerical data associated with the categories.
Bar Charts
Bar charts present data using rectangles of different heights.
➤ In the History Views group, create a new SwiftUI View file called
BarChartDayView.swift, and add this code to the top of the file:
import Charts
294
SwiftUI Apprentice Chapter 10: Working With Datasets
BarMark(
x: .value("Name", "Squat"),
y: .value("Count", 2))
}
}
}
2. Inside the chart, determine what sort of mark to use. This chart will be a bar
chart, but it could also be an area, line or point chart.
3. On the x-axis, you create a plottable value with a label and a string value.
4. Similarly, on the y-axis, you create a plottable value with a label and an integer
value.
5. Repeat the marks for each piece of data you want to chart.
Live Preview shows the bar chart with the values that you created.
295
SwiftUI Apprentice Chapter 10: Working With Datasets
Here you load up the history store with the preview data and pass the first day to the
chart.
Chart {
ForEach(Exercise.names, id: \.self) { name in
BarMark(
x: .value(name, name),
y: .value("Total Count", day.countExercise(exercise:
name)))
.foregroundStyle(Color("history-bar"))
}
RuleMark(y: .value("Exercise", 1))
.foregroundStyle(.red)
}
.padding()
296
SwiftUI Apprentice Chapter 10: Working With Datasets
foregroundStyle allows you to change the colors of the chart. history-bar is a color
defined in Assets.xcassets.
This chart is ready for use. You can substitute it for your accumulated exercises in
the history list.
BarChartDayView(day: day)
297
SwiftUI Apprentice Chapter 10: Working With Datasets
Daily chart
This chart shows you individual exercises by day.
➤ In the History Views group, create a new SwiftUI View file called
BarChartWeekView.swift, and replace the code with:
import SwiftUI
import Charts
298
SwiftUI Apprentice Chapter 10: Working With Datasets
.environmentObject(HistoryStore(preview: true))
}
}
Chart(history.exerciseDays.prefix(7)) { day in
BarMark(
x: .value("Date", day.date.dayName),
y: .value("Total Count", day.exercises.count))
}
1. When you don’t need a separate ForEach, you can initialize Chart with the chart
data. You use the first seven elements of exerciseDays. Seven is the maximum
value, so if there aren’t seven elements in the array, only the available elements
are used.
2. For each of the days, you combine all the exercises into one bar.
299
SwiftUI Apprentice Chapter 10: Working With Datasets
By choosing a unit, the missing day now shows up, and the chart presents the data in
standard week-date format with the latest date at the trailing edge.
A daily chart
Because the preview data only contains four days’ worth of data, you don’t get the
full seven days. At times like this, you’ll have to massage your data into a format that
works with your desired chart.
.onAppear {
// 1
let firstDate = history.exerciseDays.first?.date ?? Date()
// 2
let dates = firstDate.previousSevenDays
// 3
weekData = dates.map { date in
history.exerciseDays.first(
where: { $0.date.isSameDay(as: date) })
?? ExerciseDay(date: date)
300
SwiftUI Apprentice Chapter 10: Working With Datasets
}
}
Here you create an array of seven dates. By iterating through these dates, you find
out whether you performed any exercises on that date. If you have, you use that data,
otherwise you create an empty daily record for that date.
1. Find out the first date in history. If there isn’t one, use the current date.
3. Iterate through the array of dates and for each date, locate the first entry for that
date. If there isn’t one, create a new blank ExerciseDay.
Chart(weekData) { day in
A seven-day chart
301
SwiftUI Apprentice Chapter 10: Working With Datasets
Line Charts
It’s easy to replace this bar chart with a line chart.
LineMark
.symbol(.circle)
.interpolationMethod(.catmullRom)
At each data point, it draws a circle. Check the code completion in Xcode to see what
other symbols you can use. A Catmull-Rom spline interpolates the points along a
curve, making the chart smooth instead of linear.
302
SwiftUI Apprentice Chapter 10: Working With Datasets
A line chart
This is an area chart with a point chart layered on top of it. The area chart has a
gradient foreground style, and the point chart has a purple foreground style.
303
SwiftUI Apprentice Chapter 10: Working With Datasets
Chart(weekData) { day in
BarMark(
x: .value("Date", day.date, unit: .day),
y: .value("Total Count", day.exercises.count))
}
With this bar chart, your exercises are all counted together. This doesn’t help if you
want to compare your burpees to your squats. You can split out your exercises using
similar code to your list when you accumulated the exercise.
Chart(weekData) { day in
ForEach(Exercise.names, id: \.self) { name in
BarMark(
x: .value("Date", day.date, unit: .day),
y: .value("Total Count", day.countExercise(exercise:
name)))
}
}
For each day, you iterate through all the four exercise names. Exercise.names is a
property in Exercise.swift. You accumulate the current exercises into the bar mark.
The result of this chart is currently the same as the previous chart, but you’re now
able to split the bars into different colors.
Instead of using a color to determine the style, you separate out the bar by exercise.
304
SwiftUI Apprentice Chapter 10: Working With Datasets
In Live Preview, the chart displays the bars with colors marking the relative number
of exercises. Beneath the chart, the legend explains what each color represents.
.chartForegroundStyleScale([
"Burpee": Color("chart-burpee"),
"Squat": Color("chart-squat"),
"Step Up": Color("chart-step-up"),
"Sun Salute": Color("chart-sun-salute")
])
305
SwiftUI Apprentice Chapter 10: Working With Datasets
The preview updates the legend and the chart with your new colors.
Custom colors
Privacy
Skills you’ll learn in this section: User privacy
Collection and analysis of data over time can be very useful. You can track weight
trends with health data or wealth with financial data. Like Google and Apple, you can
decide how to collect users’ data, what to do with it and how present it back to your
users. If your app attracts enough users, you might be able to create machine
learning datasets and use those datasets for future apps.
Fortunately, Apple enforces user privacy. Apple’s article Protecting the User’s Privacy
(https://apple.co/3P0XSGy) tells you how to access and protect user data. Remember
that your users trust you!
306
SwiftUI Apprentice Chapter 10: Working With Datasets
Challenge
As you can see, it’s easy to design new charts. Your challenge is to incorporate new
charts into your app.
In WelcomeView.swift, add a new button beside the History button called Reports.
When you tap this button, you should show a modal view with a Toggle to show a
bar or a line chart for the last week. Integrate your existing BarChartWeekView in
the modal view.
Your challenge
As always, you can examine the project in your challenge folder for this chapter. In
addition, the challenge project has a styled modal timer view that you can examine.
307
SwiftUI Apprentice Chapter 10: Working With Datasets
Key Points
• A Set is a collection of data where each element is unique. Both Set and Array
have initializers to create one from the other.
• To show groups of data which you can collapse and expand, use a
DisclosureGroup.
• Swift Charts is a framework that displays your data in gorgeous charts with
minimal code.
• As well as bar charts, you can just as easily create line, point and area charts.
• You can layer charts on top of each other, such as layering points on top of lines.
• When you have groups of data, you can stack the data in a single bar. Charts will
automatically create different colors for the groups.
308
11 Chapter 11: Managing Data
With Property Wrappers
By Audrey Tam
In your SwiftUI app, every data value or object that can change needs a single source
of truth and a mechanism to enable views to change or observe it. SwiftUI’s property
wrappers enable you to declare how each view interacts with mutable data.
In this chapter, you’ll review how you managed data values and objects in HIITFit
with @State, @Binding, @Environment, ObservableObject, @StateObject and
@EnvironmentObject. And, you’ll build a simple app that lets you focus on how to
use these property wrappers. You’ll also learn about TextField, the environment
modifier and property wrappers @ObservedObject and @FocusState.
To help answer the question “struct or class?”, you’ll see why HistoryStore should
be a class, not a structure and learn about the natural architecture for SwiftUI apps:
the Model-View-ViewModel pattern.
309
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Getting Started
➤ Open the TIL project in the starter folder. The project name “TIL” is the acronym
for “Today I Learned”. Or, you can think of it as “Things I Learned”. Here’s how the
app should work: The user taps the + button to add acronyms like “YOLO” and
“BTW”, and the main screen displays these.
TIL in action
This app embeds a VStack in a NavigationStack, which gives you the navigation
bar where you display the title and a toolbar where you display the + button. You’ll
learn more about NavigationStack in Section 3.
This project has a ThingStore, which is like HistoryStore in HIITFit. This app is
much simpler than HIITFit, so you can focus on how you manage the data.
310
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
If you did the challenge in that chapter, you also managed HistoryStore with
@State and @Binding.
ThingStore has the property things, which is an array of String values. Like the
HistoryStore in the first version of HIITFit, it’s a structure.
In this chapter, you’ll first manage changes to the ThingStore structure using
@State and @Binding, then convert it to an ObservableObject class and manage
changes with @StateObject and @ObservedObject:
311
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
• Data model objects, often collections of objects that model the app’s data, like
daily logs of completed exercises.
Property Wrappers
Property wrappers encapsulate a value or object in a structure with two properties:
Swift syntax lets you write just the name of the property, like showHistory, instead
of showHistory.wrappedValue. And, its binding is $showHistory instead of
showHistory.projectedValue.
SwiftUI provides tools — mostly property wrappers — to create and modify the single
source of truth for values and for objects:
• User interface values: Use @State and @Binding for values like showHistory that
affect the view’s appearance. The underlying type must be a value type like Bool,
Int, String or Exercise. Use @State to create a source of truth in one view, then
pass a @Binding to this property to subviews. A view can access built-in
@Environment values as @Environment properties or with the
environment(_:_:) view modifier.
312
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
• Data model objects: For objects like HistoryStore that model your app’s data, use
either @StateObject with @ObservedObject or environmentObject(_:) with
@EnvironmentObject. The underlying object type must be a reference type — a
class — that conforms to ObservableObject, and it should publish at least one
value. Then, either use @StateObject and @ObservedObject or declare an
@EnvironmentObject with the same type as the environment object created by
the environmentObject(_:) view modifier.
While prototyping your app, you can model your data with structures and use @State
and @Binding. When you’ve worked out how data needs to flow through your app,
you can refactor your app to accommodate data types that need to conform to
ObservableObject.
A view is a structure, so you can’t change a property value unless you wrap it as a
@State or @Binding property.
The view that owns a @State property is responsible for initializing it. The @State
property wrapper creates persistent storage for the value outside the view structure
and preserves its value when the view redraws itself. This means initialization
happens exactly once.
313
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
You already got lots of practice with @State and @Binding in Chapter 5, “Moving
Data Between Views” and Chapter 6, “Observing Objects”:
In the challenge for Chapter 6, “Observing Objects”, you used @State and @Binding
to manage changes to HistoryStore. That was just an exercise to demonstrate it’s
possible, and it’s one approach you can take to prototyping. For most apps, your final
data model will involve ObservableObject classes.
Starter TIL
TIL uses a Boolean flag, showAddThing, to show or hide AddThingView. It’s a @State
property because its value changes when you tap the +, and ContentView owns it.
314
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
➤ In ContentView.swift, add this code at the top of the VStack, before the ForEach
line:
if myThings.things.isEmpty {
Text("Add acronyms you learn")
.foregroundColor(.gray)
}
315
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
You give your users a hint of what they can do with your app. The text is grayed out
so they know it’s just a placeholder until they add their own data.
➤ You’ll also add a text field to get the user’s input, but for now, just to have
something happen when you tap Done, add this line to the button action, before you
dismiss this sheet:
someThings.things.append("FOMO")
AddThingView(someThings: .constant(ThingStore()))
AddThingView(someThings: $myThings)
316
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
➤ First, pin the preview of ContentView so it’s there when you’re ready to test your
TextField.
Using a TextField
Many UI controls work by binding a parameter to a @State property of the view:
These include Slider, Toggle, Picker and TextField.
To get user input via a TextField, you need a mutable String property to store the
user’s input.
It’s a @State property because it must persist when the view redraws itself.
AddThingView owns this property, so it’s responsible for initializing thing. You
initialize it to the empty string.
➤ Now, add your TextField in the VStack, above the Done button:
317
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
1. The label “Thing I Learned” is the placeholder text. It appears grayed out in the
TextField as a hint to the user. You pass a binding to thing so TextField can
set this value to what the user types.
3. You add padding so there’s some space from the top of the view and also to the
button.
if !thing.isEmpty {
someThings.things.append(thing)
}
Instead of "FOMO", you append the user’s text input to your things array after
checking it’s not the empty string.
➤ In the ContentView Live Preview, tap +. Type an acronym like YOLO in the text
field. It automatically capitalizes the first letter, but you must hold down the Shift
key for the rest of the letters. Tap Done:
TextField input
ContentView displays your new acronym.
Improving the UX
You can improve your users’ experience by adjusting how the text field handles their
input. Acronyms should appear as all caps, but it’s easy to forget to hold down the
Shift key. Sometimes the app auto-corrects your acronym: FTW to FEW or FOMO to
DINO. And focus! Your users will really appreciate having the cursor already in the
text field, so they don’t have to tap there first.
.autocapitalization(.allCharacters)
.disableAutocorrection(true)
318
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
This is like the @State Boolean properties you use to show or hide modal sheets or
alerts, but you use a value wrapped by @FocusState to place the cursor in an
associated view.
.focused($thingIsFocused)
.onAppear { thingIsFocused = true }
Note: The value wrapped by @FocusState doesn’t have to be Bool. You can
use any value type, including enumerations, with focused(_:equals:).
When TextField appears, you set thingIsFocused to true, so the focused modifier
places the cursor in TextField.
➤ In Live Preview, tap +, type icymi (don’t touch the Shift key!) then tap Done:
Auto-focus, auto-cap
The cursor is already in the text field and, even if you type in lower case, your input
is all caps!
319
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
320
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Accessibility, Font, Weight, Line Limit, Padding and Frame Size are Inherited. Font
Color would also be inherited if the foregroundColor modifier hadn’t set it to gray.
A view can override an inherited environment value. It’s common to set a default
font for a stack then override it for the text in a subview of the stack. You did this in
Chapter 3, “Prototyping the Main View”, when you made the first page number larger
than the others:
HStack {
Image(systemName: "1.circle")
.font(.largeTitle)
Image(systemName: "2.circle")
Image(systemName: "3.circle")
Image(systemName: "4.circle")
}
.font(.title2)
.environment(\.textCase, .uppercase)
You set uppercase as the default value of textCase for ContentView and all its
subviews.
321
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Automagic uppercase
Your strings are automatically converted to uppercase.
The environment value applies to all text in your app, which looks a little strange. No
problem — you can override it.
.environment(\.textCase, nil)
You set the value to nil, so none of the text displayed by this VStack is converted to
uppercase.
➤ In the ContentView Live Preview, tap +, type icymi then tap Done:
You can use custom value data types like struct or enum to model your app’s data.
And, you can use @State and @Binding to manage updates to these values, as you
did earlier in this chapter.
322
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Most apps also use classes to model data. SwiftUI provides a different mechanism to
manage changes to class objects: ObservableObject, @StateObject,
@ObservedObject and @EnvironmentObject. To practice using @ObservedObject,
you’ll refactor TIL to use @StateObject and @ObservedObject to update
ThingStore, which conforms to ObservableObject. You’ll see a lot of similarities,
and a few differences, to using @State and @Binding.
Note: You can wrap a class object as a @State property, but its “value” is its
address in memory, so dependent views will redraw themselves only when its
address changes — for example, when the app reinitializes it.
A class is more suitable when you need shared mutable state like a HistoryStore or
ThingStore. A structure is more suitable when you need multiple independent states
like ExerciseDay structures.
For a class object, change is normal. A class object expects its properties to change.
For a structure instance, change is exceptional. A structure instance requires
advance notice that a method might change a property.
A class object expects to be shared, and any reference can be used to change its
properties. A structure instance lets itself be copied, but its copies change
independently of it and of each other.
You’ll find out more about classes and structures in Chapter 15, “Structures, Classes
& Protocols”.
323
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Just like you did with HistoryStore, you make ThingStore a class instead of a
structure, then make it conform to ObservableObject. You mark this class final to
tell the compiler it doesn’t have to check for any subclasses overriding properties or
methods.
Like HistoryStore, ThingStore publishes its array of data. A view subscribes to this
publisher by declaring it as a @StateObject, @ObservedObject or
@EnvironmentObject. Any change to things notifies subscriber views to redraw
themselves.
If a view uses an @EnvironmentObject, you must create the model object by calling
the environmentObject(_:) modifier on an ancestor view.
324
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
You first created the HistoryStore object in ContentView, applying the modifier to
the TabView:
TabView(selection: $selectedTab) {
...
}
.environmentObject(HistoryStore())
Then, in Chapter 8, “Saving History Data”, you elevated its initialization up one level
to HIITFitApp and declared it as a @StateObject.
ThingStore is now a class, not a structure, so you can’t use the @State property
wrapper. Instead, you use @StateObject. The @StateObject property wrapper
ensures myThings is instantiated only once. It persists when ContentView redraws
itself.
AddThingView(someThings: myThings)
325
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
AddThingView(someThings: ThingStore())
TIL in action
No surprise: The app still works the same as before.
Model-View-Controller
You may be familiar with Model-View-Controller (MVC) architecture for apps in
other settings, like web apps. Your data model knows nothing about how your app
presents it to users. The view doesn’t own the data, and the controller mediates
between the model and the view.
SwiftUI apps don’t need a controller. Views subscribe to observable objects and
update themselves when observed values change.
326
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Model-View
When a SwiftUI app’s views display a collection of objects or values, its model
manages the data collection. In simple apps like HIITFit and TIL, this is the model’s
only job. So the model’s name often includes the word “Store”.
MV in HIITFit
HIITFit’s model, HistoryStore, saves and loads the user’s exercise history. The
model consists of the Exercise and ExerciseDay structures. HistoryStore
publishes the exerciseDays array. ExerciseView and HistoryView subscribe to
HistoryStore.
327
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
MV in TIL
TIL’s model, ThingStore, saves the user’s array of acronyms. An acronym is simply a
String and the model publishes the things array. ContentView and AddThingView
subscribe to ThingStore. The AddThingView’s tap-Done event updates the things
array, which changes the state of ContentView.
Note: User actions can initiate network activity that updates the model. In the
Section 3 app, TheMet, the user enters a query term, causing the model to
download data from The Metropolitan Museum of Art, New York (https://
www.metmuseum.org) and decode the data into its published array of Object
values.
First, decide whether you’re managing the state of a value or the state of an object.
Values are mainly used to describe the state of your app’s user interface. If you can
model your app’s data with value data types, you’re in luck because you have a lot
more property wrapper options for working with values.
328
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
But at some level, most apps need reference types to model their data, often to add
or remove items from a collection.
Wrapping Values
@State and @Binding are the workhorses of value property wrappers. A view owns
the value if it doesn’t receive it from any parent views. In this case, it’s a @State
property — the single source of truth. When a view is first created, it initializes its
@State properties. When a @State value changes, the view redraws itself, resetting
everything except its @State properties.
The owning view can pass a @State value to a subview as an ordinary read-only
value or as a read-write @Binding.
When you’re prototyping an app and trying out a subview, you might write it as a
stand-alone view with only @State properties. Later, when you fit it into your app,
you just change @State to @Binding for values that come from a parent view.
Your app can access the built-in @Environment values. An environment value
persists within the subtree of the view you attach it to. Often, this is simply a
container like VStack, where you use an environment value to set a default like font
size.
329
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Note: You can also define your own custom environment value — for example,
to expose a view’s property to ancestor views. This is beyond the scope of this
book, but check out SwiftUI by Tutorials, Chapter 9, “State & Data Flow – Part
II” (https://bit.ly/3IffPOt).
Wrapping Objects
When your app needs to change and respond to changes in a reference type, you
create a class that conforms to ObservableObject and publishes the appropriate
properties. In this case, you use @StateObject and @ObservedObject in much the
same way as @State and @Binding for values. You instantiate your publisher class in
a view as a @StateObject then pass it to subviews as an @ObservedObject. When
the owning view redraws itself, it doesn’t reset its @StateObject properties.
If your app’s views need more flexible access to the object, you can lift it into the
environment of a view’s subtree, still as a @StateObject. You must instantiate it
here. Your app will crash if you forget to create it. Then you use
the .environmentObject(_:) modifier to attach it to a view. Any view in the view’s
subtree can subscribe to the publisher object by declaring an @EnvironmentObject of
that type.
To make an environment object available to every view in your app, attach it to the
root view when the App creates its WindowGroup.
330
SwiftUI Apprentice Chapter 11: Managing Data With Property Wrappers
Key Points
• Every data value or object that can change needs a single source of truth and a
mechanism to enable views to update it.
• When prototyping your app, you can use @State and @Binding with structures
that model your app’s data. When you’ve worked out how data needs to flow
through your app, refactor your app to accommodate data types that need to
conform to ObservableObject.
• A commonly used architecture for SwiftUI apps is the Model-View pattern, where
the model is an ObservableObject. Changes to the model’s published properties
cause updates to the view.
331
12 Chapter 12: Apple App
Development Ecosystem
By Audrey Tam
Here’s one more overview chapter before you move on to build the other two apps in
this book.
While building HIITFit, you learned a lot about Xcode, Swift and SwiftUI at a detailed
level. In the previous chapter, you got a kind of “balcony view” of how the various
property wrappers help you manage the state of your app’s data. This chapter
provides a bird’s eye view of the whole Apple app development ecosystem,
demystifying many of the terms you might hear when iOS developers get together
for a chat. You’ll start to build your own mental model of how all the parts fit
together, creating your own framework for all the new things that Apple adds every
year.
332
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Apple announced SwiftUI at its World Wide Developers Conference in June 2019.
SwiftUI builds on the Swift programming language, which Apple announced in June
2014. SwiftUI is a Domain Specific Language (DSL), written in Swift using these
new Swift features:
• Opaque result types, like some View to avoid explicitly writing out the view
hierarchy.
Swift creates faster, safer apps than Objective-C and is more protocol-oriented than
object-oriented. Chapter 15, “Structures, Classes & Protocols”, explains the
difference between class inheritance and protocols.
Objective-C entered Apple history when Apple bought NeXT in 1997, which also
brought Steve Jobs back to Apple. Jobs had resigned from Apple in 1985 after losing a
boardroom battle with CEO John Sculley over the future of the Macintosh computer.
Jobs, with five other former Apple executives, then founded NeXT Computers.
333
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
The NeXTSTEP operating system, written in Objective-C, formed the basis of Mac
OS X, released in 2001. Apple provided the Cocoa API for developers to create apps
for OS X. Cocoa consists of three frameworks reflecting the Model-View-Controller
principles: Core Data, AppKit and Foundation. The “NS” prefix in AppKit and
Foundation acknowledges their NeXTSTEP heritage.
Apple announced the iPhone in 2007 and the iPhone SDK (Software Development
Kit) in 2008. This included Cocoa Touch, with UIKit replacing AppKit. Now called
the iOS SDK, it helps you create apps that appear and behave the way users expect.
Fun Facts
• The first World Wide Web server was a NeXT Computer, and id Software developed
the video games Doom and Quake on machines running the NeXT operating
system NeXTSTEP. In 1996, NeXT Software, Inc. released WebObjects, a
framework for Web application development. Apple used WebObjects to build and
run the Apple Store, MobileMe services and the iTunes Store.
• Cocoa != Java for kids: Before Jobs returned to Apple, the Apple Advanced
Technology Group created KidSim, an app to teach kids to program. KidSim
programs were embedded in web pages to run, so they renamed and trademarked
the app as Cocoa — “Java for kids”. The Cocoa program was one of the many axed
in 1997, and Apple reused the name for the OS X API to avoid the delay of
registering a new trademark.
• While developing the iPhone, Steve Jobs didn’t want non-Apple developers to
build native iPhone apps. They were supposed to be content making web
applications for Safari. This stance changed in response to a backlash from
developers, and the iPhone SDK was released on March 6, 2008.
334
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Note: Like a lot of the content on this site, this episode is aimed at people who
want to work for iOS app development companies. If this isn’t you, skip to the
next section.
Three reasons why there are still more developers using UIKit than SwiftUI:
1. SwiftUI only works on iOS 13 or later. Some companies still need to support iOS
12 or earlier, so they can’t switch to SwiftUI quite yet.
2. SwiftUI is still not as mature as UIKit. Apple released UIKit in 2008, and it built
on macOS AppKit, which came from NeXTSTEP, so there was a lot of time to get
things right. SwiftUI still has missing features or rough edges, so some companies
want to give SwiftUI a little more time to mature.
3. Many companies have already written their apps using UIKit, and it would simply
be too much work at this point to rewrite the entire thing in SwiftUI, so a lot of
that old UIKit legacy code will remain.
SwiftUI or UIKit?
335
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
It’s not all or nothing: It’s possible to make a certain part of your app with SwiftUI
and the rest with UIKit. As companies begin to transition from UIKit to SwiftUI, we
expect to see many codebases with a mixture of both SwiftUI and UIKit code in the
years ahead.
Thanks, Ray! That’s the perfect segue into the next section…
Developers can still create Objective-C apps without Swift and Swift apps without
SwiftUI. UIKit has many more features than SwiftUI and provides much more control
over the appearance and operation of user interface elements.
But no FOMO (fear of missing out)! You can use UIKit views in your SwiftUI apps:
336
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
You can also create and manage UIViewController objects: You’ll use
SFSafariViewController in Section 3, “Your Third App”.
Apple Developer
Despite Steve Jobs’ initial intentions, Apple would like everyone to be an Apple
developer. Your needs and interests might be shared by a few other people or by a lot
of other people. But maybe not by professional iOS developers. If you create an app
you need or want, it becomes available to those other people too. Even better if your
app uses some technology that only works on the newest Apple gadgets, so they have
to upgrade to use your app. ;]
337
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
WWDC
Every June, Apple holds the 5-day World Wide Developers Conference — WWDC,
often pronounced dub-dub-dee-cee or shortened to dub-dub. The keynote on day 1
shows off all the features planned for the new versions of iOS, macOS and all the
other OSes. These launch later in the year, around September or October.
For iOS developers, the more important presentation is the Platforms State of the
Union later on day 1, where you get your first look at the APIs for adding these new
features to your apps, as well as improvements to developer tools like Xcode. During
the rest of the week, you can watch presentations that introduce and dive deeper
into the new features.
A word of caution: The WWDC presenters use a special in-house version of Xcode.
It’s different from the Xcode beta you can download, so not everything you see in the
presentations actually works in beta 1. Or beta 2. Or ever. Details of the API often
change by the time Apple releases the final version, and some promised features get
postponed or quietly disappear.
338
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Platforms
Using SwiftUI to build new iOS apps makes it easier to create similar apps on Apple’s
other platforms: macOS, watchOS and tvOS. It’s not that your iOS app will “just
work” on another platform. It probably won’t.
You can use many SwiftUI views on other platforms, but how they look or function
might be a little different. And other platforms have views that don’t exist for iOS.
Also, some features of your iOS app won’t make sense on a more stationary platform
like tvOS or on a smaller screen like watchOS.
But, the way you assemble a SwiftUI app remains the same, no matter which
platform you’re targeting. Apple expresses it this way: Learn once, apply anywhere.
Mac Catalyst is Apple’s program to make it easier to create a native Mac app from
an iPad app. You turn on Mac Catalyst in the iPad app’s project settings, then modify
the user interface to be more Mac-like. Some iPad UI elements aren’t quite right for
the Mac user experience, and some iPad frameworks just aren’t available in macOS.
Your code controls what to include using this compiler directive:
#if targetEnvironment(macCatalyst)
...
#endif
Check out Apple’s tutorial for Mac Catalyst (https://apple.co/3qPaen7). For an even
more in-depth look, browse our book Catalyst by Tutorials (https://bit.ly/32ppGwM).
Note: What about Apple Silicon? It’s Apple’s program to design and
manufacture its own Mac processors. Since its launch in 1984, the Mac has
used Motorola 68000, PowerPC and Intel CPU chips. The Apple M1 Chip
integrates Apple’s new CPU with its new GPU, neural engine and more. You
can install Rosetta 2 on an Apple Silicon Mac to run apps written for Intel
Macs.
339
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Frameworks
The SDK has a lot of frameworks, and Apple adds new ones every year. The ones
every app needs are modernized versions of the original Cocoa:
Note: Core Data is a massive topic and, if you’d like to learn more, we have a
book, Core Data by Tutorials (https://bit.ly/39lo2k3) and video courses
Beginning Core Data (https://bit.ly/2OGjuwG) and Intermediate Core Data
(https://bit.ly/3bE2H6z) to help you on your way.
340
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Charts in HistoryView
In Cards, PhotosUI with PhotosPicker:
341
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
These next two frameworks are important, but beyond the scope of this book:
• Combine: This framework is a major change to the way iOS apps handle
concurrency. See Combine: Asynchronous Programming in Swift (https://bit.ly/
3KuGurx) and the video course Reactive Programming in iOS with Combine
(https://bit.ly/3mnu1Oq).
WidgetKit to add widgets, like this one from our video course SwiftUI Charts for
WidgetKit (https://bit.ly/3EIRrnV):
342
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
MapKit to add maps, user location, routing or overlay views to your apps:
ARKit for augmented reality: See Apple Augmented Reality by Tutorials (https://
bit.ly/3OpnzQz). Be prepared for Apple’s mixed reality headset (https://bit.ly/
3XibhgR), predicted for Real Soon Now™. ;]
343
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
344
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Capabilities
Many of the frameworks are for adding special features to your apps. Apple calls
these capabilities.
➤ To see a list of capabilities, open one of your Xcode projects or create a new one.
On the project page, select a target, click Signing & Capabilities, then click +
Capability:
Capabilities
If you’re not in the Apple Developer Program, you can add only some of these
capabilities to your apps. They’re listed in the third column of Supported capabilities
(iOS) (https://apple.co/3rOhlNW).
345
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Developer Programs
So what are the three types of Developer?
Apple Developer:
• No annual fee.
• Access to App Store Connect: Distribute your beta apps to testers with
TestFlight; submit your apps to the App Store and access App Analytics.
• During virtual WWDC: You can request a lab appointment or post forum questions
to Apple engineers about WWDC content. When in-person WWDCs resume, you
can register for the ticket lottery.
What if you have apps in the App Store but you don’t renew your membership?
Here’s Apple’s answer:
Apple Enterprise Program is for companies that want to distribute apps only to
employees. The fee is US$299 or equivalent per year. Enterprise apps aren’t
submitted to the App Store so don’t have to comply with Apple’s requirements. But
there are a lot of legal requirements (https://apple.co/3coUHVU), and it’s probably
easier to just use TestFlight.
346
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
App Distribution
The actual procedure for getting your app into the App Store changes a little bit
every year. Apple’s documentation can be confusing.
Check out our book iOS App Distribution & Best Practices (https://bit.ly/3al3Hez) or
video course Publishing to the App Store (https://bit.ly/3tckW8Z).
DerivedData/Build
Xcode maintains a lot of files in ~/Library/Developer/Xcode. Of particular interest
is DerivedData, where Xcode creates a folder for every project you’ve ever created or
opened. This is where Xcode stores intermediate build results, indexes and logs.
The easiest way to locate your project’s derived data folder is with the Xcode menu
item Show Build Folder in Finder.
➤ Open one of your Xcode projects or create a new one. If it’s new, press Command-
B or refresh a preview to build it. Then select Product ▸ Show Build Folder in
Finder:
347
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
➤ In the Finder column view, scroll left to see your project’s folder in DerivedData:
➤ Select this folder, then view it as a list and open the Build folder:
348
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
349
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
DerivedData
The rest of your app’s folder in DerivedData stores data and indexes that Xcode uses
for search, Open Quickly and refactoring. Again, sometimes these get mixed up,
causing strange Xcode behavior. There’s no menu command to delete these files. You
just have to delete the whole derived data folder and let Xcode re-create it.
350
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Here are the escalating levels of intervention that most developers follow:
3. Restart Xcode.
4. Restart Mac.
Strange but true: Deleting earlier versions of Xcode can fix some weird
issues, like no color-coding in the editor or Command-/ not working.
rm -rf ~/Library/Developer/Xcode/DerivedData/*
Or, if you’re running Big Sur or later, you can open Trash and selectively erase the
folder. But, since Big Sur, macOS storage management provides a way to clear even
more space.
351
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
➤ In the Apple menu, select About this Mac and click More Info…. In the General
screen, scroll down to Storage, click Storage Settings…, then click the Developer
info icon. In the Developer window, select Xcode Caches, hold down the Command
key to select any Device Support items you don’t need, then click Delete…:
352
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Many search results will be Stack Overflow or Apple developer forum questions,
hopefully with answers.
The Kodeco team and members are a terrific resource, but there’s also a large
worldwide community of iOS developers. They’re almost universally friendly,
welcoming and generous with their time and expertise.
An easy path to join this community is to attend the monthly online events at iOS
Dev Happy Hour (https://www.iosdevhappyhour.com):
353
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Key Points
• SwiftUI is a Domain Specific Language built on Swift, a faster and safer
programming language than Objective-C.
• “SwiftUI vs. UIKit” is the most popular free episode at Kodeco and answers the big
question: Which should you learn?
• You can use UIKit views and view controllers in your SwiftUI apps.
• Apple provides a lot of resources to help you become a developer and stay up to
date: Documentation and human interface guidelines are available on the website
and in Xcode. Use the Apple Developer app to watch WWDC videos.
• The iOS Software Development Kit has a lot of frameworks, many for adding
special features (capabilities) to your apps.
• Members of the Apple Developer Program can add all the capabilities and also get
early access to beta operating systems and developer tools. And, only members can
participate fully in WWDC.
• Xcode stores intermediate build results, indexes and logs in your project’s derived
data folder. Sometimes you need to Clean Build Folder or delete the entire derived
data folder. To reclaim disk space, periodically delete the whole DerivedData
directory.
354
Section II: Your Second App:
Cards
Now that you’ve completed your first app, it’s time to apply your knowledge and
build a new app from scratch. In this section, you’ll build a photo collage app called
Cards and you’ll start from a blank template. Along the way, you’ll:
• Discover how Xcode and iOS manage app assets such as images and colors.
355
13 Chapter 13: Outlining a
Photo Collage App
By Caroline Begbie
Congratulations, you’ve written your first app! HIITFit uses standard iOS user
interaction with lists and swipeable page views. Now you’ll get your teeth into
something a bit more complex with custom gestures and custom views.
Photo collage apps are very popular, and in this section, you’ll build your own
collaging app in which you’ll create cards to share. You’ll be able to add images —
from your photos or from the internet — and add text and stickers too. This app will
be real-world with real-world problems to match.
In this chapter, you’ll take a look at a sketch outline of the app idea and create a view
hierarchy that will be the skeleton of your app.
At the end of Section 2, your finished app will look like this:
Final app
356
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Once you’ve decided that you have a hit on your hands, sketch your app out and
work out feasibility and where technical difficulties may lie.
Your photo collaging app will have a primary view — where you list all the cards —
and a detail view for the selected card — where you can add photos and text. This
might be the back-of-the-napkin sketch:
SwiftUI is great for this, because you can construct views and controls independently
using SwiftUI’s live preview. When you’re happy with how a view works, add it to
your app.
357
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Open Xcode and choose File ▸ New ▸ Project… and create a new project called
Cards using the iOS App template. If you need a refresher on how to create a new
SwiftUI project, you’ll find all the information in Chapter 1, “Checking Your Tools”.
➤ Click the run destination button and select iPhone 14 Pro. Build and run your
app using Command-R to make sure that everything works OK. Your iPhone 14 Pro
simulator should start and show ContentView’s “Hello, world!” text.
Initial view
You should take these steps every time you create a new app just in case something
in your environment has changed.
358
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
This view will show a scrolling thumbnail list of all the cards you create in your app.
Placeholder thumbnails
359
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
A ScrollView can be vertical or horizontal. The default, which you use here, is
vertical, but you can specify a horizontal axis with ScrollView(.horizontal).
➤ In live preview, scroll the list. When you scroll, you can see an ugly scroll bar by
the side of the cards.
In case you can’t see the canvas, you can enable it using the icon at the top right of
Xcode:
Show canvas
➤ Change ScrollView { to:
ScrollView(showsIndicators: false) {
As you add views, you’ll recognize that later on, some views will become more
complex. The RoundedRectangle is such a view. You’ve given it basic styling, but
you’ll probably want to style it a bit further down the line.
360
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
It’s much easier to refactor views early on, so you’ll create a new view for the
placeholder card now. You extracted a view in Chapter 3, “Prototyping the Main
View”, so this should be a refresher for you.
Xcode will copy RoundedRectangle() to a new View and rename the reference to
ExtractedView().
Extracted View
➤ Right click ExtractedView(), choose Refactor > Rename and rename
ExtractedView to CardThumbnail.
361
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Select the entire CardThumbnail structure and paste in the cut code.
➤ Open CardsListView.swift. Your list still looks the same, but it will be easier for
you to add colors and shadows to the thumbnails later.
This color will eventually come from the card data, but for the moment you’ll just
make the card yellow.
A yellow card
362
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
When you tap a card in the scrolling list in CardsListView, you want to show
SingleCardView. You can achieve this in several ways:
• Replace CardsListView in the view hierarchy. With this option, when you return
to the thumbnails after editing the card, you’d lose your current scrolling position
in the cards list.
• Present a full screen modal view. The view slides up from the bottom and covers
the whole screen. This is the option you’ll use here.
• By toggling a view state property, you can show SingleCardView in a new layer in
front of CardsListView. This is the best option if you need a different transition
to the modal’s slide from the bottom. It is, however, more complex to implement,
as you have to pass the view state property to other subviews.
.fullScreenCover(isPresented: $isPresented) {
SingleCardView()
}
363
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
.onTapGesture {
isPresented = true
}
Before tackling the button, set up your app to run in Simulator, so that if live
preview fails, you can still see your app.
CardsListView()
You call the view that will show the list of cards instead of ContentView.
➤ Build and run and make sure that your app works in Simulator, just as it does in
the preview.
You aren’t using ContentView.swift any more, but you can leave it in the project to
experiment with other SwiftUI layouts.
364
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
A Done button in SingleCardView will dismiss the full screen modal view. You can
set up buttons at the top and bottom of the screen using a navigation toolbar.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
You place a Done button at the top right of the screen. When the user taps this
button, SwiftUI dismisses the SingleCardView modal.
• principal: On iOS, the principal placement is in the center of the navigation bar.
365
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Preview SingleCardView.
No Done button
NavigationStack
Notice that the button doesn’t show up. This is because ToolbarItem(placement:)
is using navigationBarTrailing, so any item will only show up if the view is inside
a NavigationStack.
366
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Open CardsListView.swift and test your app so far. Tap a thumbnail to show the
yellow card and dismiss the card by tapping the Done button.
Each of these buttons will show a separate modal view. When you have discrete
values, such as these four destinations, you can use an enumeration to create your
set of values. Enumerations make your code easier to read and ensure that values are
restricted to those defined in the enumeration.
367
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Create a new Swift file called ToolbarSelection.swift and add this code:
enum ToolbarSelection {
case photoModal, frameModal, stickerModal, textModal
}
In this view, you’ll set up the four buttons at the bottom of the screen.
Each modal button will use this view, and you’ll style this to be more generic shortly.
Here you create an HStack containing a button that will change the modal state.
Because the text label for the button is a custom view, rather than a string, you use
the Button(action:label:) initializer. You’ll add more toolbar items in a moment
to this HStack.
368
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Stickers button
When you tap a button on the bottom bar, the button will update this property. Later
on, you’ll show the corresponding modal view.
➤ In body, locate .toolbar {. This is where you currently have the Done button.
➤ Add a new toolbar item inside toolbar(content:), under the previous toolbar
item:
ToolbarItem(placement: .bottomBar) {
BottomToolbar(modal: $currentModal)
}
369
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Here you add your new toolbar at the bottom of the screen.
Bottom toolbar
ToolbarButton is the view that displays the toolbar button. You’ll send in the modal
that the button is tied to and show the correct image for that button. You’ll get a
compile error until you fix BottomToolbar.
You already set up body to show an image and text for the Stickers button. You could
do a switch in body and show the appropriate image for all the modal options.
However, it’s more succinct to set a dictionary of all the possible options with the
text and image name. In case you need a refresher on dictionaries, you first used
them in Chapter 7, “Saving Settings”.
370
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Tuples
A tuple is a group of values. For example, you could initialize a tuple with three
elements like this:
It’s obviously good practice to name your types rather than using numbers to access
the data, which is why you defined your modalButton tuple with
(text:imageName:)
Using your dictionary, you access the text and image name and use those for the
button instead of the hard coded Stickers values.
To show all the buttons in BottomToolbar, you can iterate through all the values of
ToolbarSelection. Swift enumerations have a built-in array called allCases, but to
use it, the enumeration must conform to the CaseIterable protocol.
371
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
HStack {
ForEach(ToolbarSelection.allCases, id: \.self) { selection in
Button {
modal = selection
} label: {
ToolbarButton(modal: selection)
}
}
}
You iterate through all the available modals. Each button shows the correct image
and text for the modal, and the action sets the new modal state.
Button Preview
372
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
As of now, the buttons don’t do anything, so you’ll attach text views to each button.
As you progress through the book, you’ll replace the text view with the appropriate
content.
Here, for every modal selection, you print out the description of the modal. Later,
you’ll add cases to this switch statement when you configure each of the four modal
views.
373
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
var id = UUID()
You’ll immediately get a compiler error, saying “Enums must not contain stored
properties”. Remember that you can’t make a copy of an enumeration by
instantiating it, so you can’t add stored vars to an enumeration. Yet, you need to
include var id in order to conform to Identifiable.
Instead of a stored property, this var is a computed property. Now, when you create a
ToolbarSelection object, each object will have a different ID calculated from the
enumeration’s hash value.
374
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Open CardsListView.swift and tap a card. In live preview, tap the bottom
buttons to preview each of your modal views. The modal’s description displays on
each modal view.
A modal view
Cleaning Up
➤ Open BottomToolbar.swift, and in BottomToolbar, remove , id: \.self from
the ForEach loop.
ForEach(ToolbarSelection.allCases) { selection in
375
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Build and run your app in Simulator and check out your app’s outline so far.
376
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Challenge
Make it a habit to regularly tidy up the code and files in your app.
As an example, you can group all the files with View in their name a group called
Views. You can then have a sub group for the views used for a single card. You’ll find
suggested groups in the challenge project for this chapter.
377
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Key Points
• Prototypes are always worth doing. With a prototype it’s easier to see what’s
missing and what the next steps should be. They don’t have to be complicated. So
far, you aren’t creating or saving any data, but you can tell how the app will flow.
Writing an app is more than just writing code. It’s finding your target audience,
creating a good design and overcoming technical problems.
• Refactor your views early and often. Whenever you have a stack enclosing a
number of views with multiple modifiers, it’s worth extracting those views to
either a new View structure or to a new View property within the existing View
structure.
• There are several ways you can navigate apps. You can use built-in
NavigationStacks, completely customize with buttons or tapped views and
modal views or layered views.
• SwiftUI toolbars are great for standard placement. You must enclose them in a
NavigationStack to be able to see them.
• Dictionaries are useful for holding disparate values. For example, you can use
them to hold options to format views. Using tuples, you can create ad hoc types.
• You can show modal views conditionally using a type that conforms to
Identifiable.
378
14 Chapter 14: Gestures
By Caroline Begbie
Gestures are the main interface between you and your app. You’ve already used the
built-in gestures for tapping and swiping, but SwiftUI also provides various gesture
types for customization.
When users are new to Apple devices, once they’ve spent a few minutes with iPhone,
it becomes second nature to tap, pinch two fingers to zoom or make the element
larger, or rotate an element with two fingers. Your app should use these standard
gestures. In this chapter, you’ll explore how to drag, magnify and rotate elements
with the built-in gesture recognizers.
379
SwiftUI Apprentice Chapter 14: Gestures
➤ Open the starter project, which is the same as the previous chapter’s challenge
project with files separated into groups.
1. Create a RoundedRectangle view property. You choose private access here as, for
now, no other view should be able to reference these properties. Later on, you’ll
change the access to pass in any view.
2. Use content as the required View in body and apply modifiers to it.
380
SwiftUI Apprentice Chapter 14: Gestures
➤ Preview the view, and you’ll see your red rectangle with rounded corners.
Creating Transforms
Skills you’ll learn in this section: transformation
Each card in your app will hold multiple images and pieces of text called, generically,
elements. For each element, you’ll store a size, a location on the screen and a
rotation angle. In mathematics, you refer to these spatial properties collectively as a
transformation or transform.
381
SwiftUI Apprentice Chapter 14: Gestures
➤ Create a new group called Model that will hold data structure files.
➤ In the Model group, create a new Swift file called Transform.swift to hold the
transformation data.
➤ Replace the code in the file and create a structure with initialized spatial
properties:
import SwiftUI
struct Transform {
var size = CGSize(width: 250, height: 180)
var rotation: Angle = .zero
var offset: CGSize = .zero
}
You set up defaults for size, rotation and offset. Angle is a SwiftUI type which
conveniently works with both degrees and radians.
Notice the use of .zero here. Angle.zero and CGSize.zero are both type
properties that return zero values. You’ll discover more about type properties later
in this chapter. When the type is obvious to the compiler, as it is here, the compiler
will work out which type to use for .zero.
Often, transforms hold a scale value too, but in this case you’ll update the size of the
element instead of holding a scale value.
You hold the transform that you will apply to ResizableView as a state property.
Later on, you’ll pass the element’s saved transform in, but for now, just hold the
transform locally.
.frame(
width: transform.size.width,
height: transform.size.height)
382
SwiftUI Apprentice Chapter 14: Gestures
Because transform holds the same default size, the view does not change. Now
you’re ready to create gestures to move your view around.
You’ll start off with the drag gesture, where the user moves one finger across the
screen. This is also called a pan gesture. When the user touches down on a
ResizableView and drags a finger, the view will follow that finger. When they lift
the finger, the view will remain at that location.
You’ll give the view a modifier which will update the offset of ResizableView from
the center of its parent view. To position the view, you have a choice of using either
position(_:) or offset(_:) view modifier. You’re saving an offset value into
transform, so that’s what you’ll use here.
383
SwiftUI Apprentice Chapter 14: Gestures
The gesture updates transform’s offset property as the user drags the view.
onChanged(_:) has one parameter of type Value, which contains the gesture’s
current touch location and the translation since the start of the touch.
.offset(transform.offset)
.gesture(dragGesture)
384
SwiftUI Apprentice Chapter 14: Gestures
➤ Add a new property to ResizableView to hold the transform’s offset before you
start dragging:
385
SwiftUI Apprentice Chapter 14: Gestures
In onChanged(_:), you update transform with the user’s drag translation amount
and include any previous dragging.
In onEnded(_:), you replace the old previousOffset with the new offset, ready for
the next drag. You don’t need to use the value provided, so you use _ as the
parameter for the action method.
This works well. You can now drag your view around and position it wherever you
want.
The CGSize code is a bit long-winded though, with having to do the math on both
width and height. You can shorten this code by overloading the + operator.
Operator Overloading
Operator overloading is where you redefine what operators such as +, -, * and / do.
To add translation to offset, you must add width to width and, at the same
time, add height to height. To do this, you’ll redefine + with a new method.
➤ Create a new Swift file called Operators.swift. Any time you want to overload an
operator for a particular type, you can add the method in this file.
import SwiftUI
Here you specify what the + operator should do for a CGSize type. The parameters
are left and right, which are the items to the left and right of the + sign. You return
the new CGSize.
This is a simple example of how you want the + sign to work for CGSize. It makes
sense here to add the width and height together. However, you can redefine this
operator to do anything, and you should be very careful that the method makes
sense. Don’t do things like redefining a multiply sign to do division!
386
SwiftUI Apprentice Chapter 14: Gestures
You can see how overloading the + operator reduces the code and increases clarity.
Now that you can move your view around the screen, it’s time to rotate it. You’ll use
two fingers on the view and set up a RotationGesture to track the angle of rotation.
Just as you did with tracking the previous offset of the view, you’ll track the previous
rotation.
This will hold the angle of rotation of the view going into the start of the gesture.
387
SwiftUI Apprentice Chapter 14: Gestures
onChanged(_:) provides the gesture’s angle of rotation as the parameter for the
action you provide. You add the current rotation, less the previous rotation, to
transform’s rotation.
onEnded(_:) takes place after the user removes his fingers from the screen. Here,
you set any previous rotation to zero.
.rotationEffect(transform.rotation)
.gesture(dragGesture)
.gesture(rotationGesture)
To test your rotation effect in the live preview, because you don’t have a touch
screen available to you, you can simulate two fingers by holding down the Option
key. Two dots will appear, representing two fingers. (You may have to click the
preview before they show up.)
388
SwiftUI Apprentice Chapter 14: Gestures
Move your mouse or trackpad to change the distance between the two dots. Make
sure that they are both on the rectangle View, and click and drag. Your view should
rotate. If you have the distance between the dots correct, but you want the dots to be
elsewhere on the screen, you can hold down the Shift key as well as Option to move
the dots. Still holding Option, let go the Shift key when they are in the right place.
Order of modifiers is again important here. The pivot point of the rotation is around
the center of the view without taking any offset into consideration.
➤ Drag the view and then rotate it, and you’ll see that the view’s pivot point is
around the center of the screen. This is the view’s center point without the offset
applied.
Sometimes this may be what you want. But in your case here, you want to rotate the
view before offsetting it.
The order of gestures is also important. If you place the drag gesture after the
rotation gesture, then the rotation gesture will swallow up the touches.
389
SwiftUI Apprentice Chapter 14: Gestures
Rotation
➤ Gestures always feel better on a real device, so to run this on a device, open
CardsApp.swift
ResizableView()
Note: If you haven’t yet run an app on your device, take a look at Running
your Apps on an iOS Device in Chapter 2, “Planning a Paged App”. You’ll
need an Apple developer account set up in Settings to run the app on a device.
➤ Set your team identifier on the Cards app’s Signing & Capabilities tab.
➤ Build and run and try your gestures to see how fluid they feel. Two fingers on the
device feels much more natural than trying to manipulate the simulator gesture
dots.
390
SwiftUI Apprentice Chapter 14: Gestures
You’ll do the scale slightly differently from rotate and offset. The view will always be
at a scale of 1.0 unless the user is currently scaling. At the end of the scaling
operation, you’ll calculate the new size of the view and set the scale back to 1.0.
onChanged(_:) takes the current gesture’s scale and stores it in the state property
scale. To differentiate between the two properties called the same name, use self
to describe ResizableView’s @State property.
When the user has finished the pinch and raises his fingers from the screen,
onEnded(_:) takes the gesture’s scale and changes transform’s width and height.
You then reset ResizableView.scale to 1.0 to be ready for the next scale.
.scaleEffect(scale)
391
SwiftUI Apprentice Chapter 14: Gestures
.gesture(SimultaneousGesture(rotationGesture, scaleGesture))
You can now perform the two gestures at the same time.
➤ Try your three gestures in Live Preview. Then, build and run your app and try them
in Simulator or, if possible, on a device.
Completed gestures
392
SwiftUI Apprentice Chapter 14: Gestures
You’ve made a very useful view, one that can be used in many app contexts. Rather
than hard-coding the view you want to resize, you can change this view and make it
a modifier that acts on other views.
Here, you declare the new view modifier. For the moment, ignore all the compile
errors until you’ve completed the modifier.
➤ Remove:
Here, you set up the content that the view should use and add the modifier(_:)
with your custom view modifier.
393
SwiftUI Apprentice Chapter 14: Gestures
It’s always a good idea to keep your previews working. With view modifier previews,
you can provide an example to future users of your code how to use the modifier.
Always remember that “future users” includes you in a few weeks’ time!
CardsListView()
➤ In ResizableView.swift, resume your live preview and check out your new
modifier.
394
SwiftUI Apprentice Chapter 14: Gestures
extension View {
func resizableView() -> some View {
modifier(ResizableView())
}
}
You extend the View protocol with a default method. resizableView() is now
available on any object that conforms to View. The method simply returns your
modifier, but it does make your code easier to read.
.resizableView()
content
Eventually, content will show card elements, but for now you can test your new
resizable view. Here you test your modifier with two different types of views — two
Shapes and one Text. The Circle’s offset is applied on top of the offset in
resizableView(). Everything is put together inside a ZStack, which is a container
view that allows its children to use absolute positioning.
395
SwiftUI Apprentice Chapter 14: Gestures
There is a trick to scaling text on demand. Give the font a huge size, say 500. Then
apply a minimum scale factor to it, to reduce it in size.
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.lineLimit(1) ensures the text stays on one line and doesn’t wrap around.
396
SwiftUI Apprentice Chapter 14: Gestures
➤ Try resizing the text again in live preview. This time the text retains its size.
➤ Group Capsule and Text together inside the ZStack, and apply resizableView()
to Group instead of the two views:
Group {
Capsule()
.foregroundColor(.yellow)
Text("Resize Me!")
.fontWeight(.bold)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
}
.resizableView()
397
SwiftUI Apprentice Chapter 14: Gestures
Here, you grouped the two views together so they combine to a single view.
Grouped Views
When you resize the capsule now, you drag and resize both capsule and text at the
same time. This could be useful where you have a caption or a watermark on an
image and you want them both at the same scale.
Other Gestures
• Tap gesture
Similarly, you can use either the structure LongPressGesture to recognize a long-
press on a view, or use
onLongPressGesture(minimumDuration:maximumDistance:pressing:perform:)
if you don’t need to set up a separate gesture property.
398
SwiftUI Apprentice Chapter 14: Gestures
Type Properties
Skills you’ll learn in this section: type properties; type methods
So far, you’ve hard coded the size of the card thumbnail, and also the default size in
Transform. In most apps, you’ll want some global settings for sizes or color themes.
You do have the choice of holding constants in global space. You could, for example,
create a new file and add this code at the top level:
currentTheme is then accessible to your whole app. However, as your app grows,
sometimes it’s hard to immediately identify whether a particular constant is global
or whether it belongs to your current class or structure. An easy way of identifying
globals, and making sure that they only exist in one place, is to set up a special type
for them and add type properties to the type.
You already used the type property CGSize.zero. CGPoint also has a type property
of .zero and defines a 2D point with values in x and y. Examine part of the CGPoint
structure definition to see both stored and type properties:
extension CGPoint {
public static var zero: CGPoint {
CGPoint(x: 0, y: 0)
}
}
399
SwiftUI Apprentice Chapter 14: Gestures
When you create an instance of the structure CGPoint, you set up x and y properties
on the structure. These x and y properties are unique to every CGPoint you
instantiate.
To use CGPoint’s type property, you use the name of the type:
This sets up an instance of a CGPoint, named pointZero, with x and y values of zero.
When you instantiate a new structure, that structure stores its properties in memory
separately from every other structure. A static or type property, however, is
constant over all instances of the type. No matter how many times you instantiate
the structure, there will only be one copy of the static type property.
In the following diagram, there are two copies of CGPoint, pointA and pointB. Each
of them has its own memory storage area. CGPoint has a type property zero which is
stored once.
400
SwiftUI Apprentice Chapter 14: Gestures
➤ In Config, create a new Swift file called Settings.swift and replace the code with:
import SwiftUI
struct Settings {
static let cardSize =
CGSize(width: 1300, height: 2000)
static let thumbnailSize =
CGSize(width: 150, height: 250)
static let defaultElementSize =
CGSize(width: 250, height: 180)
static let borderColor: Color = .blue
static let borderWidth: CGFloat = 5
}
Here you create default values for the final card size, the card thumbnail size, the
card element size and for a border that you’ll use later.
Notice that you created a structure. While this works, it could become problematic,
because you could instantiate the structure and have copies of Settings throughout
your app.
However, if you use an enumeration, you can’t instantiate it, so it ensures that you
will only ever have one copy of Settings.
enum Settings {
Using an enumeration and type properties in this way future-proofs your app. Later
on, someone else might want to add another setting to your app. They won’t need to
change the enumeration itself, but they’ll simply be able to create an extension.
401
SwiftUI Apprentice Chapter 14: Gestures
For example, they could add a new type property like this:
extension Settings {
static let aNewSetting: Int = 0
}
➤ Open CardThumbnail.swift. Instead of defining the frame size here, you can rely
on your settings defaults.
.frame(
width: Settings.thumbnailSize.width,
height: Settings.thumbnailSize.height)
If you want to change these sizes later on, you can do it in Settings.
➤ Create a new group and name it Extensions. In Extensions, create a new Swift
file called ColorExtensions.swift and replace the code with:
import SwiftUI
extension Color {
static let colors: [Color] = [
.green, .red, .blue, .gray, .yellow, .pink, .orange, .purple
]
}
You created an array of Colors that’s available throughout the app by referencing
Color.colors.
402
SwiftUI Apprentice Chapter 14: Gestures
This method returns a random element from the colors array and, if the colors array
is empty, returns black.
Swift Tip: Astute readers will notice that this method could just as easily have
been a static var computed property. However, conventionally, if you’re
returning a value that may change often, or there is complex code, use a
method.
.foregroundColor(.random())
Here you use the static method that you created on Color. Each time you list the
thumbnails, they will use different colors.
➤ Preview CardsListView.swift and see your random card colors. Each time you
press the Live icon, the colors change.
Random color
403
SwiftUI Apprentice Chapter 14: Gestures
Challenge
Challenge: Make new View Modifiers
View modifiers are not just useful for reusing views, but they are also a great way to
tidy up. You can combine modifiers into one custom modifier. Or, as with the toolbar
modifier in SingleCardView, if a modifier has a lot of code in it, save yourself some
code reading fatigue, and separate it into its own file.
Your challenge is to create a new view modifier that takes the toolbar code and
moves it into a modifier called CardToolbar.
To do this, you’ll:
3. Remove the preview, as it doesn’t make sense to have one for this modifier.
4. For body, cut the toolbar and sheet modifier code from SingleCardView and
paste the modifiers on CardToolbar’s content.
When you’ve completed the challenge, your code should work the same, but, with
this refactoring, SingleCardView is easier to read.
As always, you’ll find the solution in the challenge folder for this chapter.
404
SwiftUI Apprentice Chapter 14: Gestures
Key Points
• Custom gestures let you interact with your app in any way you choose. Make sure
the gestures make sense. Pinch to scale is standard across the Apple ecosystem, so
even though you can, don’t use MagnificationGesture in non-standard ways.
• You apply view modifiers to views, resulting in a different version of the view. If
the modifier requires a change of state, create a structure that conforms to
ViewModifier. If the modifier doesn’t require a change of state, you can make
code more readable by adding a method to a View extension and use that method
to modify a view.
• static or type properties and methods exist on the type. Stored properties exist
per instance of the type. Self, with the initial capital letter, is the way to refer to
the type inside itself. self refers to the instance of the type. Apple uses type
properties and methods extensively. For example, Color.yellow is a type
property.
Create your own modifiers. Any time you repeat your view’s design, you should look
at creating a method or a modifier that encapsulates that code.
Think about parts of your app in modules. In this chapter, you created a useful
resizable view modifier which you can now use in any app that you create. When
creating views, consider how you could abstract them and make them more generic.
405
15 Chapter 15: Structures,
Classes & Protocols
By Caroline Begbie
It’s time to build the data model for your app so you have some data to show on your
app’s views.
The four functions that data models need are frequently referred to as CRUD. That’s
Create, Read, Update, Delete. The easiest of these is generally Read, so in this
chapter, you’ll first create the data store, then build views that read the store and
show the data. You’ll then learn how to Update the data and store it. That will leave
Create and Delete. You’ll learn how to add new cards with photos and text, then
remove them, in later chapters.
406
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
• PreviewData.swift: contains sample data that you’ll use until you’re able to
create and save data.
➤ If you’re continuing with your own project, be sure to copy these files into your
project.
Data Structure
Take another look at the back of the napkin sketch:
407
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
You’ll need a top level data store that will hold an array of all the cards. Each card
will have a list of elements, and these elements could be an image or text.
Data structure
You don’t want to constrain yourself to image or text though, as you might add new
features to your app in the future. Any data model you create now should be
extensible, meaning as flexible as possible, to allow future capabilities.
Before creating the data model, you’ll need to decide what types to use to store your
data. Should you use structures or classes?
A Swift data type is either a value type or a reference type. Value types, like
structures and enumerations, contain data, while reference types, like classes,
contain a reference to data.
408
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
At runtime, your app instantiates properties and assigns them to separate areas of
memory, called the stack and the heap. Value types go on the stack, which the CPU
manages and optimizes, so it’s fast and efficient. You can instantiate structures,
enumerations and tuples without counting the cost. The heap, however, is much
more dynamic and allows an app to allocate and deallocate areas of memory, while
maintaining reference counts. This makes allocating reference types less efficient.
When you instantiate a class, that piece of data should stick around for a while.
When initializing classes and structures in code, they look very similar. For example:
The important difference here is that iAmAStruct contains immutable data, whereas
iAmAClass contains an immutable reference to the data. The data itself is still
mutable and you can change it.
When you assign value types, such as a CGPoint, you make a copy. For example:
With a reference type, you access the same data. For example:
409
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Swift keeps a count of the number of references to the AClass object created in the
heap. The reference count here would be two, and Swift won’t deallocate the object
until its reference count is zero.
Changing the data like this can be a source of errors for unwitting developers. One of
Swift’s principles is to prevent accidental errors, and if you favor value types over
reference types, you’ll end up with fewer of those accidents. In this app, you’ll favor
structures and enumerations over classes where possible.
Returning to the complex matter of deciding how to store your data, you need to
choose between a structure and a class.
In general, when you hold a simple piece of data, such as a Card or a CardElement,
those are lightweight objects that you won’t need forever. Typically, you’d make
those a structure. However, when you hold a data store that you’re going to use
throughout your app, that’s a good candidate for a class. In addition, if your data has
publisher properties, it must conform to ObservableObject, which requires you to
use a class.
Now, you’ll get started creating your data model, beginning at the bottom of the data
hierarchy with the element.
➤ In the Model group, create a new Swift file called CardElement.swift and replace
the code with:
import SwiftUI
struct CardElement {
}
This is the file where you’ll describe the card elements. You’ll come back to this
shortly to define the data you’ll hold.
410
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ Create a new Swift file called Card.swift and replace the code with:
import SwiftUI
You also hold a background color for the card and an array of elements for all the
images and text that you’ll place on the card.
➤ Create a new Swift file named CardStore.swift and replace the code:
import SwiftUI
CardStore is your main data store and your single source of truth. As such, you’ll
make sure that it stays around for the duration of the app. It isn’t, therefore, a
lightweight object, and you choose to make it a class.
You’ve now set up a data model that SwiftUI can observe and write to. There is a
difficulty with card elements, however. These can be either an image or text.
411
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Class Inheritance
Skills you’ll learn in this section: class inheritance; composition vs
inheritance
You might have come across object oriented programming (OOP) in Swift or other
languages. This is where you have a base object, and other classes derive — or
inherit — from this base object. Swift classes allow inheritance. Swift structures do
not.
class CardElement {
var transform: Transform
}
Here you have a base class CardElement with two sub-classes inheriting from
CardElement. ImageElement and TextElement both inherit the transform property,
but each type has its own separate relevant data.
Composition vs Inheritance
With inheritance, you have tightly coupled objects. Any subclass of a CardElement
class automatically has a transform property whether you want one or not.
You might possibly decide in a future release to require some elements to have a
color. With inheritance, you could add color to the base class, but you’d then be
holding redundant data for the elements that don’t use a color.
412
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
An alternative scenario is to use composition with protocols, where you add only
relevant properties to an object. This means that you can hold your data in
structures.
Composition
Traditionally, inheritance is considered to be an “is a” relationship, while
composition is a “has a” relationship. But, you should avoid tightly-coupled objects
as much as you can, and composition gives you much more freedom in design.
Protocols
Skills you’ll learn in this section: create protocol; conform structures to
protocol; protocol method
You’ve used several protocols so far — such as View and Identifiable — and,
possibly, been slightly mystified as to what they actually are.
Protocols are like a contract. You create a protocol that defines requirements for a
structure, a class or an enumeration. These requirements may include properties and
whether they are read-only or read-write. A protocol might also define a list of
methods that any type adopting the protocol must include.
413
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Protocols can’t hold data; they are simply a blueprint or template. You create
structures or classes to hold data and they, in turn, conform to protocols.
View is the protocol you’ve used most. It has a required property body. Every view
that you’ve created has contained body and, if you don’t provide one, you get a
compile error.
You’ve also used Identifiable. id is a required property, so each time you conform
a type to Identifiable, you create an id property that is guaranteed to be unique.
In your app, every card element will have a transform, so you’ll change
CardElement to be a protocol that requires any structure adopting it to have a
transform property.
protocol CardElement {
var id: UUID { get }
var transform: Transform { get set }
}
Here you create a blueprint of your CardElement structure. Every card element type
will have an id and a transform. id is read-only, and transform is read-write.
TextElement also conforms to CardElement and holds a string for text, the default
text color and the default font.
414
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
With protocols, you future-proof the design. If you later want to add a new card
element that is just a solid color, you can simply create a new structure
ColorElement that conforms to CardElement.
Card holds an array of CardElements. Card doesn’t care what type of CardElement it
holds in its elements array, so it’s easy to add new element types.
protocol Findable {
func find()
}
Sometimes you want a default method that is the same across all conforming types.
For example, in your app, a card will hold an array of card elements. Later, you’ll
want to find the index for a particular card element.
This is quite hard to read and you have to remember the closure syntax. Instead, you
can create a new method in CardElement to replace it.
extension CardElement {
func index(in array: [CardElement]) -> Int? {
array.firstIndex { $0.id == id }
}
}
This method takes in an array of CardElement and passes back the index of the
element. If the element doesn’t exist, it passes back nil. The way you’ll use it is:
This is a lot easier to read than the earlier code, and the complicated closure syntax
is abstracted away in index(in:). Any type that conforms to CardElement can use
this method.
415
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Now that you have your views and data model implemented, you have reached the
exciting point of showing the data in the views. Your app doesn’t allow you to add
any data, so your starter project has some preview data to work with until you can
add your own.
➤ In the Preview Content group, take a look at PreviewData.swift and remove the
comment tags /* */. This code was commented to remove compile errors while you
built your data model.
There are five cards. The first card uses the array of four elements, which are a
mixture of images and text. You’ll use this data to test new views. The card elements
are positioned for portrait orientation on iPhone 14 Pro. As they are hard-coded, if
you run the app in landscape mode or on a smaller device, some of the elements will
be off the screen. Later, your card will take on a fixed size, and the elements will
scale to fit in the available space.
When you first instantiate CardStore, the initializer will load the preview data when
defaultData is true.
Later, when you can save and load cards from files, you’ll update this to use saved
cards. For the moment, you’ll use the preview data.
You’ll need to instantiate CardStore, and the best place to do that is at the start of
the app.
416
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
You use @StateObject to ensure that the data store persists throughout the app.
➤ Add a modifier to CardsListView() so you can address the data store through the
environment:
.environmentObject(store)
Whenever you create an environment object property, you should make sure that the
SwiftUI preview instantiates it. If you don’t do this, your preview will crash
mysteriously with no error message.
.environmentObject(CardStore(defaultData: true))
ForEach(store.cards) { card in
Here you iterate through store.cards. Remember that ForEach in this format
requires Card to be Identifiable.
You don’t need card to be mutable here, as you’ll only read from it to get the card’s
background color for the thumbnail.
.foregroundColor(card.backgroundColor)
417
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Instead of a random color, you use the background color of the card for the
thumbnail.
➤ Update the preview to use the first card in the provided preview data:
CardThumbnail(card: initialCards[0])
CardThumbnail(card: card)
➤ Preview the view and check that the scrolling card thumbnails use the background
colors from the preview data:
Choosing a Card
When you tap a card, you set isPresented to true, which triggers the full screen
modal for the single card. SingleCardView should now use the data for the selected
card.
418
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
selectedCard = card
When selectedCard is not nil, the system will show SingleCardView in the full
screen modal. When you tap the Done button and dismiss the modal, the system will
reset selectedCard to nil.
SingleCardView(card: card)
SingleCardView(card: initialCards[0])
With the background color, you’ll be able to tell whether the app is displaying the
correct selected card.
419
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Mutability
Skills you’ll learn in this section: mutability
But wait! In SingleCardView, is card mutable? You’ll want to add images and text to
the card later on, so it does need to be mutable.
The answer, of course, is that you passed card with a let and therefore it is read-
only. To get a mutable card, you need to access the selected card in the data store’s
cards array by index.
This finds the first card in the array that matches the selected card’s id and returns
the array index, if there is one.
420
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
You work out the array index of the selected card in the data store’s cards array and
pass it as a binding to SingleCardView. This should never fail but, just in case, you
add a fatal error message.
SingleCardView(card: .constant(initialCards[0]))
➤ Preview CardsListView.swift and the result is the same as previously, but you’re
now all set up to update the card with new elements.
This view will contain only the card and its elements.
import SwiftUI
421
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
}
}
}
Here you:
➤ Preview the view to see the background color from the first card in your preview
data.
➤ Under the existing CardElementView, create a new view for an image element:
422
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
This simply takes in an ImageElement and uses the stored image as the view.
In the same way, this view takes in a TextElement and uses the stored text, color and
font.
Swift Tip: To find out what fonts are on your device, first list the font families
in UIFont.familyNames. A font family might be “Avenir” or “Gill Sans”. For
each family, you can find the font names using
UIFont.fontNames(forFamilyName:). These are the weights available in the
family, such as “Avenir-Heavy” or “GillSans-SemiBold”.
Depending on whether the card element is text or image, you’ll use one of these two
views. Note the ! in front of !element.text.isEmpty. isEmpty will be true if text
contains "", and ! reverses the conditional result. This way you don’t create a view
for any blank text.
With these two views as examples, when “future you” adds a new type of element, it
will be easy to add a new view specifically for that element.
423
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
When presented with a CardElement, you can find out whether it’s an image or text
depending on its type.
CardElementView(element: initialElements[0])
Here you show the first element which contains a hedgehog image. To test the text
view, change the parameter to initialElements[3].
424
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Always be aware of whether your data is mutable. Later, you’ll update the element’s
Transform within this ForEach loop. Generally, when you iterate through an array in
a loop, the individual item is immutable. However, this variant of ForEach allows
binding syntax by adding the $ in front of the array and the individual item.
➤ Live Preview the view and see the elements all in the center of the view:
CardDetailView(card: $card)
425
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ Live Preview the app, or run in Simulator. Load the first card and move around the
elements.
You’ve now completed the R in CRUD. Your views read and display all the data from
the store. You’ll now move on to U — updating the model when you resize, move and
rotate card elements.
426
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
As you’ve learned already, inside a View, all properties are immutable unless they are
created with a special property wrapper. A state property is the owner of a piece of
data that is a source of truth. A binding connects a source of truth with a view that
changes the data.
Your source of truth for all data is CardStore. When you select a particular card, you
pass a binding to the card to SingleCardView.
CardDetailView declarations
CardDetailView expects an environment object and a binding. The type of these
are in angle brackets. store is an environment object of type CardStore, and card is
a binding of type Card.
Another common place where you might find this language construct is an Array.
You defined an array in CardStore like this:
427
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
The method receives a binding that is of Transform type and passes it on to the view
modifier.
.resizableView(transform: .constant(Transform()))
.resizableView(transform: $element.transform)
428
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
ResizableView will now operate on the mutable element’s transform property, and
the preview places the card elements in the correct position.
429
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
In the preview, you’re more closely reproducing the parent code that calls
CardDetailView, and you’re able to move and resize the elements. Any changes you
make in position or size will save to the data store.
There is still one problem. When you first reposition any element except for the
central one, it jumps to a different position.
.onAppear {
previousOffset = transform.offset
}
430
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
When the view first appears, you initialize previousOffset. This will happen only
once.
➤ Build and run and choose the first card. In the detail view, the initial position
jump has gone away, and you can now move, rotate and resize the card elements.
431
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Key Points
• Use value types in your app almost exclusively. However, use a reference type for
persistent stored data. Your stored data should be in one central place in your app.
• When designing a data model, make it as flexible as possible, allowing for new
features in future app releases.
• Use protocols to describe data behavior. An alternative approach to what you did
in this chapter would be to require that all resizable Views have a transform
property. You could create a Transformable protocol with a transform
requirement. Any resizable view must conform to this protocol.
• You had a brief introduction to generics in this chapter. Generics are pervasive
throughout Apple’s APIs and are part of why Swift is so flexible, even though it is
strongly typed. Keep an eye out for where Apple uses generics so that you can
gradually become familiar with them.
• When designing an app, consider how you’ll implement CRUD. In this chapter, you
implemented Read and Update. Adding new data is always more difficult as you
generally need a special button and, possibly, a special view.
If you’re still confused about when to use class inheritance and OOP, watch this
classic WWDC video (https://apple.co/3k9GUEM) where the protagonist, Crusty,
firmly declares “I don’t do object-oriented”.
432
16 Chapter 16: Adding Assets
to Your App
By Caroline Begbie
Initially, in this chapter, you’ll learn about managing assets held in an asset catalog
and you’ll create that all-important app icon. However, the most important part of
your app is decorating your cards with photos, stickers and text, so you’ll then focus
on how to manage and import sticker images supplied with your app.
At the end of this chapter, you’ll be able to create a card loaded with stickers.
433
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Asset catalogs are by far the best place to manage image and color sets.
Within an asset catalog, under one image set, you can define multiple images for
different themes, different devices, different scales and even different color gamuts.
When you use the name of the image set in your code, the app will automatically
load the correct image for the current environment. When you supply differently
scaled images for different devices in an asset catalog, the app store will
automatically do app thinning and only download the relevant images for that
particular device. This is potentially a huge reduction in app download size.
The asset catalog also holds the app icon, the launch screen image and launch screen
background color.
➤ Click the project name Cards at the top of the Project navigator. Choose the target
Cards. On the General tab, find App Icons and Launch Screen:
434
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
The iOS app template created an empty icon set called AppIcon when you first
created your project.
435
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
This figma file includes the app icon, itself designed in Figma with different vector
shapes:
➤ With the AppIcon set selected in Xcode, drag app-icon.png to the icon area.
436
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Build and run, and swipe up from the bottom to exit your app. You’ll see your new
icon instead of the old placeholder icon.
➤ Select the new icon you just dragged in, and show the Attributes inspector.
Change iOS from Single Size to All Sizes.
437
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
You can now drag different PNG image files to the various icon spots.
Vector vs Bitmap
You imported a bitmap PNG image for the icon. For other assets, you can use vector
formats, such as PDF or SVG. When possible, it’s always better to use vector formats.
These are made up of lines, curves and fills. For a vector line, you can set a start
point and an end point. When you scale the line, the vector resizes without losing
any of its resolution. With a bitmap line, you must stretch or compress pixels.
This image shows two 50 pixel wide images scaled up by twelve to 600 pixels. One is
bitmap and the other is vector. You can see the vector image loses none of its
sharpness.
Bitmap vs vector
➤ In Finder, drag in error-image.svg from the assets folder for this chapter to the
asset catalog panel under AppIcon.
Error image
The image imports into Xcode in the 1x space, and leaves the 2x and 3x spaces
empty. These spaces are for devices with different resolutions.
438
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
When you provide bitmap assets, you must provide them for every device resolution.
However, error-image.svg is a vector format image with a native size of 512x512.
You don’t need to scale it by 2x and 3x as Xcode can do this for you.
➤ With the error image selected, in the Attributes inspector, change Scales from
Individual Scales to Single Scale.
Single scale
Xcode removes the 2x and 3x options in the center panel. When you build for a 2x
resolution device, Xcode will automatically add to your app bundle a 512x512
optimized bitmap image scaled to the correct 2x resolution. Bundle images are held
in a .car file, the format of which is not publicly available, so you can’t inspect what
Xcode has done.
439
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Launch Screen
Skills you’ll learn in this section: launch screen; size classes
Another use for the asset catalog is to hold a launch screen image and background
color that displays while your app is launching. You’ve already come across
Info.plist in Chapter 7, “Saving Settings”. This .plist file is where you’ll set the
launch image and color.
➤ Click Cards at the top of the Project navigator and choose the Cards target.
Choose the Info tab, and you’ll see the contents of Info.plist in the Custom iOS
Target Properties section.
Info.plist
You can add new items either by right-clicking an entry and choosing Add Row or by
moving your cursor over an item and clicking the + sign that appears. You can delete
items by clicking the - sign.
➤ Click the disclosure control next to Launch Screen, then add items for Image
Name and for Background Color.
LaunchImage
440
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
LaunchColor
You may need to resize the columns to show the Value column.
Launch configuration
➤ Open Assets.xcassets, click the + sign at the bottom of the assets panel and
choose Image Set. Rename Image to LaunchImage.
➤ Click the + sign at the bottom of the assets panel again and choose Color Set.
Rename Color to LaunchColor.
When you run your app now, the app will use these for the launch screen.
Unfortunately, the simulator doesn’t clear launch screen caches, so if you change
your launch image or color, in Simulator, you’ll have to go to Device > Erase All
Contents and Settings… and clear the simulator completely. On a device, deleting
the app should be sufficient, but you might have to restart the device as well.
➤ Click LaunchImage in the catalog. You have the option of filling the three images.
However, just as with the error image, you’ll use a single scale SVG image.
441
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Launch Image
This SVG with a transparent background has a native size of 200x500px. At build
time, Xcode will create the appropriately scaled bitmap image from this and display
it in the center of the screen. When you launch the app in landscape on iPhone,
you’ll need an image with a smaller height, so you’ll use size classes to decide which
image to load.
Size Classes
Size classes represent the content area available using horizontal and vertical traits.
These two traits can be either regular or compact. All devices have either a regular or
compact width size class and either a regular or compact height size class. You can
find a list of these size classes in Apple’s Human Interface Guidelines (https://
apple.co/348lVx0) under the section Device size classes.
This is an illustration of some iPhones and iPads laid on top of each other:
442
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Size classes
For height in portrait mode, all devices fit into the regular height section. In
landscape, all iPhones use compact height, but some larger iPhones use regular
width rather than the smaller compact width.
iPads are always regular width and regular height. However, you still have to take
into account size classes on iPad, because of split screen and resizing app windows.
When in portrait mode, split screen apps are both compact width. In landscape
mode, the user can size between compact width and regular width.
For your app, the current launch image will fit on all devices except for iPhones in
landscape. So you’ll specify a sideways image for compact height.
443
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
444
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Build and run, and your launch screen should show up briefly before your app
does. Try rotating the simulator to get the different landscape launch screen. If your
launch screen doesn’t show up, remember to erase the simulator contents.
Note: At the time of writing, there appears to be a bug in scaling. The SVG
image sometimes stretches to full screen. If this bug persists, you would have
to resize the image yourself to fit an iPad screen, instead of relying on the
asset catalog to manage scaling.
There’s one thing that an asset catalog does not allow you to do. You can’t
enumerate all the images it contains.
When you release your cards app, one way of making it stand out from the crowd is
to have some excellent stickers.
You could still add the stickers to an asset catalog, but you’d have to keep track of
how many there are and ensure that you have a strict naming convention. All items
in asset catalogs need to have names that are unique in the app bundle.
445
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
As your app becomes more popular, you’ll probably add more stickers and, maybe,
categorize them into themes. It would be cumbersome to list each asset by name.
You might have multiple artists working on stickers, and you wouldn’t want them to
have access to your project.
Camping stickers
Xcode Groups
When you look in the Project navigator, currently all your groups, except for the
asset catalog, have gray folder icons.
446
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Absolute location
When you create a new group, you can choose to mirror that group with a folder on
disk. If you have a file selected inside a group connected to a disk folder, and you
create a new group with File ▸ New ▸ Group, then Xcode will create both a new
group and a new folder.
If your current selection is inside a logical group without a mirrored folder on disk,
then Xcode won’t create a new folder for a new group. The option under File ▸ New
▸ Group will do the opposite. It changes between Group with Folder or Group
without Folder depending upon whether your currently selected file is inside a
mirrored group or not.
447
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
If your current file is in a logical group, the gray icon has a tiny triangle at the
bottom left.
Groups
This is the file and folder organization in Finder:
Organization in Finder
Notice that File.swift isn’t in a physical subfolder because it’s contained in a logical
group in Xcode.
Reference Folders
Unlike groups, Xcode doesn’t organize reference folders at all. When you bring a
reference folder into the project, it will have a blue icon and its hierarchy will reflect
the disk hierarchy.
You’ll treat this folder as the master folder for your sticker assets. Any stickers that
your artists create should go into this folder.
448
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Warning: Whenever you drag a file or folder into Xcode, make sure you
examine these settings. You’ll usually check Copy items if needed, and
generally, you want to create groups, not folder references.
You now have a blue folder called Stickers in your project, with a blue sub-folder of
Camping. The blue folder marks it as a reference folder. Xcode will only allow you to
create folders inside this folder, not groups.
Reference folder
➤ In Finder, create a new folder inside Stickers called Nature and copy Camping/
tree.png to this folder. Xcode will immediately update its hierarchy to reflect what’s
happening on disk.
449
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
If you tried this with gray groups (don’t!), Xcode wouldn’t be able to find any files
you moved in Finder.
Another advantage with reference folders, is that with Stickers as the top level
folder, your artists can create new themes in different folders without touching the
Xcode project.
Note: Sometimes your project may lose the reference to the Stickers folder of
images. In this case, Stickers will appear in the Project navigator in red.
Choose the red folder name and, in the Attributes inspector, tap the folder
icon under Location. Navigate to your Stickers folder and click Choose.
Alternatively, you can delete this red item and re-import the Stickers folder as
a reference folder. If you ever want confirmation of where the folder is, right-
click the folder and choose Show in Finder.
➤ In Single Card Views, create a new sub-group called Card Modal Views, and in
this group, create a new SwiftUI View file called StickerModal.swift.
450
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Add a new case to the switch statement before the default case:
case .stickerModal:
StickerModal()
➤ Open SingleCardView.swift, Live Preview it, and pin the preview, so you can
access it from other views.
➤ Try out your new stickers modal by tapping Stickers in live preview.
When you load an image from a folder, you load it into an instance of UIKit’s
UIImage. You also need to provide the full app bundle resource path.
451
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
2. Load the UIImage using the full name and path of the sticker and use the
uiImage parameter for creating the Image view.
➤ In Live Preview, on the pinned Single Card View, tap the Stickers button and
you’ll see the sticker image.
Fire sticker
However, you don’t only want one sticker, you want to see all of them. Depending on
how many stickers you have, you shouldn’t load up all the UIImages at once, as
loading images is resource heavy and will block the user interface.
You can load the file names up front and, as the user scrolls, load the image when it’s
needed. This is called lazy loading.
452
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
You’ll first load the folder names in the top level in the Stickers folder. These will be
themes. You’ll be able to add new themes to your app in the future simply by adding
a new folder inside the Stickers folder in Finder. You won’t have to change any code
to do this.
// 1
let fileManager = FileManager.default
if let resourcePath = Bundle.main.resourcePath,
// 2
let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: resourcePath + "/Stickers"),
includingPropertiesForKeys: nil,
options: [
.skipsSubdirectoryDescendants,
.skipsHiddenFiles
]) {
// 3
for case let url as URL in enumerator
where url.hasDirectoryPath {
themes.append(url)
}
}
2. Get a directory enumerator, if it exists, for the Stickers folder. For the options
parameter, you skip subdirectory descendants and hidden files. Unless you skip
the subdirectories, an enumerator will continue down the hierarchy. You
currently just want to collect the top folder names as the themes.
Next you’ll iterate through all the theme directories and retrieve the file names
inside.
453
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
For each theme folder, you retrieve all the files in the directory and append the full
path to stickerNames. You then return this array from the method.
You temporarily print out the path name so that you can check whether you’re lazily
loading the image. You then return the UIImage loaded from the path name. If you
can’t load the image, return the error image from the asset catalog that you created
earlier. As this is still optional and you need to return a non-optional, if everything
fails, create a blank UIImage.
454
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Instead of just showing one sticker, you iterate through all the sticker names and
create an Image from the UIImage.
➤ To see the print output, build and run. Choose the first card and tap Stickers.
Watch the debug console output, and you’ll see all the images are loading up front,
ending with the tree and the guitar. As mentioned before, with a lot of stickers, this
will block the user interface.
Stickers loaded
LazyVStack {
455
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Build and run, and display the stickers modal screen again. Now, only the images
that show on screen, plus the one just after, load. Scroll down, and you’ll see in the
debug console that the guitar image loads as you approach it. Your images are now
loading lazily.
Note: At the time of writing, there appears to be a SwiftUI bug that duplicates
the calling of image(from:), causing the name of the image to print out twice
in the debug console.
These images are much too big and would look much better in a grid. Fortunately, as
well as lazy VStack and HStacks, SwiftUI provides a lazy loading grid view.
LazyVGrid and LazyHGrid provide vertical and horizontal grids. With the
LazyVGrid, you define how to layout columns and, with the LazyHGrid, you layout
rows.
let columns = [
GridItem(spacing: 0),
GridItem(spacing: 0),
GridItem(spacing: 0)
]
LazyVGrid(columns: columns) {
You still use the same ForEach and Image views but they now fit into the available
space in the grid instead of taking up the whole width of the screen. The grid uses all
the horizontal available space and divides it equally among the specified GridItems.
456
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ To visualize this, in the design canvas panel, view Sticker Modal and click the
Selectable icon. In the code panel, place the cursor on Image in body. The outlines
of the Images will show in the preview.
Vertical grid
Swift Tip: If this were a LazyHGrid, you would define rows in the same way as
you have columns, and the grid would divide up the available vertical space. To
scroll horizontally, add a horizontal axis: ScrollView(.horizontal).
➤ In the design canvas, click the Variants icon, and choose Orientation Variants.
Orientation variants
457
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Although the grid looks good in portrait mode, it would look better with more
images horizontally when in landscape.
let columns = [
GridItem(.adaptive(minimum: 120), spacing: 10)
]
Adaptive grid
The parent of the modal will pass in a state property to hold the selected image.
458
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
.onTapGesture {
stickerImage = image(from: sticker)
dismiss()
}
When the user taps an image, you’ll update the bound sticker image and dismiss the
modal.
StickerModal(stickerImage: .constant(UIImage()))
You can now select a sticker and at the same time dismiss the modal.
CardDetailView will then take over and store and show the selected sticker.
You receive the current card from the parent view and hold the current sticker
chosen from StickerModal.
➤ Locate the sheet(item:) with the stickerModal case. This will have a compile
error as you’re not yet passing the state property to StickerModal.
StickerModal(stickerImage: $stickerImage)
.onDisappear {
if let stickerImage = stickerImage {
card.addElement(uiImage: stickerImage)
}
stickerImage = nil
}
On dismissal of the modal, you should store the sticker as a card element and reset
the sticker image to nil. You’ll get a compile error until you’ve added the card
binding and written addElement(uiImage:).
459
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Open SingleCardView.swift and, in body, change the modal toolbar modifier to:
.modifier(CardToolbar(
currentModal: $currentModal,
card: $card))
Ensure that your CardToolbar parameters are in the same order that you listed the
bindings in CardToolbar.swift, otherwise you will get a compile error.
Now that you’re adding image data, instead of storing an Image, which is a View,
you’ll store the data and construct a view from that data.
You create a computed property that builds an Image from uiImage. If this is nil,
present the error image in the asset catalog. If, for some reason this doesn’t exist,
create a blank UIImage.
460
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Here you take in a new UIImage and add a new ImageElement to the card. In the
following chapter, you’ll be able to use this method for adding photos too.
➤ Build and run, select a card and add some stickers to it. Resize and reposition the
stickers as you want and create a masterpiece :].
461
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Challenges
Challenge 1: Set Up a Dark Mode Launch
Screen
Your app currently has different launch screens for portrait and landscape, when the
height size class is compact. Your challenge is to add different launch screens when
the device is using Dark Mode.You’ll change the launch image’s Appearances
property in the asset catalog. You’ll find the dark launch screen images in the assets
folder. Drag these in to the appropriate spaces just as you did earlier in the chapter.
When you test on the simulator, to get the new launch screen to show, you’ll need to
erase all device contents and settings.
462
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Key Points
• Most of the time, you should manage your images and colors in asset catalogs.
• If the asset catalog is not suitable for purpose, then use reference folders.
• In asset catalogs, favor vector images over bitmaps. They are smaller in file size
and retain sharpness when scaled. Xcode will automatically scale to the
appropriate dimensions for the current device.
• Think about how you can make your app special. Good app design together with
artwork can really make you stand out from the crowd.
For example, rather than using branding on the launch screen, they suggest making
your launch screen similar to the first screen in your app, so that it appears that the
app loads quickly.
You should study the HIG so that you know what Apple is looking for in an app.
People who use Apple devices enjoy clean, crisp interfaces, and the guidelines will
help you follow this quest. If you follow the guidelines diligently, you might even be
featured by Apple in the app store.
463
17 Chapter 17: Adding Photos
to Your App
By Caroline Begbie
In the previous chapter, you learned how to add stickers to your card. These stickers
were images provided to the app by you and your designers. Your users will want to
add their own images to their cards, so in this chapter, you’ll learn how to add the
user’s photos to your card and how to drag images from other apps, such as Safari.
464
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Loading photos is not as simple as loading stickers, because the user’s media library
might number in the tens of thousand of assets. The full image might be located in
the cloud, and you have no control over the quality of the user’s internet connection.
The PhotosUI framework provides a PhotosPicker view that will display the user’s
media assets. The user then selects photos, and each selected item goes into an
array. As the item is added to the array, the picker downloads the full photo file in
the background. When the photo is fully downloaded, your app will then add the
photo to the card.
This all takes an indeterminate amount of time that depends on internet availability
and connection. Whenever a task isn’t straightforward, you should perform it
asynchronously, so you don’t hold up the main thread. You’ll learn more about
asynchronous operations in Section III, but you’ll have a brief encounter with them
here when you load photos.
Instead of using your own modal view, you’ll use PhotosUI’s PhotosPicker.
➤ Open the starter project for this chapter, which is the same as the previous
chapter’s challenge project.
➤ In the Card Modal Views group, create a new SwiftUI View file called
PhotosModal.swift and import the framework:
import PhotosUI
465
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
1. Create an array to hold the selected images. The type PhotosPickerItem doesn’t
contain the actual image data. Instead, it contains only an identifier and the type
of content, such as jpeg, that the item supports.
3. As the user taps and selects media assets, the photos picker adds them to
selectedItems.
4. You can filter the photo library in various ways, such as screenshots or videos. For
Cards, you filter images. You can see the other available filters here (https://
apple.co/3flsc0C).
5. PhotosPicker requires a label to start it, so you include the image and text you
already set up in ToolbarButton.
Note: Be careful with your file names. If you create a structure called
PhotosPicker, that will override the one used by PhotosUI without any
warning. You can still use PhotosUI.PhotosPicker, but you have to
specifically reference PhotosUI when you do.
466
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
PhotosModal(card: .constant(Card()))
In Live Preview, you’ll see the button with the label you provided:
467
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
This is where you display the modal views when the user taps a button on the bottom
toolbar.
case .photoModal:
PhotosModal(card: $card)
Here you set up the photo button on the toolbar to display your photos modal view.
➤ Open SingleCardView.swift and pin the preview. In Live Preview, tap the Photos
button on the bottom toolbar.
468
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
switch selection {
default:
case .photoModal:
Button {
} label: {
PhotosModal(card: $card)
}
The resulting view from PhotosModal is the button you supply to PhotosPicker, so
this replaces the previous ToolbarButton.
BottomToolbar(
card: .constant(Card()),
modal: .constant(.stickerModal))
case .photoModal:
PhotosModal(card: $card)
BottomToolbar(
card: $card,
modal: $currentModal)
469
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
➤ Resume Live Preview on Single Card View, and select the Photos button on the
bottom toolbar.
This time you see the system photos picker. When you tap Cancel, the Photos modal
disappears. So far, when you select photos and tap Add, nothing happens. The
system retains the selection, however, as you’ll see if you return to the photos picker.
It’s not only photos that you might want to add to your app. You might want to be
able to copy and paste text, or even custom types, such as files created by another
app. You’ll also want to share your card with your friends, which means exporting
your card from your app. Transferable is a flexible protocol that allows you to
describe how to import and export any types.
470
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Some existing data types, such as Data, which is a string of bytes, already conform to
Transferable. When you add photos, these will be of type UIImage, which
unfortunately does not conform.
It’s easy to add conformance, and you’ll do that later in the chapter. For the moment,
though, to get you quickly adding photos, you’ll transfer the photos as Data.
Whenever selectedItems changes, you’ll print out each element in the array. After
you’ve processed each item, clear the array.
➤ Build and run the app in Simulator, choose a card, tap the Photos button on the
bottom toolbar and select the pink flowers photo and one other. Tap Add.
The details of each item selected print out in the debug console. Notice the
_supportedContentTypes. These are the supported content types for the item. The
pink flowers photo has two types: public.jpeg and public.heic. The other photo
has just one type: public.jpeg.
Console output
471
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Most apps have associated data types. For example, when you right-click a macOS
file and choose Open With, the menu presents you with all the apps associated with
that file’s data format. When you right-click a .png file, you might see a list like this:
There are many standard system UTIs which you can find at https://apple.co/
3xASdxD.
If you have a custom data format, you can create your own type in a UTType
extension:
extension UTType {
static var myType: UTType =
{ UTType(exportedAs: "com.kodeco.myType") }
}
public.data is a base type representing a stream of bytes. Using this type, you can
load the photos as a data stream and then convert the data to a UIImage.
472
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
You load the item as a Data type. result is of type Result<Success, Failure>.
Success contains the image data, and Failure contains a failure value.
For each item, you load the image on a background thread using Task {}.
switch result {
case .success(let data):
if let data,
let uiImage = UIImage(data: data) {
card.addElement(uiImage: uiImage)
}
case .failure(let failure):
fatalError("Image transfer failed: \(failure)")
}
If the result succeeds, use the data to create a UIImage and add that image to the
card’s element array. If the result fails, produce a fatal error.
Note: At the time of writing, the simulator pink flowers photo in HEIC format
causes an error. This appears to be an Apple bug, but it does give you the
chance to make sure that your PhotosPicker error checking works. When you
run your app in Simulator and choose that photo, in the console, you should
see Fatal error: Image transfer failed: with information about the failure. HEIC
format files will work on a device with your own photos.
473
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
➤ Live Preview Single Card View and add some photos (not the pink flowers) to the
card.
474
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
The photos library is not the only place you can access photos. Modern apps should
accept photos and images that you drag from any other app.
First set up Simulator so that you’ll be able to do the drag and drop.
➤ Build and run your app on an iPad simulator and turn the iPad to landscape mode.
You can use the icon on the top bar, or use Command-Right Arrow.
➤ Tap the three dots at the top of Simulator’s screen and choose Split View.
Split View
475
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Drag a giraffe
Cards is not ready to receive a drop yet, so nothing happens. If the drop area were
able to receive an item, you would get a plus sign next to the image.
476
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Just as you did with your photos, you receive the dragged image or images as an array
of data streams. You create a UIImage from the data and add the image to the card’s
array of elements. You return whether the operation was successful.
Currently you don’t use location, so any dropped items are added to the center of
the card. To calculate the offset for the element’s transform, you’ll need to convert
the location point on the card to an offset from the center of the card. This involves
knowing the screen size of the card. You’ll revisit this problem in Chapter 20,
“Delightful UX — Layout”.
As you drag over the drop area, a plus sign will appear on the drop pile, indicating
that the drop destination is allowable for this data type. When you drop the photo,
it’s added to the card at the center.
Drop is active
In Simulator, to select multiple images in Safari at the same time, pick up an image
and start dragging it. That small drag is important — you won’t be able to multiple
select without it. Then hold down Control. Release the click and then Control. A
gray dot appears on the image representing your finger on a device. Click other
images to add them to the drag pile.
477
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
A tower of giraffes
import SwiftUI
// 1
extension UIImage: Transferable {
// 2
public static var transferRepresentation: some
TransferRepresentation {
// 3
DataRepresentation(importedContentType: .image) { image in
// 4
UIImage(data: image) ?? errorImage
}
}
478
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
3. When the imported UTType is an image, you’ll import the image as data and
construct a UIImage from that data. DataRepresentation expects the return
type to be the same type as Self, in this case, a UIImage.
4. Create the UIImage. If the operation fails, create an error image using the image
in the asset catalog.
You can now use UIImage.self as a Transferable type. The data representation in
the UIImage extension reads in the data and converts it to a UIImage, which you can
add to the the card directly.
479
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
➤ First, open Card.swift in the Model group and add a new method to Card that
adds text to the card elements.
You add a new drop destination modifier that will take in a String and add a text
element to the card.
➤ Build and run the app in an iPad simulator with Safari open in Split View. In
Safari, highlight a small amount of text and drag it on to your card.
Cards will add the text element to the center of the card.
Dropped text
480
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
So far, so good. You can drag an image to your card, and you can drag some text to
your card. The only problem is that you have to change the code each time.
You can overcome this problem with a custom transfer type that conforms to
Transferable.
➤ In the Model group, create a new Swift file named CustomTransfer.swift and
replace the code with:
import SwiftUI
CustomTransfer contains two properties, one for text and one for image. The
transfer representation takes into account the image type and the text type and fills
in the relevant property. For images, the data representation is the same as you did
for UIImage, and for text, you create a String from the data. UTF8 is the most
common Unicode encoding system.
Once CustomTransfer has created either an image or some text from the transferred
data, you’ll add an element to the card.
481
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Using your custom Transferable structure, you can add both text and image
elements to the card appropriately.
When you drop the transferred items, the view will print the location to the debug
console for later use. A new task will start that adds the elements to the card.
Once you’ve set up your CustomTransfer, as well as dragging photos from another
app, you can instead copy them and paste them on your card.
482
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
SwiftUI provides PasteButton for this. It doesn’t allow a lot of customization, and
you can’t add it to a Menu, but it is easy to implement.
➤ Open CardToolbar.swift.
ToolbarItem(placement: .navigationBarLeading) {
PasteButton(payloadType: CustomTransfer.self) { items in
Task {
card.addElements(from: items)
}
}
}
You’ve now implemented paste in your app. I told you it was easy! PasteButton will
be disabled unless it detects a CustomTransfer item. Then when you tap Paste, the
items will be added to your card in the same way as the drop.
➤ Build and run the app on an iPad simulator with Safari in split screen and choose a
card. Long press an image in Safari, and choose Copy.
The image is now in the pasteboard (also known as clipboard) ready to paste. Copy-
and-paste will also work with text.
Copy an image
483
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
➤ Tap Paste a few times to add copies of the image to your card.
.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
This removes the word “Paste” leaving only the icon and gives the button a capsule
shape. The paste button is now a little less obtrusive, but the design still doesn’t fit
well.
484
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
As you build up your app, you’ll probably want to add a few extra buttons for
operations that don’t really need to be always on screen. You can add a pop-up menu
for all these operations. Unfortunately, PasteButton won’t work on this menu, so
you’ll use a Button which updates UIKit’s UIPasteboard.
ToolbarItem(placement: .navigationBarTrailing) {
menu
}
➤ This toolbar item will be more complicated than the previous one, so add a new
property to CardToolbar:
1. You add a Menu to the top toolbar just to the left of the Done button. A Menu is a
list of buttons. For this app, you’ll only have one button, but you can very easily
add more under the Paste button.
2. You only want the paste button to be enabled when there is something to paste,
so you check hasImages and hasStrings. If both are false, you disable the
button.
485
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
if UIPasteboard.general.hasImages {
if let images = UIPasteboard.general.images {
for image in images {
card.addElement(uiImage: image)
}
}
} else if UIPasteboard.general.hasStrings {
if let strings = UIPasteboard.general.strings {
for text in strings {
card.addElement(text: TextElement(text: text))
}
}
}
You can check whether the pasteboard contains images or strings. Apple’s
documentation states not to test images or strings to see whether they contain
data, but to check hasImages and hasStrings.
➤ Build and run your app on iPad with Safari in split screen. Then, try copying and
pasting text and images.
When pasting from another app, iOS will ask permission whether to paste.
Allow paste
486
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Note: Apple’s Universal Clipboard is very powerful. For example, if you run
Cards on a device, you can select and copy photos in the macOS Photos app
and paste them into Cards on the device.
Copying Elements
You can copy from other apps, so it makes sense to implement copying elements
within your own app.
➤ Open CardDetailView.swift.
import SwiftUI
The context menu will need access to the current card and current element. Creating
a view modifier should be familiar to you from Chapter 14, “Gestures”, when you
created resizableView().
487
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
.contextMenu {
Button {
if let element = element as? TextElement {
UIPasteboard.general.string = element.text
} else if let element = element as? ImageElement,
let image = element.uiImage {
UIPasteboard.general.image = image
}
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
The context menu will pop up when you perform a long press on a card element.
When you tap Copy, the pasteboard will record the text or image element details
ready for pasting elsewhere.
Your modifier is ready to use, but, as you did with ResizableView, you should make
it easier to use.
extension View {
func elementContextMenu(
card: Binding<Card>,
element: Binding<CardElement>
) -> some View {
modifier(ElementContextMenu(
card: card,
element: element))
}
}
This extension to View simply calls your new modifier with a card and an element
value.
.elementContextMenu(
card: $card,
element: $element)
488
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
You have now added a new context menu to each element that you can access with a
long press on that element. You must place the modifier before the following ones so
that the context menu appears in the correct place on the screen.
➤ Build and run the app and experiment with copying elements and pasting them in
other cards, or even in other apps. Even when copying the element in Simulator, you
can paste it into another macOS app.
Deletion
You can easily add elements to your cards by copying and pasting them in, but if you
make a mistake, you aren’t able to remove the element. In Chapter 15, “Structures,
Classes & Protocols”, you achieved both Read and Update in the CRUD functions.
Next, you’ll take on Deletion.
You’ll add an entry to the context menu. When you tap the menu item, your app will
remove the selected card element from the card’s array.
489
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Here you retrieve the index of the card element. You then remove the element from
the array using the index.
Button(role: .destructive) {
card.remove(element)
} label: {
Label("Delete", systemImage: "trash")
}
Your delete button should be highlighted as dangerous, and that’s what the
destructive role does for you. The menu item will be in red.
➤ Live Preview Single Card View, add a photo to the card, and then, long press the
photo.
➤ Tap Delete to delete the element, or tap away from the menu if you decide not to
delete it.
Delete an element
In summary, when you delete the element, you delete it from card.elements. card
is bound to cards in the data store, and cards is a published property. When cards
changes, all views containing cards will redisplay their content.
490
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Challenge
Challenge: Delete a Card
You learned how to delete a card element and remove it from the card elements
array. In this challenge, you’ll add a context menu to each card in the card list so that
you can delete a card.
2. In CardsListView, add a new context menu to a card with a delete option that
calls your new method to remove the card.
Delete a card
You’ll find the solution to this challenge in the challenge folder for this chapter.
491
SwiftUI Apprentice Chapter 17: Adding Photos to Your App
Key Points
• Instead of having to implement your own photos picker view, Apple provides the
PhotosUI framework with a PhotosPicker view. It’s an easy way to select photos
and videos from the photo library.
• Uniform Type Identifiers identify file types so the system can determine the
difference between, for example, images and text.
• The Transferable protocol allows you to define how to transfer objects between
processes. You use Transferable for drag and drop, pasting and sharing. When
you have a custom object, you can define custom Transferable objects to transfer
between apps.
• A Menu is a list of Buttons. Each Button can have a role. By making the role
destructive, the menu item will appear in red.
• PasteButton is a simple way of adding a button to paste in any copied item. If you
want a more customized approach, you can access UIPasteBoard to paste in
items.
• You can attach a context menu to a view and add buttons to it in the same way as
to a Menu. You access the context menu by a long press. SwiftUI brings the view to
the foreground and darkens the other views. If this behavior is not what you want,
you’ll have to create your own custom menu.
492
18 Chapter 18: Paths &
Custom Shapes
By Caroline Begbie
In this chapter, you’ll become adept at creating custom shapes with which you’ll
crop the photos. You’ll tap a photo on the card, which enables the Frames button.
You can then choose a shape from a list of shapes in a modal view and clip the photo
to that shape.
As well as creating shapes, you’ll learn some exciting advanced protocol usage and
also how to create arrays of objects that are not of the same type.
493
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Shapes
Skills you’ll learn in this section: predefined shapes
➤ In the Model group, create a new SwiftUI View file called Shapes.swift. This file
will hold all your custom shapes.
494
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
These are the five built-in shapes, which fill as much space as they can.
495
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Paths
Skills you’ll learn in this section: paths; lines; arcs; quadratic curves
This is the triangle shape you’ll draw first. You’ll create a path made up of lines that
go from point to point.
Triangle
Paths are simply abstract until you give them an outline stroke or a fill. SwiftUI
defaults to filling paths with the primary color, unless you specify otherwise.
496
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Shape has one required method which returns a Path. path(in:) receives a CGRect
containing the drawing canvas size in which to draw the path.
Lines
➤ Create a triangle with the same coordinates as in the diagram above. Add this to
path(in:) before return path:
// 1
path.move(to: CGPoint(x: 20, y: 30))
// 2
path.addLine(to: CGPoint(x: 130, y: 70))
path.addLine(to: CGPoint(x: 60, y: 140))
// 3
path.closeSubpath()
1. You create a new subpath by moving to a point. Paths can contain multiple
subpaths.
2. Add straight lines from the previous point. You can alternatively put the two
points in an array and use addLines(_:).
497
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Triangle Shape
Shapes fill as much space as they can. The filled path is using the fixed numbers from
path(in:). But Triangle itself is filling the whole yellow area. Your code only
replicates the triangle in the previous diagram when you add .frame(width: 150,
height: 150) to currentShape.
Fixed Triangle
If you want the triangle to retain its shape, but size itself to fill the available size, you
must use relative coordinates, rather than absolute values.
498
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Here you use addLines(_:) with an array of points to make up the triangle. You
replace the hard coded coordinates with relative ones that depend upon the width
and height. You can calculate these coordinates by dividing the hard coded
coordinate by the original frame size. For example, 20.0 / 150.0 comes out at
about 0.13.
currentShape
.aspectRatio(1, contentMode: .fit)
.background(Color.yellow)
You maintain the square aspect ratio, and the triangle will now resize to the
available space.
Resizable Triangle
499
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
.previewLayout(.sizeThatFits)
Now the preview will only show the part of Shapes that holds the triangle.
Resized preview
Arcs
Another useful path component is an arc.
Here you create a new shape in which you’ll describe a cone. To draw the cone, you’ll
draw an arc and two straight lines.
500
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
radius: radius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: true)
Here you set the center point to be in the middle of the given rectangle with the
radius set to the smaller of width or height.
The arc
Forget everything you thought you knew about the clockwise direction. In iOS,
angles always start at zero on the right hand side, and clockwise is reversed. So when
you go from a start angle of 0° to an end angle of 180° with clockwise set true, you
start at the right hand side and go anti-clockwise around the circle.
Describe an arc
501
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
This is for historical reasons. In macOS, the origin — that’s coordinate (0, 0) — is at
the bottom left, as in the standard Cartesian coordinate system. When iOS came out,
Apple flipped the iOS drawing coordinate system on the Y axis so that (0, 0) is at the
top left. However, much of the drawing code is based on the old macOS drawing
coordinate system.
➤ In Cone’s path(in:), add two straight lines to complete the cone before the
return:
You start the first line where the arc left off and end it at the middle bottom of the
available space. The second line ends at the middle of the right hand side.
Curves
As well as lines and arcs, you can add various other standard elements to a path, such
as rectangles and ellipses. With curves, you can create any custom shape you want.
502
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
The lens shape will consist of two quadratic curves, like an ellipse with a point at
each end.
If you have used vector drawing applications, you’ll have used control points to draw
curves. To create a quadratic curve in code, you set a start point, an end point and a
control point that defines where the curve goes.
Quadratic curve
The two mid points shown are calculated and define the curvature. It can take some
practice to work out the control point for the curve.
The first curve here is the same as in the diagram above, and the second curve
mirrors it.
503
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Lens shape
SwiftUI is currently filling the paths with a solid fill. You can specify the fill color or,
alternatively, you can assign a stroke, which outlines the shape.
.stroke(lineWidth: 5)
You can only use stroke(_:) on objects conforming to Shape, so you must place the
modifier directly after currentShape.
Stroke
504
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Stroke Style
When you define a stroke, instead of giving it a lineWidth, you can give it a
StrokeStyle instance.
For example:
currentShape
.stroke(style: StrokeStyle(dash: [30, 10]))
To form a dash, you create an array which defines the number of horizontal points of
the filled section followed by the number of horizontal points of the empty section.
The example above describes a dashed line where you have a 5 point vertical line,
followed by a 10 point space, followed by a one point vertical line, followed by a 5
point space.
This second example adds a dash phase, which moves the start of the dash to the
right by 15 points, so that the dash starts with the one point line.
Swift tip: You haven’t done much animation so far as you’ll cover this later in
Chapter 21, “Delightful UX — Final Touches”, but these dashed line parameters
are animatable, so you can easily achieve the “marching ants” marquee look.
505
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
You can choose to change how the ends of lines look with the lineCap parameter:
Line caps
lineCap: .square is similar to .butt, except that the ends protrude a bit further.
.stroke(
Color.primary,
style: StrokeStyle(lineWidth: 10, lineJoin: .round))
.padding()
Here you give the stroke an outline color and, using the lineJoin parameter, the two
sections of lens shape are now nicely rounded at each side:
Line join
You’ve now created a few shapes and feel free to experiment with more. The
challenge for this chapter suggests a few shapes for you to try.
Selecting an Element
Skills you’ll learn in this section: borders; clip shapes
As well as displaying a shape view, you can use a shape to clip another view. You’ll
list all your shapes in a modal so that the user can select a photo element on the card
and clip it to a chosen shape.
Before creating the modal, you’ll set up a property to hold the selected element. You
could pass this element around as a binding, but in this case you’ll add it to
CardStore as a published property. As you’ll see shortly, when this property
changes, all views affected by the property will redraw.
506
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
You’ll update this selected element when the user taps a card element.
When adding a selection property like this one, you should consider where best to
place it. When listing and selecting cards, you added selectedCard to
CardsListView. In that case, the selected card was only needed by one view, so it
was an easy decision. If your app gets more complex, you may choose to move that
selectedCard to CardStore as a published property, so that any view that uses
selectedCard will redraw when the property changes. selectedElement will be
used in several places, so it’s easier to place the property in CardStore.
.onTapGesture {
store.selectedElement = element
}
When the user taps this element, you save it as the selected element.
.onTapGesture {
store.selectedElement = nil
}
When the user taps the card background, the selection clears.
The user leaves the card by tapping Done, but you’ve defined the Done button in a
different file, and it’s a good idea to keep similar code in close proximity.
.onDisappear {
store.selectedElement = nil
}
When the user taps Done, CardDetailView disappears and performs this closure.
The user will want to know which element he’s selected, so you’ll add a border to the
element.
507
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
This allows you to determine if a particular element is the currently selected element
by comparing their ids.
.border(
Settings.borderColor,
width: isSelected(element) ? Settings.borderWidth : 0)
All views have a border, but if the element is not currently selected, the line width of
the border is 0.
➤ Test your selection and border in Live Preview. Tap the background to clear the
selection.
Border selection
508
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
You can only have one element selected at a time. When you update
store.selectedElement, this affects the border of all element views. Because
store.selectedElement is a published property, all views are redrawn with the
correct border.
➤ In the Card Modal Views group, create a new SwiftUI View file called
FrameModal.swift. This will be very similar to StickerModal.swift, but will load
your custom shapes instead of stickers into a grid.
First, set up an array of all your shapes for the modal to iterate through.
Open Shapes.swift and add a new enumeration to hold all the shapes:
enum Shapes {
}
Initially, you might think you can define the array in Shapes like this:
Associated Types
Skills you’ll learn in this section: protocols with associated types; type
erasure
509
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Earlier, you created the protocol CardElement. This doesn’t use an associated type,
and so you were able to set up an array of type CardElement. This is how you defined
CardElement:
protocol CardElement {
var id: UUID { get }
var transform: Transform { get set }
}
All of the property types in CardElement are existential types. That means they are
types in their own right and not generic. However, you might have a requirement for
id to be either a UUID or an Int or a String. In that case you can define
CardElement with a generic type of ID:
protocol CardElement {
associatedtype ID
var id: ID { get }
var transform: Transform { get set }
}
510
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
When you create a structure conforming to CardElement, you tell it what type ID
actually is. For example:
In this case, whereas the other CardElement ids are of type UUID, this id is of type
Int.
Once a protocol has an associated type, because it is now a generic, the protocol is
no longer an existential type. The protocol is constrained to using another type, and
the compiler doesn’t have any information about what type it might actually be. For
this reason, you can’t set up an array containing protocols with associated types,
such as View or Shape.
Going back to the code at the start of this section which doesn’t compile:
Even though Circle and Rectangle both conform to Shape, they are Shapes with
different associated types and, as such, you can’t put them both in the same Shape
array.
Type Erasure
You are able to place different Views in an array by converting the View type to
AnyView:
AnyView is a type-erased view. It takes in any type of view and passes back an
existential, non-generic type of AnyView.
511
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
This holds a type-erased list of all your defined shapes. When you create more
shapes, add them to this array.
512
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
}
}
.padding(5)
}
}
This is almost exactly the same code as you wrote for StickerModal. The exceptions
are:
1. Pass in an integer that will hold the index of the selected shape in the Shapes
array.
4. Fill the shape so that you have a touch area. If you don’t fill the shape, the tap
will only work on the stroke.
5. When the user taps the shape, update frameIndex and dismiss the modal.
Shapes Listing
513
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
You load the card store into the environment so you can access the selected element.
You also define the index of the frame shape that you’ll pass to FrameModal.
case .frameModal:
FrameModal(frameIndex: $frameIndex)
.onDisappear {
if let frameIndex {
card.update(
store.selectedElement,
frameIndex: frameIndex)
}
frameIndex = nil
}
Here you call the modal and update the card element with the frame index. As you
haven’t written update(_:frameIndex:) yet, you’ll get a compile error.
514
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
This will hold the element’s frame index. You add it solely to ImageElement because
the frame will clip only images, not text.
Here you pass in the element and the frame index. Because element is immutable
and you need to update its frameIndex, you create a new mutable copy and update
elements with this new instance.
.clipShape(Shapes.shapes[0])
515
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Clipped elements
Surprisingly, it’s not easy to add a conditional modifier in SwiftUI. You can show
Views conditionally using if {} else {}, and with simple conditions, you could
show the same view with and without a modifier. However, when you have multiple
modifiers on a view, this leads to heavily duplicated code.
In Chapter 9, “Refining Your App”, when you wanted to conditionally show a button
shape, you created a method with a ViewBuilder attribute, and that’s what you’ll do
here too.
516
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
➤ Open CardElementView.swift and at the end of the file, add a new extension:
// 1
private extension ImageElementView {
// 2
@ViewBuilder
func clip() -> some View {
// 3
if let frameIndex = element.frameIndex {
// 4
let shape = Shapes.shapes[frameIndex]
self
.clipShape(shape)
} else { self }
}
}
2. The ViewBuilder attribute allows you to build up views and combine them into
one. Check out Chapter 9, “Refining Your App” if you need a refresher on how
this works.
4. If there’s a value in frameIndex, clip the view with the element’s frame shape.
Otherwise, return the unmodified view.
ImageElementView(element: element)
.clip()
517
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
➤ Build and run the app and choose the first yellow card. Tap a giraffe and choose
Frames. Select a frame and the giraffe photo gets clipped to that shape. The
selection border is still rectangular, but you’ll fix that in the challenge at the end of
this chapter.
Clipped giraffe
When you tap the background near an unselected clipped image, but inside the area
of the original unclipped image, SwiftUI still thinks you’re tapping the image.
.contentShape(shape)
518
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
.environmentObject(CardStore())
In BottomToolbar, in body, you’ll duplicate the default button and use it for
frameModal.
➤ To reduce code duplication, extract the default button into a new method:
case .frameModal:
defaultButton(selection)
.disabled(
store.selectedElement == nil
|| !(store.selectedElement is ImageElement))
default:
defaultButton(selection)
519
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
By separating out frameModal, you can disable the button when there is no selected
element. It also makes no sense to be able select clip frames on a TextElement, so
you check that the selected element is an ImageElement.
➤ Build and run the app and check that you can still add frames to selected image
elements:
520
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Challenges
Challenge 1: Create new Shapes
Practice creating new shapes and place them in the frame picker modal. Here are
some suggestions:
2. In the new method, if the element is selected, when it is an image and has a
frame, replace the border modifier with an overlay of the stroked frame. If the
element is selected and doesn’t have a frame or is not an image, add a border
modifier as before.
521
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Check your changes out by running the app or by Live Previewing SingleCardView.
Clipped photos
522
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Key Points
• The Shape protocol provides an easy way to draw a 2D shape. There are some
built-in shapes, such as Rectangle and Circle, but you can create custom shapes
by providing a Path.
• Paths are the outline of the 2D shape, made up of lines and curves.
• A Shape fills by default with the primary color. You can override this with the
fill(_:style:) modifier to fill with a color or gradient. Instead of filling the
shape, you can stroke it with the stroke(_:lineWidth:) modifier to outline the
shape with a color or gradient.
• With the clipShape(_:style:) modifier, you can clip any view to a given shape.
• Associated types in a protocol make a protocol generic, making the code reusable.
Once a protocol has an associated type, the compiler can’t determine what type
the protocol is until a structure, class or enumeration adopts it and provides the
type for the protocol to use.
• Using type erasure, you can hide the type of an object. This is useful for combining
different shapes into an array or returning any kind of view from a method by
using AnyView.
• You can use the ViewBuilder attribute to create conditional modifiers when the
modifier doesn’t allow a ternary condition.
523
19 Chapter 19: Saving Files
By Caroline Begbie
You’ve set up most of your user interface, and it would be nice at this stage to have
the card data persist between app sessions. You can choose between a number of
different ways to save data.
You’ve already looked at UserDefaults and property list (plist) files in Section 1.
These are more suitable for simple data structures, whereas, when you save your
card, you’ll be saving images and sub-arrays of elements. While Core Data could
handle this, another way is to save the data to files using the JSON format. One
advantage of JSON is that you can easily examine the file in a text editor and check
that you’re saving everything correctly.
This chapter will cover saving JSON files to your app’s Documents folder by
encoding and decoding the JSON representation of your cards.
524
SwiftUI Apprentice Chapter 19: Saving Files
In the first challenge for this chapter, you’ll store the card’s background color.
ColorExtensions.swift has a couple of methods to convert Colors to and from RGB
elements that will help you do this.
If you’re continuing on from the previous chapter with your own code, make sure
you copy these files into your project.
Data store
When your app first starts, you’ll read in all the .rwcard files in the Documents
folder and show them in a scroll view. When the user taps a selected card, you’ll
process the card’s elements and load the relevant image files.
525
SwiftUI Apprentice Chapter 19: Saving Files
There are two ways you can proceed, and each has its pros and cons.
You can choose to save the card file every time you change anything, such as adding,
moving or deleting elements. This means that your data on disk is always up-to-date.
The downside is that your saving is spread out all over your app.
Alternatively, you could choose to save when you really need to:
1. When SingleCardView disappears, which happens when the user taps Done.
2. When the app becomes inactive through the user switching apps or an external
event such as a phone call.
The downside of this method is that if your app crashes before you’ve done the save,
then the last few changes the user made might not be recorded. You’ll also need to
remember when testing that the app doesn’t save in the simulator until you press
Done.
In this app, you’ll choose a hybrid approach. You’ll perform the first method of
saving whenever you create or delete card data. This is primarily because of saving
the image element’s UIImage. You’ll save the UIImage when you choose it from the
Photos or Stickers modal, and you’ll store the file id in the ImageElement. To
maintain data integrity, it’s a good idea to store the ImageElement at the same time
as the UIImage.
However, moving and resizing elements happens regularly, and saving every time
can be quite inefficient. To save the transform data, you’ll choose the second
method: saving when the user taps Done or leaves the app.
func save() {
print("Saving data")
}
526
SwiftUI Apprentice Chapter 19: Saving Files
You’ll come back to this method to perform the saving later in this chapter.
.onDisappear {
card.save()
}
➤ Build and run the app, tap a card, then tap Done. You’ll see “Saving data” appear
in the console.
Saving data
527
SwiftUI Apprentice Chapter 19: Saving Files
➤ Build and run the app, tap a card to open it and exit your app by swiping up from
the bottom. You’ll see the console message “Saving data”.
Saving data
528
SwiftUI Apprentice Chapter 19: Saving Files
➤ Return to the app in the simulator. It will resume inside the card where you left it.
There is no way to simulate a phone call on the simulator, but you can activate Siri to
test external events. Choose Device ➤ Siri and, once again, you’ll see the console
message “Saving data”.
You’ve now implemented the skeleton for the saving part of your app. The rest of the
chapter will take you through encoding and decoding data so you can perform
save().
JSON Files
Skills you’ll learn in this section: the JSON format
JSON is an acronym for JavaScript Object Notation. JSON data is formatted like this:
{
"identifier1": [data1, data2, data3],
"identifier2": data4
}
To explore how easy it is save simple data to JSON files, you’ll create a temporary
structure and save it.
Codable
Skills you’ll learn in this section: Encodable; Decodable
The Codable protocol is a type alias for Decodable & Encodable. When you
conform your structures to Codable, you conform to both these protocols. As its
name suggests, you use Codable to encode and decode data to and from external
files.
529
SwiftUI Apprentice Chapter 19: Saving Files
➤ Open CardsApp.swift and add this code to the end of the file:
After you’ve seen how Codable works, you’ll delete this code and apply your
knowledge to the more complex data in your app.
This structure contains straightforward data of types that JSON supports — an array
of Strings and an Int. Team conforms to Codable and makes Team a type that can
encode and decode itself.
Encoding
➤ In Team, create a new method:
1. Initialize the JSON encoder. prettyPrinted means that the encoded data will be
easier for you to read.
530
SwiftUI Apprentice Chapter 19: Saving Files
init() {
Team.save()
}
This will save the team data at the very start of the app so that you can examine it.
.onAppear {
print(URL.documentsDirectory)
}
You print out the URL of the Documents folder so you can find the file you’ve saved.
➤ Build and run the app. Highlight the Documents URL that shows up in the debug
console, then right-click it and choose Services ➤ Show in Finder. Drag the parent
folder to your Favorites sidebar as you’ll be visiting this folder often while you’re
testing.
Team data
➤ In Finder, right-click TeamData and open the file in TextEdit:
{
"names" : [
"Richard",
"Libranner",
"Caroline",
"Audrey",
"Sandra"
],
"count" : 5
}
This is your structure data stored in JSON format. The identifiers are the names you
used in the structure. As you can see, using Codable, it’s easy to store data.
531
SwiftUI Apprentice Chapter 19: Saving Files
Decoding
Reading the data back in is just as easy.
4. Decode the data into an instance of Team and print it to the console so that you
can see what you’ve decoded.
init() {
Team.load()
}
532
SwiftUI Apprentice Chapter 19: Saving Files
➤ Build and run, and see the new instance of Team loaded from TeamData printed
out in the console before the Documents URL.
Data types that you want to store must conform to Codable. If you check the
developer documentation for the properties contained by Team, which are String
and Int, you’ll see they both conform to Decodable and Encodable.
Custom types which store only Codable types present no problem. But how about
one of your custom types that contain types that do not conform to Codable?
You get a compile error: “Type Transform does not conform to protocol
Decodable”.
Transform contains two data types: CGSize and Angle. When you check the
documentation, you’ll find that CGSize conforms to Encodable and Decodable,
whereas Angle does not.
When you conform your custom type to Codable, there are two required methods:
init(from:) and encode(to:).
533
SwiftUI Apprentice Chapter 19: Saving Files
When all the types in your custom type conform to Codable, then all you have to do
is add Codable conformance to your custom type, and Codable will automatically
synthesize (create) the initializer and encoder methods.
import SwiftUI
You conform Angle to Codable and provide the two required methods. Because all
the types used by Transform are now Codable, your code will now compile. However,
the encoder and decoder methods you just created aren’t doing anything useful.
You’ll have to tell the coders how to encode and decode every property that you want
saved and loaded.
534
SwiftUI Apprentice Chapter 19: Saving Files
To do this, you create an enumeration that conforms to CodingKey, listing all the
properties you want saved.
You list only the properties that you want to save and restore. radians is another
Angle property, but Angle can construct that internally from degrees, so you don’t
need to store it.
You create an encoder container using CodingKeys. Then you encode degrees,
which is of type Double. This is a Codable type, so the container can encode it.
Decoding is similar.
You create a decoder container to decode the data. As degrees is a Double, you
decode a Double type. Then, you can initialize the Angle from the decoded degrees.
With Angle taken care of, and CGSize already conforming to Codable, Transform
will now be able to synthesize the encoding and decoding methods and encode and
decode itself, so your app will now compile.
You’re eventually going to save a Card, so all types in the data hierarchy will need to
to be Codable. Going up from Transform in your data structure hierarchy, the next
structure that you’ll tackle is ImageElement.
535
SwiftUI Apprentice Chapter 19: Saving Files
Encoding ImageElement
➤ Open CardElement.swift and take a look at ImageElement.
When saving an image element, you’ll save transform, uiImage and frameIndex.
You don’t need to save the UUID, as it will get reconstructed when you initialize the
element. transform and frameIndex conform to Codable, however UIImage does
not.
You could save out the UIImage data into the card data, but it’s good practice to
record binary files separately and save the binary file name with the card data.
This will hold the name of the saved image file, which will be a UUID string.
1. You now save the UIImage to a file using the code provided in
UIImageExtensions.swift. uiImage.save() saves the PNG data to disk and
returns a UUID string as the filename. Before saving, save() resizes large images,
as you don’t need to store the full resolution for the card.
2. You create the new element with the string filename and the original uiImage.
You’ll also need to remove the image file from disk when the user deletes the
element.
536
SwiftUI Apprentice Chapter 19: Saving Files
You check that the element is an ImageElement and use the method provided in
UIImageExtensions.swift to remove the file from disk.
When adding a second initializer to the main definition of a structure, you lose the
default initializer and have to recreate it yourself. However, adding initializers to
extensions doesn’t have this effect. When you conform ImageElement to Codable,
you provide the decoding initializer init(from:). By adding the initializer to this
extension, you keep both the default initializer and the new decoding one.
537
SwiftUI Apprentice Chapter 19: Saving Files
uiImage = UIImage.errorImage
}
}
1. Decode the transform and frame index. They are Codable, so they take care of
themselves.
4. If there’s an error loading the image, use the error image in Assets.xcassets.
Here you’re encoding the transform, the frame index and the image filename.
For Card, you’ll save the id. This will be the name of the JSON file that you’ll store all
the data in, so it’s important to keep track of the id to ensure data integrity. You’ll
store the background color in the first challenge at the end of the chapter. You’ll also
store image elements and text elements in two separate arrays.
538
SwiftUI Apprentice Chapter 19: Saving Files
1. Decode the saved id string and restore id from the UUID string.
2. Load the array of image elements. You use the += operator to add to any elements
that may already be there, just in case you load the text elements first. You’ll load
the text elements in the challenge at the end of the chapter.
var id = UUID()
Here you encode the id as a UUID string. You also extract all the image elements
from elements using compactMap(_:)
539
SwiftUI Apprentice Chapter 19: Saving Files
When code is more complex than the above, you can replace the closure with:
func save() {
do {
// 1
let encoder = JSONEncoder()
// 2
let data = try encoder.encode(self)
// 3
let filename = "\(id).rwcard"
let url = URL.documentsDirectory
.appendingPathComponent(filename)
// 4
try data.write(to: url)
} catch {
print(error.localizedDescription)
}
}
540
SwiftUI Apprentice Chapter 19: Saving Files
2. Set up a Data property. This is a buffer that will hold any kind of byte data and is
what you will write to disk. Fill the data buffer with the encoded Card.
➤ Add:
save()
• remove(_:)
• addElement(uiImage:)
These are the methods that update the image file, so you do an additional save to
protect data integrity. You already call save() when the user presses the Done
button and, also, when the app scene phase changes.
➤ Open the Documents folder in Finder. The folder path prints out in the console,
but you should have the folder in your Favorites sidebar.
➤ In Simulator, choose the yellow card and add a new photo. (Don’t use the pink
flowers as currently that file format does not work.) When the card adds the new
element, it saves the photo to a PNG file and itself to a file with the .rwcard
extension. In Finder, open the .rwcard file in TextEdit — you should just be able to
double click it to open it.
{"id":"9E91BACF-8ABB-47AA-8137-80FD5FDB7F9B","imageElements":
[{"frameIndex":null,"imageFilename":null,"transform":{"offset":
[27,-140],"size":[250,180],"rotation":{"degrees":0}}},
{"frameIndex":null,"imageFilename":null,"transform":{"offset":
[-80,25],"size":[380,270],"rotation":{"degrees":0}}},
{"frameIndex":null,"imageFilename":null,"transform":{"offset":
[80,205],"size":[250,180],"rotation":{"degrees":0}}},
{"frameIndex":null,"imageFilename":"010050AF-A949-4DA4-9284-
CF998EF6885E","transform":{"offset":[0,0],"size":
[250,180],"rotation":{"degrees":0}}}]}
541
SwiftUI Apprentice Chapter 19: Saving Files
You’ll see something like the above. This is the JSON format as described earlier. You
can see that you’re saving the card id, which matches the filename and, also, an
array of four imageElements. The first three elements will have null in the filename
as they were provided by the preview data and never saved to a file. The last element
will contain the name of the saved image file in imageFilename.
If you want to make the output more human readable, in save(), after initializing
encoder, you can add:
encoder.outputFormatting = .prettyPrinted
Loading Cards
Skills you’ll learn in this section: file enumeration
Now that you’ve saved a card, you’ll start the app by loading them.
File Enumeration
To list the cards, you’ll iterate through all the files with an extension of .rwcard and
load them into the cards array.
➤ Open CardStore.swift and create a new extension with the method to load the
files:
extension CardStore {
// 1
func load() -> [Card] {
var cards: [Card] = []
// 2
let path = URL.documentsDirectory.path
guard
let enumerator = FileManager.default
.enumerator(atPath: path),
let files = enumerator.allObjects as? [String]
else { return cards }
// 3
let cardFiles = files.filter { $0.contains(".rwcard") }
for cardFile in cardFiles {
do {
// 4
let path = path + "/" + cardFile
542
SwiftUI Apprentice Chapter 19: Saving Files
let data =
try Data(contentsOf: URL(fileURLWithPath: path))
// 5
let decoder = JSONDecoder()
let card = try decoder.decode(Card.self, from: data)
cards.append(card)
} catch {
print("Error: ", error.localizedDescription)
}
}
return cards
}
}
1. You’ll return an array of Cards from load(). These will be all the cards in the
Documents folder.
2. Set up the path for the Documents folder and enumerate all the files and folders
inside this folder.
3. Filter the files so that you only hold files with the .rwcard extension. These are
the Card files.
5. Decode each Card from the Data variable. You’ve done all the hard work of
making all the properties used by Card and its subtypes Codable, so you can then
simply add the decoded Card to the array you’re building.
Instead of using the default data, you can choose to load the cards from disk.
➤ Build and run the app and check out your saved data.
543
SwiftUI Apprentice Chapter 19: Saving Files
544
SwiftUI Apprentice Chapter 19: Saving Files
You create a new card with a random background color, add it to the array of cards
and save it to disk.
➤ At the end of the VStack, so that it shows up under list, add the new button:
Button("Add") {
selectedCard = store.addCard()
}
When you tap the Add button, you call your new addCard() method in store. This
adds a new Card to the store’s cards array and saves the card file to disk.
➤ Open your app’s Documents folder in Finder and remove all the files from the
folder. This will reset your app’s data.
No app data
545
SwiftUI Apprentice Chapter 19: Saving Files
➤ Tap Add to add a new card. The background color is random and won’t be saved
until you complete the challenge at the end of the chapter.
A new .rwcard file will appear in your app’s Documents folder. Add a couple of
photos and stickers to the card. These will get saved right away. Move them around
and tap Done to save the transforms. Your new card will show in the list of cards.
When you re-run your app, any cards will appear just as you created them (but with a
yellow background).
546
SwiftUI Apprentice Chapter 19: Saving Files
Challenges
Challenge 1: Save the Background Color
One of the properties not being stored is the card’s background color, and your first
challenge is to fix this. Instead of making Color Codable, you’ll store the color data
in CGFloats. In ColorExtensions.swift, there are two methods to help you:
In Card.swift, encode and decode the background color using these two methods.
Before testing your solution, remove all files from the app’s Documents folder.
When you change the format of the file, it becomes unreadable. When adding
properties to files in an app that you’ve already released, you would have to take this
into account, as you wouldn’t want to lose your users’ data. Generally you’d store a
version number in your files and have a startup method that does an upgrade of files
if the data is an older version.
547
SwiftUI Apprentice Chapter 19: Saving Files
1. Create a new SwiftUI View file for your text entry modal. You will need to hold a
TextElement binding property sent from CardToolbar to hold the text data
temporarily, just as you’ve done for your other picker modals with frameIndex
and stickerImage. This time, though, in CardToolbar, instantiate the state
property and don’t make textElement an optional. You can check whether text is
empty with if textElement.text.isEmpty.
2. In your new modal view file, add an environment dismiss property as you did for
your other modals and replace body contents with:
let onCommit = {
dismiss()
}
TextField(
"Enter text", text: $textElement.text, onCommit: onCommit)
.padding(20)
The text field will show a placeholder and update the text String with the user’s
input. When the user presses Return, the modal will close.
548
SwiftUI Apprentice Chapter 19: Saving Files
4. Make TextElement Codable so that you save and restore the text with the card.
5. In Card’s Codable extension, make sure that you encode and decode the text
elements with the image elements.
When you finish this challenge, give yourself a big pat on the back, as you’ve now
created an app that has a complex UI and persists data each time you run the app.
This is the meat and vegetables of app development. The following chapters cover
making your app look gorgeous and round off the meal with an exotic dessert.
549
SwiftUI Apprentice Chapter 19: Saving Files
Key Points
• Saving data is the most important feature of an app. Almost all apps save some
kind of data, and you should ensure that you save it reliably and consistently.
Make it as flexible as you can, so you can add more features to your app later.
• ScenePhase is useful to determine what state your app is in. Don’t try doing
extensive operations when your app is inactive or in the background as the
operating system can kill your app at any time if it needs the memory.
• JSON format is a standard for transmitting data over the internet. It’s easy to read
and, when you provide encoders and decoders, you can store almost anything in a
JSON file.
• Codable encompasses both decoding and encoding. You can extend this task and
format your data any way you like.
550
20 Chapter 20: Delightful UX
— Layout
By Caroline Begbie
With the functionality completed and your app working so well, it’s time to make the
UI look and feel delightful. Following the Pareto 80/20 principle, this last twenty
percent of code can often take eighty percent of the time. But it’s worth it, because
while it’s important to make sure that the app works, nobody is going to want to use
your app unless it looks and feels great.
551
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
• The asset catalog has more pleasing random colors to use for backgrounds, as well
as other colors that you’ll use in these last chapters. ColorExtensions.swift now
uses these colors.
• ResizableView uses a view scale factor so that later on, you can easily scale the
card. The default scale is 1, so you won’t notice it to start with.
• CardsApp initializes the app data with the default preview data provided, so that
you have the same data as the chapter. Remember to change to @StateObject
var store = CardStore() in CardsApp.swift when you want to start saving
your own cards again.
• Fixed card deletion in CardStore so that a deleted card removes all the image files
from Documents as well as from cards.
View Hierarchy
552
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
As you can see, it’s very modular. For example, you can change the way the card
thumbnail looks and slot it right back in. You can easily add buttons to the toolbar
and add a corresponding modal.
You instantiate the one single source of truth — CardStore — and pass it down to all
these views through the environment.
App Design
When there are no cards, the user will see a large add button. There will also be a
wide Create New button at the bottom. This is the design that you’ll attempt to
duplicate.
553
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
This will delete all the data you have so far created for the app. For the moment,
you’ll use the default data provided with the app.
.background(
Color("background")
.ignoresSafeArea())
This will use a color from the asset catalog named background for the background
color. This is defined as light gray for light appearance and dark gray for dark
appearance. By using default parameters for ignoresSafeArea(_:edges:), you
ensure the background covers all the screen.
➤ Preview the view. In this image, the background color is pink for clarity; yours will
be light gray.
554
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Layout
Skills you’ll learn in this section: control view layout
It’s time to take a deeper look at how SwiftUI handles view layout.Most of the time,
SwiftUI views lay themselves out and look great, and you don’t have to think about
the layout at all. But then comes the time where you want exact positioning, or a
view isn’t behaving the way that you thought it would, and you might start fighting
the system. Once you understand layout and treat it logically, then it all becomes
much easier.
Layout starts from the top of the view hierarchy. The parent view tells its children, “I
propose this size”. Each child then takes as much room as it needs within the
parent’s available space and tells the parent “I only need this size”. This continues all
the way down the view hierarchy. The parent then resizes itself to the size of its child
views.
➤ In the canvas, switch from Live to Selectable, to see the correct view preview size.
Selectable
➤ In LayoutView, add a new modifier to Text:
.background(Color.red)
555
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
In the canvas, the red color shows how much space the Text view takes up on screen.
LayoutView has a fixed size of 500 by 300 points. Text takes up the amount of space
needed for the letters in the assigned font size. Color is a bit different. It’s a late
binding token, which means that the size is assigned at the last moment.
556
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Here you create a horizontal stack with two Text views. The second Text has
padding.
LayoutView still has the fixed size of 500 by 300 points. HStack presents 500 by 300
points to its children. The first Text returns the space it needs, but the second text
has a padding modifier, so returns its space plus the padding. HStack then takes up
only the space required by its two child views plus HStack’s default padding between
the two child views. HStack’s gray background color fills out the space taken up by
HStack underneath the two Text views.
Every time you add a modifier, you create a new layer in the view hierarchy. But don’t
worry about the efficiency of this — SwiftUI views are lightweight and adding new
views is incredibly fast.
When you want to lay out views relative to parent view sizes, you can specify
minimum and maximum widths and heights using
frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:al
ignment:).
557
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
.frame(maxWidth: .infinity)
The HStack now tells its parent that it wants the maximum available width, so
HStack, with its gray color, expands to the whole width of the view.
Maximum width
Remember your earlier problem with the background color only taking up the width
of the ScrollView? Specifying a frame with maxWidth and maxHeight of infinity
would be one way of filling up the entire available background.
Note: If you want to visualize how much space views take up, try
adding .background(Color.red.clipped()) as a modifier to the various
views. You clip the color, as the background can sometimes render outside the
view frame.
Lazy views fill the vertical or horizontal space, depending on the type of view, of the
top level view. These views are finalized late, as their content is only loaded when it
is necessary.
558
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
LazyHStack
Later in the chapter, you’ll explore GeometryReader, which takes up the entire
available space of its parent and returns the size in points. Use GeometryReader as a
last resort as there are usually other ways to achieve a fluid, animatable layout.
Instead of showing one column of scrolling cards, you’ll add a LazyVGrid to show
the cards in multiple columns. This should be adaptive depending on the device’s
current display width. The LazyVGrid expands horizontally to fit the parent’s size,
so you’ll coincidentally solve the problem of the background color that you had
earlier.
559
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
This returns an array of GridItem — in this case, with one element — you can use
this to tell the LazyVGrid the size and position of each row. This GridItem is
adaptive, which means the grid will fit as many items as possible with the minimum
size provided.
.padding(.top, 20)
In Live Preview, the background color now fills the entire screen. Check out the
orientation variants to see how the number of columns changes.
Orientation variants
560
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
However, when you change the layout to split screen, the thumbnail should size
smaller. You’ll test for the size of the device by using the compact or regular layout.
Currently, you set the size of the thumbnail in CardThumbnail, but since the number
of columns in CardsListView depends on the size of the thumbnail, you’ll size the
thumbnail in CardsListView.
These environment properties contain whether the size class is compact or regular
so that you’ll know how much space you have available.
If both size classes are regular, you can show a larger size thumbnail.
GridItem(.adaptive(
minimum: thumbnailSize.width))
.frame(
width: thumbnailSize.width,
height: thumbnailSize.height)
You use your new conditional size for the thumbnail and for the column layout.
➤ Change the run destination to iPad and build and run the app. On iPad with split
screen, you can check both compact and regular sizes.
561
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Add a split screen with Safari and change the size of the split screen. Check out
the changing size of the thumbnails.
1. Create a simple button using a Label format so that you can specify a system
image. When tapped, you create a new card and assign it to selectedCard. When
selectedCard changes, SingleCardView will show.
2. The button stretches all the way across the screen, less the padding.
3. The background color is in the asset catalog. You’ll customize the button text
color shortly.
562
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
createButton
Create button
The button code has a “gotcha”. Although the button frame extends all the way
across the screen, only the text is tappable.
Button {
selectedCard = store.addCard()
} label: {
Label("Create New", systemImage: "plus")
.frame(maxWidth: .infinity)
}
...
The button looks the same but is tappable all the way across.
563
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
card.backgroundColor
.cornerRadius(10)
This changes the corner radius to match the design, but otherwise produces the same
result as before.
.shadow(
color: Color("shadow-color"),
radius: 3,
x: 0.0,
y: 0.0)
Here you add a shadow with your specified color and a radius of 3. With the x and y
positions both being zero, the shadow will be three points all around the view.
Outline Colors
564
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
This is a very subtle outline color, just to raise up the cards from the background
slightly, but if your designer tells you to add it, trust the designer. :]
Color(UIColor.systemBackground)
In the Variants preview, the card color is now the same as the screen’s background
color and you’ll be able to see the shadow.
card.backgroundColor
565
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
This creates a new temporary card with a plus symbol on it. With the device in Dark
Mode, the card should be black, and in Light Mode, the card should be white.
Color.primary gives black in Light Mode, and sometimes you can use
colorInvert() for the inverse. However, this results in some View rather than the
Color you need here. UIKit provides a systemBackground color, so you can use that
instead.
Use Spacers to center the card in the view, keeping createButton at the foot of the
screen.
Group {
if store.cards.isEmpty {
initialView
} else {
list
}
}
CardStore(defaultData: false)
566
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Your new card shows up in place of the ScrollView, and you can either use the
Create New button or this card to create a new card.
CardStore(defaultData: true)
567
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
AccentColor is automatically created when you create a new project using the App
template.
➤ Change the color to black for Any Appearance and white for Dark Appearance.
The Create button text is now black and doesn’t show on the black bar. Black is a
great color for buttons the card detail view, but not so great for this button.
Black text
➤ In createButton, add a new modifier after background(Color("barColor"):
.accentColor(.white)
As the button is dark in both light and dark appearances, you set the button’s accent
color to always be white, overriding the app’s default color.
568
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Accent color
Throughout the app, text takes on AccentColor as defined in Assets.xcassets,
except for where you specify accentColor(_:) on specific views.
Currently a card takes up the full size of the screen, less the top and bottom safe
areas, no matter what device or orientation you’re using. This obviously doesn’t work
when you’ve created a portrait card and then turn the device to landscape.
You’re going to create cards with a fixed size of 1300 by 2000. The entire card will be
visible at one time, no matter the orientation, and you’ll calculate the appropriate
size of the card view using a geometry reader proxy size.
569
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
GeometryReader
GeometryReader is a container view that takes up the entire available space and
returns its preferred size in points. Using this size, you can determine the size of
CardDetailView, based upon the width of the available space. Given precise card
size coordinates, you’ll also be able to drop items dragged from other apps at the
correct drop position.
GeometryReader { proxy in
HStack {
...
}
.background(Color.gray)
}
.background(Color.yellow)
GeometryReader takes up the size of the parent, in this case the whole 500 x 300
point view. It returns a value of type GeometryProxy, which includes a size property
so that you can find out exactly the size of the view. You can then lay out child views
using this size.
GeometryReader
Notice that GeometryReader changes alignment behavior. Instead of HStack being
centered in its parent view, it is now aligned to the top left of its parent view. You’ll
discover more about alignment later in this chapter.
570
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
To center HStack, you calculate the leading padding, using the geometry proxy
width.
GeometryProxy size
Notice the order of the modifiers. If you change the order of any one of these, you’ll
get a different result. Before filling with color, you must set the size of the view. If
you calculate the padding before filling with gray, then you’ll center the text views
but not the background gray color.
You can now calculate the frame of CardDetailView using the geometry reader
proxy size.
.frame(
width: Settings.cardSize.width,
height: Settings.cardSize.height)
.scaleEffect(0.8)
571
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
You set the card frame to the final card size and scale it by 80%.
572
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
You have to take into account the size of CardDetailView on any size device, so it’s
easier to convert the frame to the correct size than use scaleEffect(_:anchor:).
However, this does mean that in views further down the view hierarchy, you’ll have
to take into account how much CardDetailView is scaled.
These methods calculate the size and scale of a view with the correct aspect ratio
using a given size. This size comes from the view’s’ GeometryReader’s
GeometryProxy.
➤ Open SingleCardView.swift and replace the existing frame and scale modifiers
on CardDetailView(card:) with these:
// 1
.frame(
width: Settings.calculateSize(proxy.size).width,
height: Settings.calculateSize(proxy.size).height)
// 2
.clipped()
// 3
.frame(maxWidth: .infinity, maxHeight: .infinity)
573
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
1. Calculate the size of the card view given the available space.
2. The background color will spill out of the frame, so clip it.
3. Make sure that CardDetailView takes up all of the space available to it. This will
center the card view in the geometry reader.
In portrait mode, the card probably looks fine in the canvas. The problem comes
when you view the card in landscape, and find that the elements aren’t scaling
properly.
Incorrect scaling
➤ In the Views group, open ResizableView.swift.
Notice that the new changes in this file adjust all the offsets and sizes to be scaled to
viewScale. This defaults to 1, so you don’t have to specify a view scale if you don’t
want to.
574
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
.resizableView(
transform: $element.transform,
viewScale: viewScale)
When ResizableView transforms the size of each element, it now uses the new view
scale.
CardDetailView(
card: $card,
viewScale: Settings.calculateScale(proxy.size))
You calculate the scale of the view and pass it down the hierarchy.
With the view scaled, the element’s default size will be too small.
575
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Build and run on various devices and orientations and check out your newly scaled
card view. The card stays in portrait and is fixed to a scaled 1300 by 2000 size. The
elements are also scaled and you can manipulate them in the same way as you did
before.
Alignment
Skills you’ll learn in this section: stack alignment
The final subject in layout that you’ll cover is alignment. Take another look at the
previous image. Currently, the images in your toolbar buttons are different sizes
which misaligns the button text. Your attention-to-detail gene should have been
crying inwardly because of this.
576
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Stack Alignment
With an HStack, you describe how child views should align vertically, and with a
VStack, you describe the horizontal view alignment
577
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
HStack(alignment: .top) {
HStack(alignment: .bottom) {
578
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Challenges
Challenge 1: Resize the Bottom Toolbar Icons
When you build and run the app on iPhone and rotate to landscape, you’ll see that
because the images and text escape from the constrained size of the toolbar, the
alignment is lost. In addition, the home bar covers the text.
Escaping buttons
For your challenge, you’ll check the size class of the device use a different icon view
for each size class. The compact size class will only show the image, whereas the
regular size class will show both image and text.
To achieve this:
2. Use the environment’s vertical size class to determine whether to show either
regularView or compactView.
579
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Try out drag and drop on iPad, and you have an infinite number of Google images to
decorate your card.
580
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Key Points
• Even though your app works, you’re not finished until your app is fun to use. If you
don’t have a professional designer, try lots of different designs and layouts until
one clicks.
• Stacks have alignment capabilities. If these aren’t enough, you can create your
own custom alignments, too. The Apple video, Building Custom Views with
SwiftUI (https://apple.co/39uamSx), examines SwiftUI’s layout system in depth.
581
21 Chapter 21: Delightful UX
— Final Touches
By Caroline Begbie
An iOS app is not complete without some snazzy animation. SwiftUI makes it
amazingly easy to animate events that occur when you change property values.
Transition animations are a breeze.
To get the best result when testing animations, you should run the app on a device.
Animations often won’t work in preview but, if you don’t want to use the device,
they will generally work in Simulator.
582
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
The project has an additional group called Supporting Code. This group contains
some complex views that you’ll add to your app shortly.
Sometimes in a more complex app, after showing the launch screen, your app will
take a few seconds to do all the loading housekeeping. To prevent the UI from
appearing to stall, the app can perform an animation to distract the user. Apps such
as Twitter and Uber use animation to reflect their branding.
You’ll create an animated splash screen where the letters C-A-R-D-S will drop down
from the top and, when that animation is complete, the animation view will slide to
the main cards view.
Final animation
583
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ In the Cards group, under CardsApp.swift, create two new SwiftUI View files
named AppLoadingView.swift and SplashScreen.swift.
When showSplash is true, you’ll show the splash animation, otherwise you’ll show
the main CardsListView. At the moment, you never set showSplash to false, so
CardsListView will never show. Sometimes the live preview doesn’t show
animations correctly — or at all — so in order to see the animation on the simulator,
you’ll keep it this way until you perfect your splash animation.
.environmentObject(CardStore(defaultData: true))
This sets up the card store so that the app will still work in Live Preview.
AppLoadingView()
You show the intermediate view which contains the splash screen.
584
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Build and run, and you’ll see the default “Hello World” from SplashScreen.
Hello, World
➤ Open SplashScreen.swift and add this new method to SplashScreen:
Here you create a view, with a shadow, that takes in a letter and a color.
585
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Here you create the view with the letter “C” and the name of a color set up in your
asset catalog.
The card
You now have a stationary card. You’ll separate out the animation movement into a
new view modifier.
To drop the card from the top, you’ll animate content‘s offset. If animating is true,
then the card’s offset is off the top of the screen at -700 points. When false, the
offset will be the final designated position. You change animating to false when
the view appears.
586
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ At the end of SplashScreen.swift, add a new extension where you can improve
the modifier’s ease-of-use:
Here, you call the view modifier with the final Y position of the card.
➤ Live Preview the view, and you’ll see your card 200 points below the center, but
not animated yet.
587
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
SwiftUI Animation
Skills you’ll learn in this section: explicit animation; animation timing;
slow animations for debugging
SwiftUI makes animating any view parameter that depends on a property incredibly
easy. You simply surround the dependent property with a closure:
withAnimation {
property.toggle()
}
And that’s it! Any parameter in your entire app that depends on property, will
animate automatically.
withAnimation {
animating = false
}
➤ Live preview the view. Your card now animates from the top and ends up at a Y
offset of 200.
➤ Build and run the app in Simulator and choose Debug ▸ Slow Animations.
This menu option is a debug feature to slow down animations, so that you can see
them properly. You’ll now see a check mark next to the menu item.
➤ Build and run the app again to see the animation in slow motion.
ZStack {
Color("background")
.ignoresSafeArea()
card(letter: "S", color: "appColor1")
.splashAnimation(finalYposition: 240, delay: 0)
card(letter: "D", color: "appColor2")
.splashAnimation(finalYposition: 120, delay: 0.2)
card(letter: "R", color: "appColor3")
.splashAnimation(finalYposition: 0, delay: 0.4)
card(letter: "A", color: "appColor6")
588
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
This sets up all the card letters with their final positions and colors. The delay
parameter doesn’t do anything yet, but you’ll use it shortly. The background color is
in your asset catalog.
➤ Live Preview or run in Simulator. In this animation, all the cards animate
downwards with the same timing, which isn’t aesthetically pleasing.
withAnimation(Animation.default.delay(delay)) {
589
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Here you’re using the default animation with a delay modifier. You’ve already set up
the cards with their delay. Each card has a 0.2 second delay greater than the previous
card.
➤ Live Preview the result. With the delays, the card animation is staggered.
Animation delay
An Animation can have various qualities. The most common are:
• easeIn: where the animation starts slowly but speeds up to the end.
• easeOut: where the animation starts at speed but slows down toward the end.
• linear: where the animation speed is constant all the way through.
withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
This animation lasts for 1.5 seconds and slows gradually at the end of the animation.
590
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Live Preview first to see the animation in 1.5 seconds. Then build and run on the
simulator with slow animations. You can see that the cards fall closer together
toward the end of the animation.
withAnimation(animation.delay(delay)) {
animating = false
}
591
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Live Preview this, and you’ll see that each card bounces as it hits its offset
position. Experiment with the values of each of the animation spring properties to
see how they affect the animation.
.rotationEffect(
animating ? .zero
: Angle(degrees: Double.random(in: -10...10)))
The card animates to a random rotation between -10 and 10 degrees as it drops.
Random rotation
592
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
For implicit animation, you animate any view with an animatable parameter
automatically.
.onAppear {
animating = false
}
This adds an implicit animation to the view. The view watches the property
animating, and whenever animating changes, the view animates with the
Animation provided.
In this case, as you are only animating views with one animatable property, the
implicit animation will appear exactly the same as the explicit animation. Explicit
animations can be less code, but implicit animations give you more control by being
able to animate each view depending on the animated property with different
animations.
593
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Animated Transitions
Skills you’ll learn in this section: transitions
You’ll now transition your splash screen to the main CardsListView. SwiftUI makes
this easy with built-in transition effects, but you can also have complete control over
how the view transitions.
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.linear(duration: 5)) {
showSplash = false
}
}
}
Here you set showSplash to false after a delay and use explicit animation.
showSplash controls which view shows. You want the splash screen to show for a
second or two and then transition to the main view.
Slowing the animation in Simulator doesn’t work well when testing this transition,
so you give the transition animation a slow duration of 5 seconds to see what’s
happening.
➤ In Simulator, choose Debug ▸ Slow Animations to turn off the slow animations.
➤ As Live Preview doesn’t work well with transition animations, build and run the
app.
594
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
The default transition does an opacity fade from one view to another.
Fade transition
➤ In AppLoadingView, add a modifier to CardsListView():
.transition(.slide)
➤ Build and run to see the slide transition over the specified five second duration.
Slide transition
595
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
As well as opacity and slide, there are a couple more automatic transitions:
• move: allows you to specify the edge that the new view moves in from.
You can also have a different transition for each direction by using:
withAnimation {
This replaces the five second duration with the default transition duration.
➤ Build and run to see your completed splash screen animation and transition.
Scale transition
596
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
You’ll add a picker view to the top of the list of cards to choose how you view the
cards. You can either view them in the scrolling list or in a carousel. When you have a
set of mutually exclusive values, you can use a picker control to decide between
them.
There are various picker styles for mutually exclusive picking. For example,
WheelPickerStyle shows the options in a scrollable wheel. Apple’s Clock app uses a
wheel picker for the Timer. You’ll use a SegmentedPickerStyle, which is a
horizontal control that holds one value at a time.
The Carousel
Carousel.swift, included in the starter project in the Supporting Code group, is an
alternative view for listing the cards. It’s a an example of a TabView, similar to the
one you created in Section 1.
➤ Open Carousel.swift and Live Preview the view. Swipe to view each card.
Carousel
597
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Each card should take up most of the device’s screen, so the code uses
GeometryReader to determine the size. There should be nothing new to you in this
code. One of SwiftUI’s great advantages is that you can be given a view like this, and
it’s an easy matter to slot it into your own code.
Adding a Picker
➤ In the Views group, under CardsListView.swift, create a new SwiftUI View file
named ListSelection.swift.
➤ At the top of the file, after import SwiftUI, create a new enumeration that
describes how you are viewing the list of cards:
enum ListState {
case list, carousel
}
listState holds the current picker selection and you’ll pass this in from
CardsListView.
598
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
2. You assign SFSymbols for each option. When the user chooses an option, the
tag(_:) modifier will update listState with the specified value.
3. You tell the Picker what picker style to use. Other picker styles include menu and
wheel, which displays options in a scrollable wheel.
Segmented picker
In the app, when you tap the right segment, the cards should display in the carousel;
tapping the left segment will display them in the scrolling list.
ListSelection(listState: $listState)
Group {
switch listState {
case .list:
list
case .carousel:
Carousel(selectedCard: $selectedCard)
}
}
599
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
You show the scrolling list or the carousel depending on listState. Similar to the
list, when you select a card from the carousel, changing the value of selectedCard
will run the full screen modal.
At the moment, when you create a card, you’re the only person who can admire it. As
a final feature, you’ll add sharing.
You’ll create a share button on the navigation bar. On tapping this button, you’ll
screen capture the card. You’ll then use this screenshot in the built-in Share sheet
for sharing to other apps such as email or your Photos library.
600
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
extension UIImage {
// 1
@MainActor static func screenshot(
card: Card,
size: CGSize
) -> UIImage {
// 2
let cardView = ShareCardView(card: card)
let content = cardView.content(size: size)
// 3
let renderer = ImageRenderer(content: content)
// 4
let uiImage = renderer.uiImage ?? UIImage.errorImage
return uiImage
}
}
1. MainActor ensures that a method is performed on the main dispatch queue. Any
time you are dealing with views, you should be on the main thread. Note that any
method that calls UIImage.screenshot(card:size:) must also be marked with
MainActor, otherwise it will not compile.
2. Load the card into a view and extract the content. Specifying the size of the
content, means that you can scale it to any size preview you want.
3. Render the image from the view. ImageRender<Content> initializes with a view
and draws it to a Canvas. You can render shapes or text or any other View to an
image.
4. Extract a UIImage from the rendered image, but if there’s an error, use the error
image in the asset catalog.
601
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ In the Single Card Views group, open CardToolbar.swift and add this code after
the Done button ToolbarItem:
ToolbarItem(placement: .navigationBarLeading) {
let uiImage = UIImage.screenshot(
card: card,
size: Settings.cardSize)
let image = Image(uiImage: uiImage)
// Add ShareLink here
}
You create a new toolbar item at the leading edge of the navigation bar and load an
Image ready for sharing.
Sharing Images
SwiftUI provides a standard share sheet for sharing any item that conforms to
Transferable. For example, this code will allow you to save text to the Files app
through the share sheet:
602
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
ShareLink will add an icon, seen here at the top left of the screen, where you can
start the share. A sheet will pop up, and you’ll see a preview of the text at the top left
of the sheet. The share sheet determines what apps to show from the type of the
item.
ShareLink(
item: image,
preview: SharePreview(
"Card",
image: image)) {
Image(systemName: "square.and.arrow.up")
}
Here you use a longer ShareLink initializer. In place of text, you share your screen-
capture image. You create your own preview image and provide a custom icon.
➤ Build and run your app in Simulator. Open the first card and tap the share icon at
the top left. Pull up the sheet to see where you can share the card.
603
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ As no option appears to save your card to Photos, tap Save to Files, then Save,
and then open the Files app in Simulator. In the Files app, locate your card. Long
press your card and choose Get Info to see the properties of the imported file.
There’s only one problem. You’d much rather have it in Photos than Files.
App properties are held in your app’s Info.plist. You’ll save a property here to allow
Photo Library additions, and the option to save a photo will automatically appear in
the share sheet’s list of actions.
In the Project navigator, select the topmost Cards and choose the Cards target, then
choose Info from the options across the top.
604
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
This is the message your users will see, so you might add something soothing about
not using their personal data for nefarious purposes.
The app asks for permission to save to photos, showing the message you entered in
the Info key.
605
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Tap OK and the card will save to the photo library. Check out the Photos app on
the simulator to see your photo library.
606
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Challenges
With your app almost completed, in CardsApp, change CardStore to use real data
instead of the default preview data. Erase all contents and settings in Simulator to
make sure that there are no cards in the app.
To achieve this:
1. Locate the code where you save the card in SingleCardView.swift. First use
UIImage.screenshot(card:size:) to generate a UIImage and then save the
UIImage to a file. UIImageExtensions.swift contains a method
UIImage.save(to:) to save the file. Use card.id.uuidString as the filename.
If you have done this part correctly, when testing this in Simulator, the card
thumbnail image will load when you first run the app, but not when you change a
card by moving one of the elements. In CardsListView.swift, CardThumbnail will
only refresh if there are published changes. CardThumbnail uses card from
store.cards, and this is the property that you need to update.
607
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
3. Add a uiImage: UIImage? property to Card and update this property when you
load the card image in SingleCardView.swift. Updating this property means that
you update the published property cards in CardStore, and the card thumbnail
will redraw.
First, preview and examine TextView and make sure you understand it. SwiftUI views
look complicated, but you have encountered almost everything in this file before.
608
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Your challenge is to add this view to the modal view TextModal under the current
TextField.
With the new font and color, style the text currently being entered in the TextField.
Use .font(.custom(textElement.textFont, size: 30)) to style the font.
To test the view, run the app in Simulator or Live Preview SingleCardView.
609
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Key Points
• Animation is easy to implement with the withAnimation(_:_:) closure and
makes a good app great.
• Transitions are also easy with the transition(_:) modifier. Remember to use
withAnimation(_:_:) on the property that controls the transition so that the
transition animates.
• Picker views allow the user to pick one of a set of values. You can have a wheel
style picker or a segmented style picker.
• Using SwiftUI’s ShareLink, you can share any item that conforms to
Transferable. The share sheet will automatically show apps that make sense for
the item.
A great example of an app with complex layout and animation is Apple’s Fruta
sample app (https://apple.co/2XE8tNF). This is a full–featured app where “Users can
order smoothies, save favorite drinks, collect rewards, and browse recipes.” Fruta
also has various features, such as widgets. Download the app and see if you can work
out how it all fits together.
610
Section III: Your Third App:
TheMet
You’ve now built two apps with beautiful user interfaces. But, you’re probably
wondering how to build an app that accesses resources on the internet. Fear not! In
this section, you’ll build TheMet, an app that allows you to view items in the
collection at the Metropolitan Museum of Art — better known as The Met. Along the
way, you’ll:
• Learn how to build lists of information and navigate between views using SwiftUI.
611
22 Chapter 22: Lists &
Navigation
By Audrey Tam
Most apps have at least one view that displays a collection of similar items in a table
or grid. When there are too many items to fit on one screen, the user can view more
items by scrolling — vertically, horizontally or both. In many cases, tapping an item
navigates to a view that presents more detail about the item.
In this section, you’ll start implementing TheMet, an app that searches The
Metropolitan Museum of Art, New York (https://www.metmuseum.org) for objects
matching the user’s query term.
612
SwiftUI Apprentice Chapter 22: Lists & Navigation
Getting Started
➤ Open the TheMet app in the starter folder. For this chapter, the starter project
initializes the Object data in Preview Content. In Chapter 24, “Downloading Data”,
you’ll fetch this data from collectionapi.metmuseum.org.
List
You encountered the SwiftUI List view in Chapter 10, “Working With Datasets”,
where you learned how to let users edit the history of exercises in HIITFit.
List is the easiest way to present a collection of items in a view that scrolls
vertically. You can display individual views and loop over arrays within the same
List. In this chapter, you’ll start by just listing objects, then you’ll embed the list in
a navigation stack so users can navigate to a detail view for each list item.
613
SwiftUI Apprentice Chapter 22: Lists & Navigation
In Live Preview, it doesn’t look like much but, later in this chapter, you’ll spruce it up
with a title, custom colors and a search button.
NavigationStack
In Chapter 13, “Outlining a Photo Collage App”, you used NavigationStack so you
could add toolbar buttons to SingleCardView. Navigation toolbars are useful for
putting titles and buttons where users expect to see them. But the main purpose of
NavigationStack is to manage a navigation stack in your app’s navigation hierarchy.
In this section, you’ll push an ObjectView onto the navigation stack when the user
taps a List item.
614
SwiftUI Apprentice Chapter 22: Lists & Navigation
NavigationStack {
List(store.objects, id: \.objectID) { object in
Text(object.title)
}
.navigationTitle("The Met")
}
615
SwiftUI Apprentice Chapter 22: Lists & Navigation
NavigationLink(object.title) {
ObjectView(object: object)
}
A NavigationLink takes two arguments — a label and a destination. Here, you label
the link with the object’s title and, in a trailing closure, set its destination to
ObjectView. Each List row acquires a disclosure indicator, telling the user there’s
more to see.
616
SwiftUI Apprentice Chapter 22: Lists & Navigation
ContentView is currently the only view in the navigation stack. When you tap a list
item, NavigationStack pushes ObjectView onto the navigation stack: It’s now the
top view on the stack, so it’s the view that’s visible.
NavigationStack gives you a “back” button, labeled the same as the root view’s
navigationTitle.
➤ Tap the back button to pop this view off the navigation stack, revealing
ContentView again.
One of the Object properties is objectURL — the URL of the object’s page at
metmuseum.org. There’s an easy way to open this page in the device’s default
browser.
Link Button
➤ In ContentView.swift, comment out the NavigationLink(...) { ... } code
and type the following code:
You create a special button whose label is the object’s title. Tapping this button
opens its destination URL in the associated app. You create the URL from the Object
property objectURL. The associated app is Safari (in a simulator) or your device’s
default browser.
617
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Build and run in the simulator or on your device: There’s no disclosure indicator
anymore because the whole list row is a button. Tap an item to open the object’s web
page in Safari or your device’s default browser:
618
SwiftUI Apprentice Chapter 22: Lists & Navigation
SFSafariViewController
You might prefer your users don’t leave your app. You can open a Safari browser in
your app. Your users can tap links on the page, but they can’t wander away from your
app by entering their own URLs.
➤ Look at SafariView.swift:
import SwiftUI
import SafariServices
func makeUIViewController(
context: UIViewControllerRepresentableContext<SafariView>
) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(
_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>)
{}
}
You use Representable protocols to insert UIKit views or view controllers into your
SwiftUI apps. Instead of creating a structure that conforms to View for a SwiftUI
view, you create a structure that conforms to either UIViewRepresentable — for a
single view — or UIViewControllerRepresentable — to use a view controller for
complex management of views.
619
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Go back to ContentView.swift and delete the Link(...) { ... } code and type
the following code in its place:
NavigationLink(
destination: SafariView(url: URL(string: object.objectURL)!))
{
HStack {
Text(object.title)
Spacer()
Image(systemName: "rectangle.portrait.and.arrow.right.fill")
.font(.footnote)
}
}
The destination is a SafariView that loads the object’s web page. This time, you
define the label in the trailing closure because it’s more complex than a String —
you add an icon to indicate that tapping the item takes the user to a web page.
620
SwiftUI Apprentice Chapter 22: Lists & Navigation
Now, NavigationStack pushes SafariView onto the navigation stack and gives you
the standard The Met back button. There’s no location field, although there are
buttons for the share sheet, and the user can open this page in their default browser.
➤ Tap the back button to pop this view off the navigation stack and return to the list
view.
AsyncImage
In the next chapters, you’ll learn how to use URLSession methods to download
Object data from metmuseum.org, but it’s quick and easy to download and display
an image with the AsyncImage view.
If an object is in the public domain, then its images are available for use without
restriction under the Met’s Open Access program, and its primaryImageSmall
property is a non-empty string — a web address.
The url argument is URL? so, if primaryImageSmall yields a valid URL, the view
returns an image. You modify this image with the usual image modifiers and reuse
the PlaceholderView “picture frame” as a placeholder while the image is
downloading.
Note: Bear in mind that you can’t apply modifiers directly to AsyncImage,
instead you need to apply them to image. There’s more you can do with
AsyncImage, like animate the way the image appears or handle possible errors.
If you want to learn about it, take a look at AsyncImage’s official
documentation (https://apple.co/3XBOqfx).
621
SwiftUI Apprentice Chapter 22: Lists & Navigation
In Live Preview, the image for “Bahram Gur Slays the Rhino-Wolf” appears:
navigationDestination
This app can download two kinds of objects from metmuseum.org. Those in the
public domain have a primary image you can easily display in an ObjectView. What
should your app do for objects that aren’t in the public domain? Well, you can just as
easily load their web page into a SafariView. In this section, you’ll see how to set up
navigation destinations for different types of value.
622
SwiftUI Apprentice Chapter 22: Lists & Navigation
NavigationLink(object.title) {
ObjectView(object: object)
}
623
SwiftUI Apprentice Chapter 22: Lists & Navigation
You’ll soon set up the List with both navigation links, but first, see what happens:
Now, here’s one way to display public-domain objects with the object.title label
and non-public-domain objects with the WebIndicatorView label.
624
SwiftUI Apprentice Chapter 22: Lists & Navigation
if !object.isPublicDomain,
let url = URL(string: object.objectURL) {
NavigationLink(destination: SafariView(url: url)) {
WebIndicatorView(title: object.title)
}
} else {
NavigationLink(object.title) {
ObjectView(object: object)
}
}
625
SwiftUI Apprentice Chapter 22: Lists & Navigation
Using navigationDestination
➤ Replace your if-else code with the following:
if !object.isPublicDomain,
let url = URL(string: object.objectURL) {
NavigationLink(value: url) {
WebIndicatorView(title: object.title)
}
} else {
NavigationLink(value: object) {
Text(object.title)
}
}
You use the value initializer for NavigationLink, so both label views are in the
trailing closures. This version expects you to modify the enclosing List with a
matching navigationDestination for each type of value.
➤ In Live Preview, try both kinds of navigation link to confirm they still work the
same.
626
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Back in ContentView.swift, refresh Live Preview to see the oil lamp item no
longer uses WebIndicatorView(title:):
627
SwiftUI Apprentice Chapter 22: Lists & Navigation
628
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Back in ContentView.swift, refresh Live Preview, then tap the oil lamp item:
629
SwiftUI Apprentice Chapter 22: Lists & Navigation
630
SwiftUI Apprentice Chapter 22: Lists & Navigation
In your app, Assets.xcassets defines these two colors as met-background and met-
foreground. ColorExtension.swift extends Color to add metBackground and
metForeground as static properties. You’ll use these colors to differentiate the
public-domain and non-public-domain rows.
.listRowBackground(Color.metBackground)
.foregroundColor(.white)
.listRowBackground(Color.metForeground)
631
SwiftUI Apprentice Chapter 22: Lists & Navigation
You’ve made the non-public-domain rows red, changing the text color to white, so it
shows up on the red background. And you made the public-domain rows sky blue
If the object’s objectURL is valid, you wrap its title in a WebIndicatorView and
style it to look like the non-public-domain rows in your List. Then, you create a
Link button with this view as its label.
632
SwiftUI Apprentice Chapter 22: Lists & Navigation
633
SwiftUI Apprentice Chapter 22: Lists & Navigation
You provide a starting query term and initialize the value that shows or hides the
alert.
.toolbar {
Button("Search the Met") {
query = ""
showQueryField = true
}
.foregroundColor(Color.metBackground)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.metBackground, lineWidth: 2))
}
When the user taps this button, it resets query to the empty string and sets
showQueryField to show the alert. You set the color of the button’s text and border
to metBackground to make it stand out.
634
SwiftUI Apprentice Chapter 22: Lists & Navigation
You pass a binding to query to the text field. You’ll fill in the Search button’s action
after you write the download code in Chapter 24, “Downloading Data”.
635
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ First, embed List in a VStack, then add this code at the top of the VStack:
➤ In Live Preview, tap the search button, then type some text:
You-searched-for message
The message updates to You searched for ‘’, then it shows whatever you typed into
the text field. Looking good! Now, you’re all set to learn how to download data from
a server, after the next chapter, which covers some HTTP and REST API basics.
636
SwiftUI Apprentice Chapter 22: Lists & Navigation
Key Points
• The SwiftUI List view is the easiest way to present a collection of items in a view
that scrolls vertically. Call .listRowBackground on the view in the row, not on the
List itself.
• A NavigationStack can contain alternative root views. You modify each with its
own navigationTitle and toolbars.
• A NavigationLink has an initializer that takes two arguments — a label view and
a destination view. You can supply a String for the label, and NavigationLink
will create a Text view.
• Your app can open a web link in the device’s default browser using Link or as a
Safari view within your app.
• It’s easy to download an image and display it with the AsyncImage view.
637
23 Chapter 23: Just Enough
Web Stuff
By Audrey Tam
This chapter covers some basic information about HTTP messages between iOS apps
and web servers. It’s just enough to prepare you for the following chapter, where
you’ll implement downloads from the metmuseum.org server.
If you already know all about HTTP messages, skip down to the section “Exploring
metmuseum.org” to familiarize yourself with the API you’ll use in the following
chapters.
638
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Apps like Safari and TheMet are clients of these servers. A client sends a request to a
server, which sends back a response. This communication consists of plain-text
messages that conform to the Hypertext Transfer Protocol (HTTP). Hypertext is
structured text that uses hyperlinks between nodes containing text. Web pages are
written in HyperText Markup Language (HTML).
HTTP has several methods, including POST, GET, PUT and DELETE. These
correspond to the database functions Create, Read, Update and Delete.
Note: HTTPS is the secure, encrypted version of HTTP. It protects your users
from eavesdropping. The underlying protocol is the same but, instead of
transferring plain-text messages, everything is encrypted before it leaves the
client or server.
639
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
HTTP Messages
A client’s HTTP request message contains headers. A POST or PUT request has a
body to contain the new or updated data. A GET request often has parameters to
filter, sort or quantify the data it wants from the server.
A server’s HTTP response message also has headers and a body. A key part of the
response is the status code — ideally, 200 OK in response to a GET request or 201
Created in response to a POST request. You don’t want to see any error status codes
like 404 Not Found:
640
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
The HTTP 418 I’m a teapot client error response code indicates that the server refuses
to brew coffee because it is, permanently, a teapot. A combined coffee/tea pot that is
temporarily out of coffee should instead return 503. This error is a reference to Hyper
Text Coffee Pot Control Protocol defined in April Fools’ jokes in 1998 and 2014. Some
websites use this response for requests they do not wish to handle, such as automated
queries.
Usually, you’ll work with three content types for text data, depending on the
structure:
• JSON (JavaScript Object Notation) is the most common data format used for HTTP
communication by app clients. It’s a structured data format consisting of numbers,
strings, and arrays and dictionaries that can contain strings, numbers and nested
arrays and dictionaries.
• Web forms use form-encoded, which looks like a query string. A query string is a
collection of key-value pairs, separated by & and preceded by ?.
When working with binary data some of the most used types are PDF, image formats
and multi-part form data, when the client sends any kind of binary file along with
text elements.
REST API
In Chapter 12, “Apple App Development Ecosystem”, you learned about the
numerous frameworks you can use to develop iOS apps. An Apple framework is one
kind of Application Programming Interface (API). It tells you how to use the
standard components created by Apple engineers.
641
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Another kind of API is the set of rules for clients to request resources from a server.
Most of the APIs you’ll use for your apps are REST APIs, which use HTTP. For each
resource available on the server, the REST API documentation tells you how to
construct a request:
In the next chapter, you’ll set up TheMet to communicate with the museum’s REST
API. In this chapter, you’ll explore this API’s documentation (https://
metmuseum.github.io).
Browser
The easiest way to make a simple HTTP GET request is to enter the URL in a browser
app like Safari.
https://www.metmuseum.org/art/the-collection
642
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
This is the endpoint of the metmuseum.org art collection. You get a page similar to
this:
cURL
A browser is a fully-automated HTTP tool. At the other end of the spectrum is the
command-line tool cURL (https://curl.se) — “the internet transfer backbone for
thousands of software applications”.
The documentation for a REST API often provides sample requests to show you how
to use it. Very often, these use cURL.
curl https://api.github.com/zen
You send an HTTP request to GitHub’s API server. The response is a random item
from their design philosophies, like “Favor focus over features” or “Avoid
administrative distraction”. There are lots more request examples at GitHub’s
Getting started with the REST API (https://bit.ly/3iD717R).
But, you exclaim, curl doesn’t show any response headers either! Well, like all Unix
commands, curl has a wealth of options, including --include and its shortcut -i, to
include the HTTP response headers in its output.
643
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
curl -i https://api.github.com/zen
HTTP/2 200
server: GitHub.com
date: Fri, 09 Dec 2022 01:49:10 GMT
content-type: text/plain;charset=utf-8
...
x-ratelimit-reset: 1670551677
x-ratelimit-resource: core
x-ratelimit-used: 2
accept-ranges: bytes
x-github-request-id: F968:61FE:7DF301:85B2E3:63929416
Headers beginning with x- are custom headers set up by the organization. For
example, x-ratelimit-limit and x-ratelimit-used indicate how many requests a
client can make in a rolling time period (typically an hour) and how many of those
requests the client has already made.
The curl --verbose or -v option displays request headers and a lot more.
curl -v https://api.github.com/zen
Lines that start with < are response headers, and lines that start with * are additional
information provided by cURL.
644
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
You might not enjoy typing long structured command lines, especially something
like this sample cURL command to create a new GitHub repository:
curl -i -H \
"Authorization: token
5199831f4dd3b79e7c5b7e0ebe75d67aa66e79d4" \
-d '{ \
"name": "blog", \
"auto_init": true, \
"private": true, \
"gitignore_template": "nanoc" \
}' \
https://api.github.com/user/repos
This POST command sends authorization data in a request header and the request
body as data in JSON format. The endpoint doesn’t name a specific user because
GitHub knows that from the token value.
Another problem with using cURL: If the response is complex, it’s hard to examine it
in the terminal.
curl https://collectionapi.metmuseum.org/public/collection/v1/
objects/437133
This is a request to the API you’ll use for TheMet. The response is pretty mind-
numbing:
645
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
If you concentrate, you might be able to see from this output that the response body
is a dictionary where a couple of items are arrays of dictionaries. You can use a tool
like codebeautify.org/jsonviewer to format and beautify this so it’s easier to read.
Exploring metmuseum.org
Apps like Postman let you create HTTP requests by filling in fields and selecting
from drop-down menus. You can pretty-print responses, and this also gives you
syntax highlighting.
646
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ Download Postman (https://bit.ly/3VTp6B4) for your Mac’s chip and open the app.
If you don’t want to create an account, click Skip and go to the app. Then, in
Scratch Pad, under Get started, click Create a request and hide the side bar:
Requesting Objects
➤ Open Postman and enter this URL in the GET field:
https://collectionapi.metmuseum.org/public/collection/v1/
objects/437133
You set the resource endpoint. How do you know what to ask for? Scroll down in
metmuseum.github.io: In the Endpoints section, you’ll see four API endpoints:
Objects, Object, Departments and Search. Scroll further to see, for each endpoint, its
description, request parameter list, request endpoint, response field list and
examples of requests and responses.
647
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
The URL you entered in the GET field is an Object request for the example objectID:
648
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Scrolling through the response body, it’s much easier to see all the keys in the top
level dictionary: objectID, isHighlight and so on. Your app needs only a few of
these keys.
➤ Click Headers to see the status code 200 OK, response time, size and other
response headers:
Response headers
Content-Type is application/json; charset=UTF-8. The key information here is
json. This tells you how to decode the response body. In the next chapter, you’ll use
JSONDecoder to extract the attributes you want and store them in the Object
structure so you can display them in your app.
Note: UTF-8 string encoding is a version of Unicode that is very efficient for
storing regular text, but less so for special symbols or non-Western alphabets.
Still, it’s the most popular way to deal with Unicode text today.
Media URLs
➤ Go back to the Body tab: This object isn’t in the public domain, so it has empty
strings for its primary image values. In the GET field, replace the object ID 437133
with our old friend 452174, then click Send:
Rhino-wolf object
649
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
This is the “Bahram Gur Slays the Rhino-Wolf” object. In the previous chapter, you
used AsyncImage to download its primaryImageSmall.
➤ The value of the primaryImageSmall key is a link. Click it to insert it into the GET
field, then send the request.
Primary image
Postman is able to display the image. You can also Command-click the link to open
it in your browser.
Content-Type: image/jpeg
The Content-Type is now image/jpeg.
650
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ At the top-level, click the + to add a new request and send a request for this URL:
https://collectionapi.metmuseum.org/public/collection/v1/search?
q=rhino
This is the request you’ll send from your app when the user enters a query term.
Then, to display the list of matching objects, you’ll request the object for each object
ID in the objectIDs array.
URL-encoding
➤ Now, send a search request for “rhino wolf”:
https://collectionapi.metmuseum.org/public/collection/v1/search?
q=rhino wolf
651
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
The console shows the “official” request URL, where the query string is actually
rhino%20wolf.
Postman URL-encoded the space between rhino and wolf to %20. URLs sent over the
internet can contain only letters, digits and these punctuation marks: -, _, . and ~.
When / and ? are delimiters in the URL, they don’t get encoded.
TheMet only needs to GET resources from the server, and your users don’t need to
authenticate.
You usually need to implement authentication for apps that let users access
restricted materials or create, update or delete server records. If you’re building an
app that requires this capability, consider using Sign In with Apple. You can learn
more by following our video course Sign in with Apple (https://bit.ly/3VOGbMG).
To try out a POST request, you’ll use Postman to send something like this GitHub
curl example:
curl -i -H \
"Authorization: token
5199831f4dd3b79e7c5b7e0ebe75d67aa66e79d4" \
-d '{ \
"name": "blog", \
"auto_init": true, \
"private": true, \
"gitignore_template": "nanoc" \
}' \
https://api.github.com/user/repos
This example shows how to create a new GitHub repository, so it requires GitHub-
user authentication. Remember when you set up your GitHub account in Xcode, you
had to generate a personal access token? You’ll need one here, too.
652
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ If you haven’t saved a plain-text copy of your GitHub personal access token,
generate a new one (https://bit.ly/2Y71Ofh) with repo scope:
653
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ Click Send:
➤ In the request Body tab, select raw, check the format is JSON, then type this in the
text view:
{
"name": "api-test-repo",
"auto_init": true
}
➤ Click Send:
654
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
That worked! Check your GitHub account to see there really is a new repository
named api-test-repo:
655
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Key Points
• Client apps send HTTP requests to servers, which send back responses.
• An HTTP response from an API endpoint contains a status code and some content.
Text content is usually in JSON format and may contain URIs the client app can
use to access media resources.
• HTTP requests follow the rules of the server’s REST API, whose documentation
specifies resource endpoints, HTTP methods and headers, and how to construct
POST and PUT request bodies.
• You can send simple GET requests in a browser app. Use cURL or an app like
Postman to create and send requests and inspect responses.
656
24 Chapter 24: Downloading
Data
By Audrey Tam
Most apps access the internet in some way, downloading data to display or keeping
user-generated data synchronized across devices. TheMet app needs to create and
send HTTP requests and process HTTP responses. Downloaded data is usually in
JSON format, which your app needs to decode into its data model.
If your app downloads data from your own server, you might be able to ensure the
JSON structure matches your app’s data model. But TheMet needs to work with the
metmuseum.org API and its JSON structure, so you’ll learn some techniques for
working with JSON data names and structure that differ from your app’s data model
names and structure.
657
SwiftUI Apprentice Chapter 24: Downloading Data
Getting Started
Open the DownloadingData playground in the starter folder. If the editor window is
blank, show the Project navigator (Command-1) and select DownloadingData
there.
Playgrounds are useful for exploring and working out code before moving it into
your app. You can quickly inspect values produced by code and methods without
needing to build a user interface or search through a lot of debug console messages.
URLSession
URLSession is Apple’s framework for HTTP messages. Apple’s documentation page
includes this note:
Note: The URLSession API involves many different classes that work together
in a fairly complex way which may not be obvious if you read the reference
documentation by itself.
658
SwiftUI Apprentice Chapter 24: Downloading Data
2. Create a URL from a String like https://metmuseum.org. This could fail — for
example, if the string is empty.
3. Create a URLRequest from this URL: This object contains the HTTP method and
headers and other request properties.
4. Send the URLRequest in the shared session and await a Data instance and a
URLResponse object.
5. Decode the data into your app’s data model: Most REST APIs send JSON data.
You’ll use a JSONDecoder to decode this into your data model. The code is much
simpler than what you did in Chapter 19, “Saving Files” because TheMet needs
only a few properties from the metmuseum.org API, and its JSON names and
structures are a good match for TheMet’s data model.
Note: URLSession and the broader topic of networking have their own video
courses: Beginning Networking with URLSession (https://bit.ly/3GtY2DE) and
Advanced Networking with URLSession (https://bit.ly/3GwHE5m).
Asynchronous Methods
Most URLSession methods involve network communication, so you can’t predict
how long they’ll take to complete. In the meantime, the system must continue to
interact with the user. To make this possible, URLSession methods are asynchronous:
They dispatch their work onto another queue and immediately return control to the
main queue, so it can respond to user interface events. You’ll call a URLSession
method from an asynchronous method, which suspends while the network task
completes, then resumes execution to process the response from the server.
Note: Learn more about asynchronous methods and concurrency in our book
Modern Concurrency in Swift (https://bit.ly/3i15FrM) and its companion video
courses Modern Concurrency: Getting Started (https://bit.ly/3If528z) and
Modern Concurrency: Beyond the Basics (https://bit.ly/3vtbIsd).
659
SwiftUI Apprentice Chapter 24: Downloading Data
https://collectionapi.metmuseum.org/public/collection/v1/search?
medium=Quilts|Silk|Bedcovers&q=quilt
This URL lists query parameter names and values after the ? separator. The query
parameter medium=Quilts|Silk|Bedcovers matches any object whose medium is
Quilts or Silk or Bedcovers. You can send the request just like this in Postman or
Safari but, to send it from your app, the | character must be URL-encoded as %7C:
https://collectionapi.metmuseum.org/public/collection/v1/search?
medium=Quilts%7CSilk%7CBedcovers&q=quilt
You certainly don’t want to do the URL-encoding yourself! Fortunately, you can hand
this work over to URLComponents and URLQueryItem. You’ll use these in this
playground to create a flexible approach to composing REST requests, so you can
easily change query parameter values.
URLComponents
URLComponents enables you to construct a URL from its parts and, also, to access the
parts of a URL. Components include scheme, host, port, path, query and
queryItems. URL itself gives you access to URL components like
lastPathComponent.
You set the URL string for the API’s base endpoint and add the search endpoint to
create a URLComponents instance. Then, you create an array of URLQueryItem values.
The URLQueryItem parameters are the parameter names and values in the sample
request.
660
SwiftUI Apprentice Chapter 24: Downloading Data
The last line displays the final URL string in the sidebar. The line above it displays
the final URL. What’s the difference? Time to find out!
Note: In a playground, you can write an expression on its own line to display
its value.
➤ Click the Execute Playground arrow on the last line number or at the bottom of
the playground:
Note: Clicking the arrow next to a line of code runs the playground only up to
that line.
The sidebar displays values for some lines with buttons for Quick Look and Show
Result.
➤ Click the Show Result button of the last code line to show the result below the
code line. You can click the display window to resize it, if necessary.
"https://collectionapi.metmuseum.org/public/collection/v1/
search?medium=Quilts%7CSilk%7CBedcovers&q=quilt"
661
SwiftUI Apprentice Chapter 24: Downloading Data
➤ Now look at the url on the line above. Notice it’s not in quotation marks, because
it’s not a String. In fact, it’s an Optional. Click its Show Result button:
You can create a URL from a String, if the String has all the right parts. Then, you
can access these parts as properties of the URL instance: host, baseURL, path,
lastPathComponent, query and so on.
If you try to create a URL from a String that wouldn’t work in a browser, the
initializer returns nil. That’s why urlComponents.url is an Optional and there’s a
url? in the last code line: If url is nil, it doesn’t have an absoluteString property.
var baseParams = [
"hasImages": "true",
"q": ""
]
urlComponents.setQueryItems(with: baseParams)
662
SwiftUI Apprentice Chapter 24: Downloading Data
You create a dictionary whose keys are query parameter names. The first parameter
hasImages matches any object whose web page displays at least one image. You
include q (search term) with the default value "", and this dictionary lets you easily
change its value.
baseParams["q"] = "rhino"
urlComponents.setQueryItems(with: baseParams)
urlComponents.url?.absoluteString
You’re adding the query parameter q=rhino to the request URL by setting
baseParams["q"] = "rhino" then calling setQueryItems(with:).
➤ Copy and paste your absoluteString into your browser to see what you get.
{"total":3,"objectIDs":[452648,241715,452174]}
The response body contains JSON data that maps directly into your ObjectIDs
structure. Only six objects match q=rhino, and only three of these have images.
663
SwiftUI Apprentice Chapter 24: Downloading Data
{"total":103,"objectIDs":
[551786,852562,472562,317877,544740,329077,437422,459027,459028,
544320,200668,824771,438821,460281,310453,53660,452102,207157,23
7451,450605,549236,451725,436607,435997,436098,712539,192770,399
01,435848,776714,439327,204587,197460,197461,448280,485416,38388
3,334030,811772,811771,687615,377933,436102,452032,437059,850659
,430812,736196,626692,759529,822751,435702,435621,452740,40080,4
36658,488221,764636,436105,39742,437585,228995,437878,60470,4523
64,228990,200840,53238,838076,53162,436838,436803,452651,437868,
453385,201718,437174,437508,435991,464118,451287,436884,436885,4
35864,437368,438779,73651,44759,436529,435844,437873,341703,4371
59,453895,437173,844492,39895,436099,733847,437936,450761,435678
,437061]}
This behavior is … unexpected. And not correct: None of these objects matches
q=rhino. It seems the metmuseum.org API has an undocumented requirement that
the q parameter must be the final parameter. So, for this API, you’ll need to add the q
parameter manually.
➤ In the baseParams initialization, delete q="" and the comma at the end of
"hasImages": "true":
var baseParams = [
"hasImages": "true"
]
664
SwiftUI Apprentice Chapter 24: Downloading Data
Now, you’re all set to send a request with this URL in the playground, then decode
the data.
Task { // 4
let (data, response) = try await session.data(for: request)
guard
let response = response as? HTTPURLResponse,
(200..<300).contains(response.statusCode)
else {
print(">>> response outside bounds")
return
}
let objectIDs = try decoder.decode(ObjectIDs.self, from: data)
// 5
objectIDs.objectIDs
}
665
SwiftUI Apprentice Chapter 24: Downloading Data
1. You create a URLRequest with queryURL. In this playground, you know queryURL
is a valid URL, so it’s safe to force-unwrap it. When this code is in a method, you
would do this assignment in a guard statement and exit if the value is nil.
3. You create a JSONDecoder to decode the JSON data you’re about to download.
5. Finally, you decode the data into an ObjectIDs instance, then display the array
of object IDs.
➤ Execute the playground to see the IDs of the two matching objects:
Decoding JSON
If there’s a good match between your data model and a JSON value, the default
init() of JSONDecoder is poetry in motion, letting you decode complex structures
of arrays and dictionaries in a single line of code. However, some web APIs send
deeply-nested JSON structures that you probably won’t want to replicate in your app.
Then, you’ll need to use CodingKey enumerations and custom init(from:)
methods.
666
SwiftUI Apprentice Chapter 24: Downloading Data
JSON values sent by real-world APIs rarely match the way you want to name or
structure your app’s data. Very often, your app doesn’t need all the JSON values. You
saw in the previous chapter the enormous number of fields in an object’s record:
This will work perfectly fine: JSONDecoder will decode only these values from the
response data and ignore the rest.
667
SwiftUI Apprentice Chapter 24: Downloading Data
Note: If you need Swift structures and coding keys for the complete JSON
schema, paste a sample response body into quicktype (https://
app.quicktype.io) and select the Swift and Struct options.
Many APIs use snake_case for JSON names, but Swift property names use
camelCase. There’s an easy fix; you simply tell the decoder to do the translation:
If the JSON structure matches your app’s data model, but some of the names are
different, you simply have to define a CodingKey enumeration to assign JSON item
names to your data model properties. For example, Object has a
primaryImageSmall property that matches the JSON item’s name. The value of this
key is a URL string, so you might want to name your app’s property imageURL. Here’s
how you’d do it:
668
SwiftUI Apprentice Chapter 24: Downloading Data
Fortunately, this isn’t the case with the metmuseum.org data you need for TheMet,
although the Object record does contain a few arrays. Suppose you want to use the
Wikidata_URL of one of the term items in the tags array:
"tags": [
{
"term": "Animals",
"AAT_URL": "http://vocab.getty.edu/page/aat/300249525",
"Wikidata_URL": "https://www.wikidata.org/wiki/Q729"
},
{
"term": "Poetry",
"AAT_URL": "http://vocab.getty.edu/page/aat/300055931",
"Wikidata_URL": "https://www.wikidata.org/wiki/Q482"
},
{
"term": "Men",
"AAT_URL": "http://vocab.getty.edu/page/aat/300025928",
"Wikidata_URL": "https://www.wikidata.org/wiki/Q8441"
},
{
"term": "Horses",
"AAT_URL": "http://vocab.getty.edu/page/aat/300250148",
"Wikidata_URL": "https://www.wikidata.org/wiki/Q726"
}
]
1. Define your data model to mirror the JSON value and see how nifty automatic
JSON decoding can be.
2. Flatten the JSON value into your data model: In exchange for more decoding
work, you’ll get sensible data structures that are easier and more natural to work
with.
To see how to do this and much more, check out our tutorial Encoding and Decoding
in Swift (https://bit.ly/3bqsrBY).
669
SwiftUI Apprentice Chapter 24: Downloading Data
Downloading Objects
Now, back to your playground code, where you sent a query request then decoded the
response data into an ObjectIDs instance. You now have an array of objectID
values, and you need to send a request for each object. You’ll store the downloaded
objects in an array.
You loop over the values in objectIDs.objectIDs. For each objectID, you run code
that’s very similar to what you did for the query request:
1. You construct the endpoint URL for this objectID and use it to create a
URLRequest.
2. You send the request, await the data and response, then check the status code.
3. You decode data into an Object instance, then append it to your objects array.
To check the array, you display it.
670
SwiftUI Apprentice Chapter 24: Downloading Data
➤ Execute the playground, show the result for objects and open an object:
➤ Open the project in this chapter’s starter folder: It’s the same as the final project
from Chapter 22, plus URLComponentsExtension.swift. If you prefer to continue
with your project, open the playground’s navigation panel and drag this file into your
project from the playground’s Sources folder.
Next, you’ll use your playground code to implement some helper methods that
fetchObjects(for:) will call. The helper methods will be asynchronous and might
throw errors, so fetchObjects(for:) also has these keywords.
671
SwiftUI Apprentice Chapter 24: Downloading Data
TheMetService
It’s good practice to keep your networking code separate from your data model, in a
separate file. And, it’s common to use either “networking” or “service” in the
filename.
struct TheMetService {
let baseURLString = "https://collectionapi.metmuseum.org/
public/collection/v1/"
let session = URLSession.shared
let decoder = JSONDecoder()
The first three lines are from your playground, and you’ll adapt playground code to
implement the two methods. fetchObjects(queryTerm:) in TheMetStore will first
call getObjectIDs(from:), then loop over the objectIDs array, calling
getObject(from:) for each objectID.
Both methods are async because they’ll call session.data(for:), and both
methods can rethrow errors thrown by session.data(for:). The JSONDecoder can
also throw errors, but these errors are extremely useful for finding any JSON-
decoding problems, so you’ll catch and print them right away. For now, both methods
return nil, so the compiler doesn’t complain.
getObjectIDs(from:)
➤ In getObjectIDs(from:), insert this code above return nil:
guard
var urlComponents = URLComponents(string: baseURLString +
"search")
else { // 2
672
SwiftUI Apprentice Chapter 24: Downloading Data
return nil
}
let baseParams = ["hasImages": "true"]
urlComponents.setQueryItems(with: baseParams)
urlComponents.queryItems! += [URLQueryItem(name: "q", value:
queryTerm)]
guard let queryURL = urlComponents.url else { return nil }
let request = URLRequest(url: queryURL)
2. You create the URLRequest, taking greater care to unwrap most of the optional
values.
do { // 2
objectIDs = try decoder.decode(ObjectIDs.self, from: data)
} catch {
print(error)
return nil
}
return objectIDs // 3
1. This is the playground code that calls data(for:), awaits data and response,
then checks the status code. You add getObjectIDs to the print message, so you
know which method had the problem. Because getObjectIDs is an asynchronous
method, it already runs in an asynchronous context, so you don’t need to embed
this code in a Task.
2. The decoder can throw errors, so you call it in a do-catch statement. You print
the raw error value, as this gives you more information about what went wrong.
3. If execution reaches this line, everything has worked without errors, and you
return ObjectIDs.
673
SwiftUI Apprentice Chapter 24: Downloading Data
getObject(from:)
fetchObjects(queryTerm:) in TheMetStore will loop over the objectIDs array,
calling getObject(from:) for each objectID.
do { // 4
object = try decoder.decode(Object.self, from: data)
} catch {
print(error)
return nil
}
return object // 5
2. You create the URLRequest, taking greater care to unwrap the optional value. You
don’t need to use URLComponent to construct objectURLString because
objectID is an Int, so there won’t be any characters that need to be URL-
encoded.
674
SwiftUI Apprentice Chapter 24: Downloading Data
3. This is modified from the playground code that calls data(for:), awaits data
and response, then checks the status code. Some objectID values return 404-
not-found, so you print the actual statusCode and the problem URL string.
5. If execution reaches this line, everything has worked without errors, and you
return the resulting Object.
fetchObjects(for:)
➤ In TheMetStore.swift, add these properties to TheMetStore:
You set 30 as the default maxIndex value. You don’t need to change the
@StateObject line in ContentView unless you want a different maxIndex value.
675
SwiftUI Apprentice Chapter 24: Downloading Data
.task {
do {
try await store.fetchObjects(for: query)
} catch {}
}
When the app starts, ContentView appears and this task runs. Because it modifies
NavigationStack, it runs only once, no matter how often you navigate to
ObjectView and back to ContentView.
676
SwiftUI Apprentice Chapter 24: Downloading Data
Next, you need to call fetchObjects(for:) when the user enters a new query term.
677
SwiftUI Apprentice Chapter 24: Downloading Data
➤ Unfold the VStack and locate the alert modifier. Add this code inside the
button’s action closure:
Task {
do {
store.objects = []
try await store.fetchObjects(for: query)
} catch {}
}
➤ Build and run the project in a simulator, then tap the search button and enter a
term like “persimmon”:
678
SwiftUI Apprentice Chapter 24: Downloading Data
You get a much longer list, including two non-public-domain objects that have
images.
What if you decide to search for a different term while the app is still downloading
objects for the current query term?
➤ Search for something that fetches a lot of objects, like “cat”. While these are still
downloading, search for “rhino” — you know this query returns only three objects.
679
SwiftUI Apprentice Chapter 24: Downloading Data
You’ll store the alert button task in fetchObjectsTask and cancel it before you start
the next task.
fetchObjectsTask?.cancel()
fetchObjectsTask = Task {
If there’s a fetchObjectsTask, you cancel it. In any case, save the new Task to
fetchObjectsTask.
➤ Build and run the project in a simulator, search for “cat”, then search for “rhino”:
680
SwiftUI Apprentice Chapter 24: Downloading Data
➤ Open TheMetStore.swift:
“… from background threads”: What’s this about threads? Your app runs its code on
threads — the main thread and one or more background threads. The user interface
of every iOS app runs on the main thread. A SwiftUI app’s user interface consists of
SwiftUI views, so these always run on the main thread. The main thread must always
be responsive to the user, so asynchronous methods run on a background thread to
avoid slowing down or blocking the main thread.
Here’s the problem: Any code that updates the user interface must run on the main
thread. If an asynchronous method contains code that updates the user interface, it
must somehow run that code on the main thread.
The “old concurrency” way to do this was to dispatch this code to the main queue.
The new Swift concurrency way to do this is to run the code on MainActor.
681
SwiftUI Apprentice Chapter 24: Downloading Data
await MainActor.run {
objects.append(object)
}
This is the only line of code that must run on the main thread, so you embed it in a
MainActor.run closure. You use await to call it asynchronously, so the system can
suspend and resume execution on the correct actor.
Note: You can annotate a method with @MainActor to ensure it runs on the
main thread or annotate a property to ensure it can only be updated from the
main thread. Or you can annotate an entire class with @MainActor, if almost
all its properties and methods need to be on the main thread, then mark any
exceptions with the nonisolated keyword. Learn more about Swift
concurrency from our book Modern Concurrency in Swift (https://bit.ly/
3i15FrM) or video courses Modern Concurrency: Getting Started (https://
bit.ly/3If528z) and Modern Concurrency: Beyond the Basics (https://bit.ly/
3vtbIsd).
➤ Build and run the project in a simulator, then enter a search term:
682
SwiftUI Apprentice Chapter 24: Downloading Data
.overlay {
if store.objects.isEmpty { ProgressView() }
}
Congratulations, you’ve built a working app for exploring the collections of The
Metropolitan Museum of Art, New York. And, you’re now ready to apply everything
you’ve learned about URLSession, URLComponents and JSONDecoder to your own
apps.
683
SwiftUI Apprentice Chapter 24: Downloading Data
Key Points
• The URLSession API involves many different classes that work together in a fairly
complex way. For simple download tasks, use the built-in shared session. Use
URLComponents to create a URL-encoded URL from the endpoint string and query
parameters, then create a URLRequest from this URL. Send this request with
data(for:) and await the Data instance and URLResponse object. If the status
code indicates success, use a JSONDecoder to decode the data into your data
model.
• Asynchronous methods run on background threads. Any code that updates the
user interface must run on the main thread. One way to do this is to call
MainActor.run.
• Playgrounds are useful for working out code. You can quickly inspect values
produced by methods and operations.
684
25 Chapter 25: Widgets
By Audrey Tam
Ever since Apple showed off its new home screen widgets in the 2020 WWDC
Platforms State of the Union, everyone has been creating them. It’s definitely a
useful addition to TheMet, providing convenient and quick access to objects listed in
your app.
Note: The WidgetKit API continues to evolve at the moment, which may
result in changes that break your code. Apple’s template code has changed a
few times since the 2020 WWDC demos. You might still experience some
instability. That said, Widgets are cool and a ton of fun!
685
SwiftUI Apprentice Chapter 25: Widgets
Getting Started
▸ Open the starter project or continue with your app from the previous chapter.
WidgetKit
WidgetKit is Apple’s API for adding widgets to your app. The widget extension
template helps you create a timeline of entries. You decide what app data you want to
display and the time interval between entries.
You also define a view for each size of widget — small, medium, large, extra large —
you want to support. The extra large size is available only in iPadOS.
Widget timeline
Here’s a typical workflow for creating a widget:
1. Add a widget extension to your app. Configure the widget’s display name and
description.
2. Select or adapt a data model type from your app to display in the widget. Create a
timeline entry structure — a Date plus your data model type. Create sample data
for snapshot and placeholder entries.
686
SwiftUI Apprentice Chapter 25: Widgets
3. Decide which widget sizes to support: Create small, medium or large views to
display one or more data model values. In iOS 16, you can also create accessory
views to display on the device’s lock screen.
687
SwiftUI Apprentice Chapter 25: Widgets
➤ Name it TheMetWidget, select your team and make sure Include Live Activity
and Include Configuration Intent are not checked:
There are two widget configurations: Static and Intent. A widget with
IntentConfiguration uses Siri intents to let the user customize widget parameters.
Your widget for TheMet will be static.
688
SwiftUI Apprentice Chapter 25: Widgets
@main
struct TheMetWidgetBundle: WidgetBundle {
var body: some Widget {
TheMetWidget()
}
}
689
SwiftUI Apprentice Chapter 25: Widgets
➤ Open TheMetWidget.swift, then find TheMetWidget and edit the last two
modifiers: configurationDisplayName(_:) and description(_:).
1. The structure’s name and its kind property are the name you gave it when you
created it.
2. You define your widget’s timeline, snapshot and placeholder entries in Provider.
4. In this structure, you only need to customize the name to The Met and the
description to View objects from the Metropolitan Museum. Your users will
see these in the widget gallery.
690
SwiftUI Apprentice Chapter 25: Widgets
➤ You can try out your widget in a simulator. If you want to install your app on your
iOS device, you need to sign both targets. In the Project navigator, select the top
level TheMet folder. Use your organization instead of “com.yourcompany” in the
bundle identifiers and set the team for each target.
Note: Your widget’s bundle ID prefix must be the same as your app’s. This isn’t
a problem with TheMet but, if your project has different bundle IDs for Debug,
Release and Beta, you’ll need to edit your widget’s bundle ID prefix to match.
➤ TheMetWidget is a second target, and it’s probably the currently selected scheme.
Make sure you select the TheMet scheme, then build and run. Tap the Home button
in the simulator tool bar to close the app, then press on some empty area of your
home window until you see delete buttons on the app icons.
Note: The app’s scheme TheMet might disappear from the scheme menu. If
this happens, select Manage Schemes… from the menu, then click
Autocreate Schemes Now:
691
SwiftUI Apprentice Chapter 25: Widgets
➤ Tap + in the upper left corner. If you’ve installed the app on a device, your gallery
looks something like this:
692
SwiftUI Apprentice Chapter 25: Widgets
693
SwiftUI Apprentice Chapter 25: Widgets
Your widget works! Now, you simply have to make it display information from
TheMet.
➤ Close the app then long-press the widget to open its menu and select Remove
Widget. Get into the habit of removing the widget after you’ve confirmed it’s
working. This is especially important if you’ve installed the app on your device.
While you’re developing your widget, it will display a new view every three seconds,
and that’s a real drain on your battery.
An Xcode error appears, because the widget doesn’t know about Object. You need to
add Object.swift to the widget target.
694
SwiftUI Apprentice Chapter 25: Widgets
➤ In Project navigator, select Object.swift. Show the File inspector and check the
Target Membership box for TheMetWidgetExtension:
Provider Methods
Adding the object property to SimpleEntry causes errors in Provider because it
creates SimpleEntry instances in its methods placeholder(in:),
getSnapshot(in:completion:), getTimeline(in:completion:) and also in the
preview. Provider methods are called by WidgetKit, not by any code you write.
• To display your widget for the first time, WidgetKit calls placeholder(in:) and
applies a redacted(reason: .placeholder) modifier to mask the view’s
contents. This method is synchronous: Nothing else can run on its queue until it
finishes, so don’t do any network downloads or complex calculations in this
method.
695
SwiftUI Apprentice Chapter 25: Widgets
extension Object {
static func sample(isPublicDomain: Bool) -> Object {
if isPublicDomain {
return Object(
objectID: 452174,
title: "Bahram Gur Slays the Rhino-Wolf",
creditLine: "Gift of Arthur A. Houghton Jr., 1970",
objectURL: "https://www.metmuseum.org/art/collection/
search/452174",
isPublicDomain: true,
primaryImageSmall: "https://images.metmuseum.org/
CRDImages/is/original/DP107178.jpg")
} else {
return Object(
objectID: 828444,
title: "Hexagonal flower vase",
creditLine: "Gift of Samuel and Gabrielle Lurie, 2019",
objectURL: "https://www.metmuseum.org/art/collection/
search/828444",
isPublicDomain: false,
primaryImageSmall: "")
}
}
}
This method returns a sample object, either in the public domain or not. The method
is static, so you can call it with Object.sample(isPublicDomain: true).
➤ Now, in TheMetWidget.swift, fix the errors one by one, or use the handy shortcut
Control-Option-Command-F for Editor ▸ Fix All Issues to insert the missing
argument. Replace all the Object placeholders in TheMetWidget.swift with:
Object.sample(isPublicDomain: true)
696
SwiftUI Apprentice Chapter 25: Widgets
➤ Create a new SwiftUI View file named WidgetView.swift, making sure you set
TheMetWidgetExtension as its target:
The widget needs an Entry to display and, to start, you’ll display the object’s title.
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: true)))
.previewContext(WidgetPreviewContext(family: .systemSmall))
697
SwiftUI Apprentice Chapter 25: Widgets
import WidgetKit
Note: Xcode might fail to build, complaining that “Embedded binary is not
signed with the same certificate as the parent app” or “Reference to invalid
associated type ‘Entry’ of type ‘Provider’”. Changing Timeline<Entry> to
Timeline<SimpleEntry> in the getTimeline(in:completion:) signature in
TheMetWidget.swift can get rid of this problem.
It works! Now, to make it look more like the app’s list view, you’ll need the
WebIndicatorView from ContentView.swift and the metBackground color defined
in Assets and ColorExtension.swift.
698
SwiftUI Apprentice Chapter 25: Widgets
Instead of adding the whole ContentView.swift to the widget target, you’ll move
WebIndicatorView to a separate SwiftUI View file, along with PlaceholderView.
The app displays a detail view for public-domain objects, with some text and an
image. By the end of this chapter, you’ll implement a deep-link to the object’s detail
view, so here you include a little system image to suggest what the user will see.
VStack {
Text("The Met") // 1
.font(.headline)
.padding(.top)
Divider() // 2
if !entry.object.isPublicDomain { // 3
WebIndicatorView(title: entry.object.title)
.padding()
.background(Color.metBackground)
.foregroundColor(.white)
} else {
DetailIndicatorView(title: entry.object.title)
.padding()
.background(Color.metForeground)
}
}
.truncationMode(.middle) // 4
.fontWeight(.semibold)
699
SwiftUI Apprentice Chapter 25: Widgets
1. You can’t use NavigationStack in a widget view, so you create your own title
with headline font size and top padding to push it away from the top edge.
3. You display the object’s title so it looks similar to how it appears in the app’s list.
4. You apply truncationMode and fontWeight to the VStack so it works for both
WebIndicatorView and DetailIndicatorView.
A Group of Previews
You can preview both sample objects by creating a Group:
Group {
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: true)))
700
SwiftUI Apprentice Chapter 25: Widgets
.previewContext(WidgetPreviewContext(family: .systemSmall))
// non-public-domain sample object
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: false)))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
➤ In the preview canvas, select the second Widget View to see the WebIndicator
view:
701
SwiftUI Apprentice Chapter 25: Widgets
➤ To see how the medium size might be useful, go to Object.swift and replace the
title of the public-domain object:
title: "\"Bahram Gur Slays the Rhino-Wolf\", Folio 586r from the
Shahnama (Book of Kings) of Shah Tahmasp",
702
SwiftUI Apprentice Chapter 25: Widgets
703
SwiftUI Apprentice Chapter 25: Widgets
.supportedFamilies([.systemMedium, .systemLarge])
WidgetView(entry: entry)
Here, you set WidgetView as the view to use when you want to display content.
.previewContext(WidgetPreviewContext(family: .systemMedium))
➤ Make sure the scheme is TheMet, then build and run the app. After it launches,
close the app.
If you had a small widget installed before this, it’s now gone. And, when you add a
widget, the small size isn’t an option:
704
SwiftUI Apprentice Chapter 25: Widgets
Notice the gallery uses the non-public-domain sample object, which means it’s
calling getSnapshot(in:completion:).
Note: If your widget doesn’t appear in the gallery, or doesn’t work correctly,
delete the app then build and run again. If the problem persists, restart the
simulator or device.
➤ Add a widget, then tap the screen or the Done button to exit screen-editing mode.
705
SwiftUI Apprentice Chapter 25: Widgets
This code creates each entry with the same Object.sample. You’ll modify the
method so it displays items in the app’s objects array. Waiting an hour between
entries is no good for testing purposes, so you’ll shorten the interval to a few
seconds.
While debugging, you limit the number of downloaded objects to a small number.
You set the query term to something that returns objects with distinct titles.
706
SwiftUI Apprentice Chapter 25: Widgets
let interval = 3
Task { // 1
do {
try await store.fetchObjects(for: query)
} catch {
store.objects = [
Object.sample(isPublicDomain: true),
Object.sample(isPublicDomain: false)
]
}
}
1. You call fetchObjects(for:) to fill the objects array and use this to create an
array of SimpleEntry values, three seconds apart. If fetchObjects(for:) fails,
you fill the array with the two sample objects.
707
SwiftUI Apprentice Chapter 25: Widgets
➤ Build and run, then close the app. Look for your widget and add it. Then watch it
display the first six persimmon objects:
Note: If you already had a Widget added in the home screen and it isn’t
showing the objects, remove it and add it again.
The widget might take a while to start displaying. In the meantime, it displays the
placeholder view. If nothing happens after a couple of minutes, build and run the app
again. After the sixth object, there’s a longer interval while the widget re-fetches the
same six objects.
Note: At the time of writing, the widget doesn’t work correctly on my iPhone.
It displays the first object, but doesn’t update.
708
SwiftUI Apprentice Chapter 25: Widgets
➤ Tap the widget to reopen your app. Set a new query term, wait for the list to
reload, then close the app. Your widget is still displaying persimmon objects:
2. Allow the user to set a different query term for the widget.
Later in this chapter, you’ll implement a deep link from the widget into your app to
open the detail view of the widget’s entry. This won’t make sense if the widget’s
array could be different from the app’s array. So this chapter chooses the first design
option.
709
SwiftUI Apprentice Chapter 25: Widgets
Xcode Tip: App group containers allow apps and targets to share resources.
Whenever the user changes the query term in your app, fetchObjects(for:)
downloads and decodes a new objects array. To share this array with your widget,
you’ll create an app group. Then, in TheMetStore.swift, you’ll write a file to this
app group, which you’ll read from in TheMetWidget.swift.
➤ If you haven’t signed the targets yet, do it now: In the Project navigator, select
the top level TheMet folder. For each target, change the bundle identifier prefix to
your organization instead of com.yourcompany and set the team.
➤ Now select the TheMet target. In the Signing & Capabilities tab, click +
Capability, then drag App Groups into the window. Click + to add a new container.
➤ Now select the TheMetWidgetExtension target and add the App Groups
capability. If necessary, scroll through the App Groups to find and select
group.your.prefix.TheMet.objects.
710
SwiftUI Apprentice Chapter 25: Widgets
import WidgetKit
WidgetCenter.shared.reloadTimelines(ofKind: "TheMetWidget")
extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier:
"group.your.prefix.TheMet.objects")!
}
}
This is simply some standard code for getting the app group container’s URL. Be sure
to substitute your bundle identifier prefix.
It makes sense to write this app group file just after you’ve decoded the data into the
objects array. To write an array to a file, you JSON-encode it. Then the widget
JSON-decodes the file contents.
711
SwiftUI Apprentice Chapter 25: Widgets
func writeObjects() {
let archiveURL = FileManager.sharedContainerURL()
.appendingPathComponent("objects.json")
print(">>> \(archiveURL)")
Here, you convert your array of Object values to JSON and save it to the app group’s
container.
➤ In fetchObjects(for:), add the following to call your new helper method before
the call to WidgetCenter:
writeObjects()
The existing call to WidgetCenter now tells the widget to reload its timeline
whenever your app has written a new array of objects into a file in the app group.
Now, you can read your objects array from the app group file.
712
SwiftUI Apprentice Chapter 25: Widgets
} catch {
print("Error: Can't decode contents")
}
}
return objects
}
This reads the Object values from the file that fetchObjects(for:) saved into the
app group’s container.
You read the objects array from the app group file and use it instead of
store.objects to create entries.
Note: If you changed the bundle identifier, you’ll end up having two apps.
Delete the old one before running the project.
713
SwiftUI Apprentice Chapter 25: Widgets
➤ Build and run, then close the app. Look for your widget and add it. Watch it display
a few persimmon objects, then tap the widget to reopen your app. Set query to
giraffe, wait for the list to reload, then close the app. After a while, your widget will
start displaying giraffe objects:
Note: If the widget keeps displaying persimmon objects, tap the widget or the
app’s icon to reopen the app, then close the app again.
Your widget is working well, and you could happily install it on your device now. If
you want to do so, skip down to the end of this chapter to change the timeline back
to one-hour intervals.
The next section adds a feature many users expect: When you tap the widget, the
app should open in the ObjectView for the current widget entry, if it’s public
domain. Tapping a non-public-domain object should open the metmuseum.org page
for the object.
714
SwiftUI Apprentice Chapter 25: Widgets
Note: When you install the app on a device, deep-linking works when the app
is running in the background. If the app isn’t running at all, tapping the widget
opens the app and shows the list.
For this app, the objectID property of Object uniquely identifies it. So the URL to
open “Hexagonal flower vase” is simply:
URL(string: "TheMet://828444")
And you can access this objectID value as the host property of the URL. So simple!
In Your Widget
➤ In WidgetView.swift, add this modifier to the top-level VStack, where you set
truncationMode and fontWeight:
.widgetURL(URL(string: "themet://\(entry.object.objectID)"))
715
SwiftUI Apprentice Chapter 25: Widgets
Note: In the medium and large widget sizes, you can use
Link(_:destination:) to attach links to different parts of the view.
In Your App
In your app, you implement .onOpenURL(perform:) to process the widget URL. You
attach this modifier to either the root view, in TheMetApp, or to the top level view of
the root view. For TheMet, you’ll attach this to the NavigationStack in
ContentView, because the perform closure must assign a value to a @State property
of ContentView.
You need to trigger navigation programmatically: You’ll add the widget’s object to a
navigation path to make the app open the correct navigation destination.
When the widget sends a widgetURL to the app, you’ll check whether the object is in
the public domain or not. Then, you’ll append either the object or its URL to path,
and NavigationStack will use this to select the correct navigationDestination.
Note: If your NavigationStack presents only one type of view, path can be an
array of the data type you pass to that view: [Object] for ObjectView or
[URL] for SafariView. You’ll still need to use NavigationLink(value:) with
the .navigationDestination(for:) modifier.
716
SwiftUI Apprentice Chapter 25: Widgets
NavigationStack(path: $path) {
You pass a binding to path to the navigation stack. Now, you can observe the current
state of the stack or modify path to specify where to navigate.
➤ Now, add this modifier to NavigationStack, at the same level as the task that
calls fetchObjects(for:):
.onOpenURL { url in
if let id = url.host,
let object = store.objects.first(
where: { String($0.objectID) == id }) { // 1
if object.isPublicDomain { // 2
path.append(object)
} else {
if let url = URL(string: object.objectURL) {
path.append(url)
}
}
}
}
1. Extract an id value from the widget URL, then find the first object whose
objectID matches this id value. Because url.host is a String, convert the
objectID value to String before comparing.
2. If the object is in the public domain, append it to path. Otherwise, append the
URL created from its objectURL.
➤ At the top of ContentView, change query to peony: This query returns more non-
public-domain objects, so you’ll be able to test that tapping these objects opens the
app in SafariView.
717
SwiftUI Apprentice Chapter 25: Widgets
➤ Build and run, wait for the list to load, then close the app and add your widget. Tap
a public-domain entry to see it open the ObjectView for that object:
718
SwiftUI Apprentice Chapter 25: Widgets
Note: The project in the final folder still displays every three seconds.
719
SwiftUI Apprentice Chapter 25: Widgets
You’re restoring the template code’s original timing. Now, your widget will add
entries one hour apart from each other. You can add it to your device’s home screen
with no worries about excessive battery use.
Refresh Policy
In getTimeline(in:completion:), after the for loop, you create a
Timeline(entries:policy:) instance. The template sets policy to .atEnd, so
WidgetKit creates a new timeline after the last date in the current timeline. As you
saw when the widget was downloading a small number of its own objects, the new
timeline doesn’t start immediately.
Of course, your current timeline fires at 3-second intervals, which is far from normal.
With a more normal interval, like one hour, you probably won’t notice any delay.
• after(_:) : Specify a Date when you want WidgetKit to refresh the timeline. Like
atEnd, this is more a suggestion to WidgetKit than a hard deadline.
• never: Use this policy if your app uses WidgetCenter to tell WidgetKit when to
reload the timeline. This is a good option for TheMet. You’ve already seen the
timeline reload almost immediately when you change a query option in your app.
You could add code to your app to call fetchObjects(for:) at the same time
every day, and this would also refresh your widget’s timeline.
720
SwiftUI Apprentice Chapter 25: Widgets
Key Points
• WidgetKit is still a relatively new API. You might experience some instability. You
can fix many problems by deleting the app or by restarting the simulator or device.
• To add a widget to your app, decide what app data you want to display and the
time interval between entries. Then, define a view for each size of widget — small,
medium, large — you want to support.
• Add app files to the widget target and adapt your app’s data structures and views
to fit your widgets.
• Create an app group to share data between your app and your widget.
721
26 Conclusion
We hope you’re excited by the new world of iOS and SwiftUI development that lies
before you!
By completing this book, you’ve gained the knowledge and tools you need to build
beautiful iOS apps. Set your imagination free and couple your creativity with your
newfound knowledge to create some impressive apps of your own.
There is so much more to the iOS ecosystem and Kodeco has the resources to help
your continued growth as an iOS developer:
• SwiftUI by Tutorials will expand your knowledge of SwiftUI and explores more
advanced developer topics.
• iOS App Distribution & Best Practices will guide you through the process of
publishing your app on the App Store.
• The many video courses and free tutorials on Kodeco explore diverse topics from
MapKit to Core Data to animation and much more.
If you have any questions or comments as you work through this book, please stop by
our forums at https://forums.kodeco.com and look for the particular forum category
for this book.
Thank you again for purchasing this book. Your continued support is what makes the
tutorials, books, videos, conferences and other things we do at Kodeco possible, and
we truly appreciate it!
722