SwiftUI Apprentice 1st Edition 2021
SwiftUI Apprentice 1st Edition 2021
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 of 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.
raywenderlich.com 2
SwiftUI Apprentice
raywenderlich.com 3
SwiftUI Apprentice
raywenderlich.com 4
SwiftUI Apprentice
raywenderlich.com 5
SwiftUI Apprentice
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Where to go from here?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Chapter 3: Prototyping the Main View . . . . . . . . . . . . . . . . . . . . . . 79
Creating the Exercise view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Creating the Header view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Playing a video . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Finishing the Exercise view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Chapter 4: Prototyping Supplementary Views . . . . . . . . . . . . . 107
Creating the History view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Creating the Welcome view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Chapter 5: Organizing Your App's Data . . . . . . . . . . . . . . . . . . . . 130
Creating the Exercise structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Structuring HistoryView data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Localizing your app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Chapter 6: Adding Functionality to Your App . . . . . . . . . . . . . . 156
Managing your app’s data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Navigating TabView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Interacting with page numbers and ratings . . . . . . . . . . . . . . . . . . . . . . . . . 164
Showing and hiding modal sheets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Chapter 7: Observing Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Showing/Hiding the timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Adding an exercise to history . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
raywenderlich.com 6
SwiftUI Apprentice
raywenderlich.com 7
SwiftUI Apprentice
Gradients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Chapter 11: Understanding Property Wrappers . . . . . . . . . . . 277
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
Tools for managing data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Managing UI state values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Accessing environment values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Managing model data objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Wrapping up property wrappers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Chapter 12: Apple App Development Ecosystem . . . . . . . . . . 298
A brief history of SwiftUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
SwiftUI vs. UIKit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Apple Developer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
Housekeeping & trouble-shooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
raywenderlich.com 8
SwiftUI Apprentice
raywenderlich.com 9
SwiftUI Apprentice
Challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Chapter 17: Interfacing With UIKit . . . . . . . . . . . . . . . . . . . . . . . . 435
UIKit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Using the Representable protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
UIKit delegate pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Picking photos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
NSItemProvider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Adding PhotoPicker to your app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444
Drag and drop from other apps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453
Chapter 18: Paths & Custom Shapes . . . . . . . . . . . . . . . . . . . . . . . 454
The starter project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
Shapes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
Paths. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
Strokes and fills . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
Clip shapes modal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Associated types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
Shape selection modal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472
Add the frame picker modal to the card . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Chapter 19: Saving Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
The starter project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
The saved data format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
When to save the data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481
JSON files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
Codable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
raywenderlich.com 10
SwiftUI Apprentice
raywenderlich.com 11
SwiftUI Apprentice
NavigationView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570
Header view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 579
Custom design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 584
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 595
Chapter 23: Just Enough Web Stuff . . . . . . . . . . . . . . . . . . . . . . . . 596
Servers and resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597
REST API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 600
Sending and receiving HTTP messages. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 601
Exploring api.raywenderlich.com. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 605
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 618
POST request & authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 624
Chapter 24: Downloading Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Asynchronous functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 626
Creating a REST request . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 626
Decoding JSON. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
Decoding the contents response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 640
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651
Chapter 25: Implementing Filter Options . . . . . . . . . . . . . . . . . . 652
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 652
From playground to app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653
Debugging with a breakpoint. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 658
Improving the user experience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 661
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 662
Implementing HeaderView options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 666
Implementing filters in FilterOptionsView . . . . . . . . . . . . . . . . . . . . . . . . . . 671
Implementing query filters in HeaderView. . . . . . . . . . . . . . . . . . . . . . . . . . 675
One last thing… . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 678
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 679
raywenderlich.com 12
SwiftUI Apprentice
raywenderlich.com 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.raywenderlich.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 sell this book
without prior authorization, or distribute it to friends, coworkers or students; they
would 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.
raywenderlich.com 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.
raywenderlich.com 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 12.5 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.
raywenderlich.com 16
ii Book Source Code &
Forums
• https://github.com/raywenderlich/suia-materials/tree/editions/1.0
Forums
We’ve also set up an official forum for the book at https://forums.raywenderlich.com/
c/books/swiftui-apprentice/79. This is a great place to ask questions about the book
or to submit any errors you may find.
raywenderlich.com 17
iii About the Cover
SwiftUI Apprentice
That’s not an 18th-century writer’s quill on the cover or even a gorgeous deep water
plant, but a sea pen — a creature belonging to the Cnidaria family, which includes
jellyfish, coral and sea anemone.
The earliest fossils date this colonial creature to the Cambrian period — some 541
million years before being featured on our book — and the pens are still thriving
today in both coastal and deep temperate and tropical waters.
You can observe the soft colorful stalks of the pens lodged in the seafloor, often
grouped like an underwater garden of feathers. And just as you glance at an
application on your phone and take in one interface that is actually made of many
working features, the sea pen isn’t one creature at all, but a colony of creatures.
raywenderlich.com 18
SwiftUI Apprentice About the Cover
The main stem is one polyp — the first to take root — that then supports many other
smaller polyps whose tentacles make up the fringes we see. And like the various
aspects of the seamless interfaces made with SwiftUI, the delicate polyps are cleverly
designed for different purposes to support the whole creature – intaking water,
maintaining structure, eating and reproducing. The pens have been observed
expanding and shrinking, articulating their frayed bodies, reacting to crabs and other
touch, and even glowing.
A sea pen may be stationary, like any application anchored within the confines of
your device. But the pens show us that with the right construction and design, you
can architect something cohesive, fluid, beautiful and dynamic.
raywenderlich.com 19
SwiftUI Apprentice Dedications
— Audrey Tam
— Caroline Begbie
raywenderlich.com 20
SwiftUI Apprentice Dedications
Richard Critz did double duty as editor and final pass editor for
this book. He is the iOS Team Lead at raywenderlich.com and 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. On Twitter, while being
mainly read-only, he can be found @rcritz. The rest of his
professional life can be found at www.rwcfoto.com.
raywenderlich.com 21
SwiftUI Apprentice Dedications
raywenderlich.com 22
vi 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.
raywenderlich.com 23
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.
raywenderlich.com 24
1 Chapter 1: Checking Your
Tools
By Audrey Tam
You’re eager to dive into this book 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.
Getting started
To develop iOS apps, you need a Mac with Xcode installed. If you want to run your
apps on an iOS device, you need an Apple ID. And if you have an account on GitHub
or similar, you’ll be able to 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 29 GB free space on your Mac’s drive.
➤ Open the App Store app, then search for and GET Xcode. This is a large download
so it takes a while. Plenty of time to fix yourself a snack while you wait. Or, to stay in
the flow, browse Chapter 12, “Apple App Development Ecosystem”.
raywenderlich.com 25
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ When the installation finishes, OPEN it from the App Store page:
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 installation, you’ll see this window: Install
additional required components?:
➤ When this installation process finishes, you might have to open Xcode again. The
first time you open Xcode, you’ll see this Welcome window:
raywenderlich.com 26
SwiftUI Apprentice Chapter 1: Checking Your Tools
If you don’t want to see this window every time you open Xcode, uncheck “Show this
window when Xcode launches”. You can manually open this window from the Xcode
menu Window ▸ Welcome to Xcode or press Shift-Command-1. And, there is an
Xcode menu item to perform each of the actions listed in this 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.
raywenderlich.com 27
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Select iOS ▸ App and click Next. Now, you get to name your project:
• 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.FirstApp.
When you submit your app to the App Store, this bundle identifier uniquely
identifies your app.
raywenderlich.com 28
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Click Next. Here’s where you decide where to save your new project.
Note: If you forget where you saved a project, you can find it by selecting File
▸ Show in Finder from the Xcode menu.
➤ 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.
raywenderlich.com 29
SwiftUI Apprentice Chapter 1: Checking Your Tools
Looks like there’s a lot going on! Don’t worry, most iOS developers know enough
about Xcode to do their job, but almost no one knows how to use all of it. Plus Apple
changes and adds to it every year. The best (and only) way to learn it is to jump in
and start using it.
You can hide or show the navigator with the toolbar button just above it, and the
same for the inspectors. The debug area has a hide 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.
raywenderlich.com 30
SwiftUI Apprentice Chapter 1: Checking Your Tools
Note: There’s a handy cheat sheet of Xcode keyboard shortcuts in the assets
folder for this chapter. Not a complete list, just the ones that many people use.
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:
2. Source Control: View Git repository working copies, branches, commits, tags,
remotes and stashed changes.
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 very handy for
projects with a lot of files in a deeply nested hierarchy.
raywenderlich.com 31
SwiftUI Apprentice Chapter 1: Checking Your Tools
Editor
When you’re working in a code file, the Editor shows the code and a Minimap. The
minimap is useful for long code files with many properties and methods. You can
hover the cursor over the minimap to locate a specific property, then click to go
directly to it. You don’t need it for the apps in this book, so you may want to hide it
via the button in the upper right corner of the editor.
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 or four 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-4:
The fourth tab appears when you select a file in the Project navigator. If you select a
folder, you get only the first three tabs.
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.
raywenderlich.com 32
SwiftUI Apprentice Chapter 1: Checking Your Tools
Navigation preferences
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
set preferences for which shortcut does what. To avoid confusion while working
through this book, you’ll set your preferences to match the instructions you’ll see.
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.
raywenderlich.com 33
SwiftUI Apprentice Chapter 1: Checking Your Tools
import
The first line of code is an import statement:
import SwiftUI
This works just like in most programming languages. It allows your code to access
everything in the built-in SwiftUI module. See what happens if it’s missing.
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.
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.
raywenderlich.com 34
SwiftUI Apprentice Chapter 1: Checking Your Tools
The required component of the View protocol is the body computed property, which
returns a View. In this case, it returns a Text view that displays the usual “Hello,
world!” text.
Swift Tip: If there’s only a single code statement, you don’t need to explicitly
use the return keyword.
The Text view has a padding modifier — an instance method of View — that adds
space around the text. You can see it in this screenshot:
raywenderlich.com 35
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Select the Text view in either the code editor or the canvas, then select the
Attributes inspector. Click in the Add Modifier field and wait a short while until
the modifiers menu appears:
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 to open the Attributes
inspector pop-up window.
struct ContentView_Previews
Below ContentView is a ContentView_Previews structure.
raywenderlich.com 36
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Press Command-Z to undo or, if the five lines are still selected, press Command-/
to uncomment them.
For most apps, ContentView.swift is just the starting point. Often, ContentView
just defines the app’s organization, orchestrating several subviews. And usually,
you’ll define these subviews in separate files.
raywenderlich.com 37
SwiftUI Apprentice Chapter 1: Checking Your Tools
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.
The new file window displays a lot of options! The one you want is iOS ▸ User
Interface ▸ SwiftUI View. In Chapter 5, “Organizing Your App’s Data”, you’ll get to
create a Swift File.
raywenderlich.com 38
SwiftUI Apprentice Chapter 1: Checking Your Tools
Swift Tip: Swift convention is to name types (like struct) with CamelCase
and properties and methods with camelCase.
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 looks almost the same as the ContentView of a
new project.
import SwiftUI
Like ContentView, the view’s body contains Text("Hello, world!"), but there’s no
padding.
Now, in ContentView.swift, in the code editor, delete the Text view, then type bye.
Xcode suggests some auto-completions:
raywenderlich.com 39
SwiftUI Apprentice Chapter 1: Checking Your Tools
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, then add parentheses, so the line looks like this:
ByeView()
• FirstAppApp.swift: This file contains the code for your app’s entry point. This is
what actually launches your app.
@main
struct FirstAppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
raywenderlich.com 40
SwiftUI Apprentice Chapter 1: Checking Your Tools
The @main attribute marks FirstAppApp 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.
Info.plist
raywenderlich.com 41
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.
• Products: This is where Xcode stores your app after you build and run the project.
A project can contain other products, like a Watch app or a framework.
In this list, the last two items are groups. Groups in the Project navigator appear to
be folders, but they don’t necessarily match up with folders in Finder. In particular,
there’s no Products folder in your project in Finder.
➤ In the Project navigator, select Products ▸ FirstApp.app, then show the File
inspector:
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.
raywenderlich.com 42
SwiftUI Apprentice Chapter 1: Checking Your Tools
Xcode Preferences
Xcode has a huge number of preferences you can set to make your time in Xcode
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 preconfigured font and color themes for you to choose from or
modify.
raywenderlich.com 43
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.
raywenderlich.com 44
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:
• 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:
raywenderlich.com 45
SwiftUI Apprentice Chapter 1: Checking Your Tools
This can be incredibly useful when you’re trying to find your way around some
complex deeply-nested code.
Adding accounts
You can access some Xcode features by adding login information for your Apple ID
and source control accounts.
Preferences ▸ Accounts
➤ Add your Apple ID. If you have a separate paid Apple Developer account, add that
too.
To run your app on a device, you’ll need to select a Team. 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.
To add capabilities like push notifications or Apple Pay to your app, you need to set
Team to a Developer Program account.
Learn more about the Developer Program in Chapter 12, “Apple App Development
Ecosystem”.
raywenderlich.com 46
SwiftUI Apprentice Chapter 1: Checking Your Tools
• 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.
New Remote...
raywenderlich.com 47
SwiftUI Apprentice Chapter 1: Checking Your Tools
Create-remote options
And here it is:
raywenderlich.com 48
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.
• Scheme menu: This button’s label is the name of the app. Select, edit or manage
schemes. Each product has a scheme. FirstApp has only one product, so it has only
one scheme.
• Run destination menu: This menu defaults to its last item, currently iPod touch.
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: Label is a + sign. Opens the library of views, modifiers, code
snippets and media and colors stored in Assets. Option-click this button to keep
the library open.
• Code review button: If this project has a local git repository, this button shows a
diff of the current version of the current file and the most recent committed
version. You can choose earlier committed versions from a menu.
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.
raywenderlich.com 49
SwiftUI Apprentice Chapter 1: Checking Your Tools
raywenderlich.com 50
SwiftUI Apprentice Chapter 1: Checking Your Tools
For example:
Note: To zoom in or out on the preview canvas, use the + or - buttons in the
canvas toolbar.
raywenderlich.com 51
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 FirstApp,
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”.
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. :]
raywenderlich.com 52
SwiftUI Apprentice Chapter 1: Checking Your Tools
Check do-not-show-this-again.
➤ Don’t click Stop, although that will work: The currently running process will
stop, and the new process will run. And this will happen every time you forget to stop
the app. It takes just a moment, but it jars a little. Every time. And it’s easy to get rid
of.
The app loads with your new change. But that’s not what I want to show you.
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.
Apple does its best to protect its users from malicious apps. Part of this protection is
ensuring Apple knows who is responsible for every app on your device. Before you
can install your app from Xcode onto your device, you need to select a team (your
Apple ID), to get a signing certificate from Apple.
raywenderlich.com 53
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ In the project page, select the target. In the Signing & Capabilities tab, check
Automatically manage signing, then select your account from the Team menu:
Note: The Bundle Identifier of your project uses your organization name
because you created it as a new project. The other apps in this book have
starter projects with com.raywenderlich as the organization. If you want to
run these apps on an iOS device, you need to change the organization name in
the bundle ID to something that’s uniquely yours. This is because one of the
authors has already signed the app with the original bundle ID, and you’re not
a member of our teams.
To run this book’s apps on your iOS device, it must have iOS 14 installed. If it’s not
the absolute latest update, select the project, then set its iOS Deployment Target
to match your device.
raywenderlich.com 54
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Connect your device to your Mac with a cable. Use an Apple cable, as other-brand
cables might not work for this purpose.
Note: If your account is a paid Apple Developer account, you won’t need to do
the next several steps. Running your app on your device will just work.
The first time you connect a device to your Mac, the device will ask Trust This
Computer?
➤ Select your device from the run destination menu: It appears at the top, above the
simulators:
raywenderlich.com 55
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Unlock your device, then build and run your project. Keep your device screen
active until the app launches on your device.
This is the first time you’re running an app on a device, so there are several extra
steps that Apple makes you perform, mainly trying to make sure nothing nasty
installs itself on your device.
➤ First, you need to allow codesign to access the certificate that Xcode stored in
your keychain:
Next, you’ll see FirstApp’s app icon appear on the screen of your device, but this
error message appears on your Mac:
raywenderlich.com 56
SwiftUI Apprentice Chapter 1: Checking Your Tools
➤ Well, the app icon is on your device’s screen, so why not tap it to see what
happens?
Untrusted Developer
You can allow using these apps in Settings is a pretty minimal hint, but open Settings
to see what’s there. You’ll probably never guess where to look, so here are the
relevant screenshots:
➤ Tap Apple Development…, then tap Trust “Apple Development… and finally,
tap Trust.
You won’t need to do this again unless you delete all your apps from this device.
raywenderlich.com 57
SwiftUI Apprentice Chapter 1: Checking Your Tools
What matters is, 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.
raywenderlich.com 58
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 Preferences.
• You can set some navigation keyboard shortcuts in Preferences, to match the
instructions in this book.
• The template project defines an App that launches with ContentView, which
displays “Hello, world!” in a Text view.
• 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.
• You must add an Apple ID account in Xcode Preferences to run your app on an iOS
device.
• The first time you run your project on an iOS device, Apple requires you to
complete several “Trust” steps.
raywenderlich.com 59
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.
HIITFit screens
There’s a lot going on in these screens, especially the one with the exercise video.
raywenderlich.com 60
SwiftUI Apprentice Chapter 2: Planning a Paged App
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.
• 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 10, “Refining Your App”, you’ll fine-tune the appearance to look like the
screenshots above.
raywenderlich.com 61
SwiftUI Apprentice Chapter 2: Planning a Paged App
• 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 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.
raywenderlich.com 62
SwiftUI Apprentice Chapter 2: Planning a Paged App
Creating Pages
Skills you’ll learn in this section: adding a Git repository to an existing
project; 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.
This project doesn’t have a Git repository. You could use the git init command
line, but Xcode provides a quick way.
raywenderlich.com 63
SwiftUI Apprentice Chapter 2: Planning a Paged App
Now you’ll see markers when you modify or add files, and you can commit every
major addition as you build this app. I’ll suggest the first few commits, then it will be
up to you to make sure you commit a working copy before undertaking a new task.
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.
➤ In the canvas, click Resume if you don’t see ContentView, then double-click the
Text view: You’ve selected “Hello world” in the code:
raywenderlich.com 64
SwiftUI Apprentice Chapter 2: Planning a Paged App
Note: Don’t press enter after typing Welcome. If necessary, refresh the
preview.
The Text view just 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.
➤ Click anywhere outside the Text view to deselect “Welcome”, then select the Text
view again. 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.
➤ Change “V” to “H” to see the two views displayed in a horizontal stack:
raywenderlich.com 65
SwiftUI Apprentice Chapter 2: Planning a Paged App
Using TabView
Here’s how easy it is to create a TabView:
Here’s how you label the tabs. It’s actually very 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 to use the pop-up inspector. It uses less space.
raywenderlich.com 66
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Click in the Add Modifier field, then type tab and select Tab Item from the
menu:
Text("Welcome")
.tabItem { Item Label }
.tabItem { Text("Welcome") }
TabView {
Text("Welcome")
.tabItem { Text("Welcome") }
Text("Exercise 1")
.tabItem { Text("Exercise 1") }
Text("Exercise 2")
.tabItem { Text("Exercise 2") }
}
raywenderlich.com 67
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Click the Live Preview button to see how this would work on a device:
➤ Now tap a tab label to switch to that tab. This is the way tab views normally
operate. To make the tabs behave like pages, add this modifier to the TabView:
.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))
raywenderlich.com 68
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Check out the 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.
raywenderlich.com 69
SwiftUI Apprentice Chapter 2: Planning a Paged App
Grouping files
Skills you’ll learn in this section: creating and grouping project files
raywenderlich.com 70
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ Select the three view files, then right-click and select New Group from Selection:
Group folders just help you organize all the files in your project. In Chapter 5,
“Organizing Your App’s Data”, 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;
Fix button in error messages; placeholders in auto-completions
➤ Back 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")
}
raywenderlich.com 71
SwiftUI Apprentice Chapter 2: Planning a Paged App
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.
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.
raywenderlich.com 72
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ In Chapter 5, “Organizing Your App’s Data”, you’ll create an Exercise data type
but, for this prototype, in ExerciseView.swift, simply create two arrays at the top of
ExerciseView, just above var body:
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.
➤ Still inside ExerciseView, above the var body property, add this property:
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 and type 0. So now you have this line of code:
ExerciseView(index: 0)
raywenderlich.com 73
SwiftUI Apprentice Chapter 2: Planning a Paged App
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.
➤ Change "Hello, World!" to the exercise name for this index value.
Text(exerciseNames[index])
Now there’s a placeholder for the index value: What should you type there?
Looping
Skills you’ll learn in this section: ForEach, Range vs. ClosedRange;
developer documentation; initializers with parameters
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 TabView with this code:
ForEach loops over the range 0 to 4 but, because of that < symbol, not including 4.
raywenderlich.com 74
SwiftUI Apprentice Chapter 2: Planning a Paged App
Each integer value 0, 1, 2 and 3 creates an ExerciseView with that index value. 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 ..<:
raywenderlich.com 75
SwiftUI Apprentice Chapter 2: Planning a Paged App
➤ 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. Sure enough, Range is “A half-open interval from a lower bound up to, but not
including, an upper bound”, which matches the Quick Help you saw for ..<.
➤ You won’t need the TabView index dots. Open ContentView.swift and change:
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(
PageIndexViewStyle(backgroundDisplayMode: .always))
to:
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
This is a good place to commit the changes you’ve made to your project with a
commit message like “Set up paging tab view.”
raywenderlich.com 76
SwiftUI Apprentice Chapter 2: Planning a Paged App
Xcode Tip: To commit changes to your local Git repository, select Source
Control ▸ Commit… or press Option-Command-C. If asked, check all the
changed files. Enter a commit message, 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
raywenderlich.com 77
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.
raywenderlich.com 78
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
• 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.
raywenderlich.com 79
SwiftUI Apprentice Chapter 3: Prototyping the Main View
• 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 just 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.
There’s a lot to do in this view, so you’ll start by creating an outline with placeholder
Text views.
➤ Open ExerciseView.swift.
➤ The canvas preview uses the run destination simulated device by default. You’ll
start by laying out the interface for the iPad version of HIITFit, so select an iPad
simulator:
raywenderlich.com 80
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")
}
The first Text view is the starting point for the Header view. You’ll create the
Header and Rating views in their own files. The video player, timer and buttons are
simple views, so you’ll just create them directly in ExerciseView.
raywenderlich.com 81
SwiftUI Apprentice Chapter 3: Prototyping the Main View
You’ll create this Header view by adding code to ExerciseView, then you’ll extract it
as a subview and move it to its own file.
➤ To prepare for the later extraction, embed the first Text view in a VStack: Hold
down the Command key, click Text(exerciseNames[index]) then select Embed in
VStack:
Note: This version of the Command-click menu appears only when the
canvas is open. If you don’t see the Embed in VStack option, press Option-
Command-Return to open the canvas.
VStack {
Text(exerciseNames[index])
}
raywenderlich.com 82
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Click in the Add Modifier field, then type font and select Font from the menu:
.font(.title)
➤ Xcode suggests the font size title, but this is only a placeholder. To “accept” this
value, click .title, then press Return to set it as the value.
raywenderlich.com 83
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ 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:
Show the Font menu in the Attributes inspector for font or title.
➤ Select Large Title from the menu: Now “Squat” is even bigger!
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.
raywenderlich.com 84
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ Here’s another way to see the font menu. Select .largeTitle and replace it with .
— Xcode’s standard auto-suggest mechanism lists the possible values:
➤ Once you’re familiar with SwiftUI modifiers, you might prefer to just type.
Delete .font(.largeTitle) and type .font. Xcode auto-suggests two font
methods:
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).
raywenderlich.com 85
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 view and search the collection. Download
and install it from apple.co/3hWxn3G. 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 and select the Indices category.
Scroll down to the numbers:
raywenderlich.com 86
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ SF Symbol names can be long, but it’s easy to copy them from the app. Select a
symbol, then open the app’s Edit menu:
➤ Select the no-fill “1.circle” symbol, press Shift-Command-C, then use the name
to add this line of code below the title Text:
Image(systemName: "1.circle")
Image is another built-in SwiftUI View, and it has an initializer that takes an SF
Symbol name as a String.
HStack {
Image(systemName: "1.circle")
Image(systemName: "2.circle")
Image(systemName: "3.circle")
Image(systemName: "4.circle")
}
raywenderlich.com 87
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ You could add .font(.title) 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)
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.
You’ll use Xcode’s refactoring tool, which works well. But it’s always a good idea to
commit your code before a change like this, just in case. Select Source Control ▸
Commit… or press Option-Command-C.
raywenderlich.com 88
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Extracting a subview
OK, drum roll …
➤ Command-click the VStack containing the title Text and the page numbers
HStack, then select Extract Subview from the menu:
raywenderlich.com 89
SwiftUI Apprentice Chapter 3: Prototyping the Main View
The error flag shows where you need a parameter. The index property is local to
ExerciseView, so you can’t use it in HeaderView. You could just 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])
➤ Now press Command-N to create a new SwiftUI View file and name it
HeaderView.swift. Because you were in ExerciseView.swift when you pressed
Command-N, the new file appears below it and in the same group folder.
Your new file opens in the editor with two error flags:
➤ To fix the first, in ExerciseView.swift, select the entire 17 lines of your new
HeaderView 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")
Because you pass only the exercise name to HeaderView, the preview doesn’t need
access to the exerciseNames array.
raywenderlich.com 90
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Selecting Preview Layout from the Attributes inspector for Header view
➤ Select Preview Layout to add this modifier:
.previewLayout(.sizeThatFits)
➤ The placeholder value is sizeThatFits, and this is what you want, but you must
“accept” it. Click sizeThatFits, then press Return to set it as the value.
raywenderlich.com 91
SwiftUI Apprentice Chapter 3: Prototyping the Main View
You’ve made a copy of the preview in the canvas and in the code:
Group {
HeaderView(exerciseName: "Squat")
.previewLayout(.sizeThatFits)
HeaderView(exerciseName: "Squat")
.previewLayout(.sizeThatFits)
}
Just like when you duplicated the Text view, Xcode embeds the two views in a
container view. This time it’s a Group, which doesn’t specify anything about layout.
Its only purpose is to wrap multiple views into a single view.
Swift Tip: The body and previews properties are computed properties. They
must return a value of type some View, so what’s inside the closure must be a
single view.
➤ Now you can modify the second preview. Click its Inspect Preview button:
Second preview with dark color scheme and accessibilityLarge font size
raywenderlich.com 92
SwiftUI Apprentice Chapter 3: Prototyping the Main View
That’s how easy it is to see how this view appears on a device with these settings.
Now return to ExerciseView.swift, where the header is just the way you left it.
Playing a video
Skills you’ll learn in this section: using AVPlayer and VideoPlayer; using
bundle files; optional types; Make Conditional; using GeometryReader; adding
padding.
import AVKit
Xcode complains it “cannot find ’url’ in scope”, so you’ll define this value next.
raywenderlich.com 93
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 = index {...} to check whether an optional has a value.
The index on the right of = is the optional value. If it has a value, the index on
the left of = is an Int and the condition is true. If the optional has no value,
the assignment = is not performed and the condition is false. You can also
check index != nil, which returns true if index has a value.
Note: You’ll learn more about the app bundle in Chapter 8, “Saving Settings”
and about optionals in Chapter 9, “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. ;]
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.
raywenderlich.com 94
SwiftUI Apprentice Chapter 3: Prototyping the Main View
➤ It’s easy to test this else code: Create a typo by changing the withExtension
argument to np4, then refresh the preview:
➤ Now click Live Preview, then click the play button to watch the video. If the play
button disappears, try this: Click on the video then press Space.
raywenderlich.com 95
SwiftUI Apprentice Chapter 3: Prototyping the Main View
GeometryReader { geometry in
The video player now uses only 45% of the screen height:
raywenderlich.com 96
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Adding padding
➤ The header looks a little squashed. Control-Option-click HeaderView to add
padding to its bottom:
raywenderlich.com 97
SwiftUI Apprentice Chapter 3: Prototyping the Main View
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.
Now 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
To finish off the Exercise view, add the timer and buttons, then create the Ratings
view.
These are high-intensity interval exercises, so the timer counts down from 30
seconds.
raywenderlich.com 98
SwiftUI Apprentice Chapter 3: Prototyping the Main View
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, 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 90 points to make a really big timer.
raywenderlich.com 99
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()
raywenderlich.com 100
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 7, “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 6, “Adding Functionality to Your App”.
raywenderlich.com 101
SwiftUI Apprentice Chapter 3: Prototyping the Main View
.previewLayout(.sizeThatFits)
A rating view is usually five stars or hearts, but the rating for an exercise should
reflect the user’s exertion.
➤ To find a more suitable rating symbol, open the SF Symbols app and select the
Health category:
➤ Replace the boilerplate Text with this code, pasting the symbol name in between
double quotation marks:
Image(systemName: "waveform.path.ecg")
.foregroundColor(.gray)
You’ve added the SF Symbol as an Image and set its color to gray.
raywenderlich.com 102
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 previews! 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)
}
}
raywenderlich.com 103
SwiftUI Apprentice Chapter 3: Prototyping the Main View
That’s better! Now the symbols are all in a row. But they’re very small.
.font(.largeTitle)
Bigger is better!
➤ 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()
raywenderlich.com 104
SwiftUI Apprentice Chapter 3: Prototyping the Main View
raywenderlich.com 105
SwiftUI Apprentice Chapter 3: Prototyping the Main View
Key points
• SwiftUI is declarative: Simply declare views in the order you want them to appear.
• Create separate views for the elements of your user interface. This makes your
code easier to read and maintain.
• Use the SwiftUI convention of putting each modifier on its own line. This makes it
easy to move or delete a modifier.
• Xcode and SwiftUI provide auto-suggestions and default values that are often what
you want.
• Let Xcode help you avoid errors: Use the Command-menu to embed a view in a
stack or in an if-else closure, or extract a view into a subview.
• The SF Symbols app provides icon images you can configure like text.
• Previews are an easy way to check how your interface appears for different user
settings.
raywenderlich.com 106
4 Chapter 4: Prototyping
Supplementary Views
By Audrey Tam
• Welcome
• History
• Success
In the previous chapter, you laid out the Exercise view. In this chapter, you’ll lay out
the History and Welcome views then complete the challenge to create the Success
view. And your app’s prototype will be complete.
In this chapter, you’ll just do a mock-up of the list view. After you create the data
model in the next chapter, you’ll modify this view to use that data.
➤ Continue with your project from the previous chapter or open the project in this
chapter’s starter folder.
raywenderlich.com 107
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ 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"))
.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.
raywenderlich.com 108
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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.
DateFormatter has only the default empty initializer. You create one, 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.
raywenderlich.com 109
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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.
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. Apple’s
documentation for Formatting Quick Help is at apple.co/33hohbk.
raywenderlich.com 110
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
To display the completed exercises for each day, you’ll use ForEach to loop over the
elements of the exercises1 and exercises2 arrays.
In ContentView, you looped over a number range. Here, you’re using the third
ForEach initializer:
The exercises1 array is the Data and \.self is the key path to each array element’s
identifier. The \.self key path just says 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:
raywenderlich.com 111
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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:
Button(action: {}) {
Image(systemName: "xmark.circle")
}
raywenderlich.com 112
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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’ll move the button into the top right
corner of the view, where there’s nothing in the VStack to cover it.
You can specify an alignment value for any kind of stack, but they 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) {
raywenderlich.com 113
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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)
➤ 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")
raywenderlich.com 114
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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.
Image(systemName: "hand.wave")
Now to rename the exerciseName property. Its purpose is really to be the title of the
page, so titleText is a better name for it.
raywenderlich.com 115
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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.
raywenderlich.com 116
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ The first instance is highlighted differently. Type titleText, and all the instances
change:
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.
raywenderlich.com 117
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
ZStack {
VStack {
HeaderView(titleText: "Welcome")
}
}
Spacer()
Button("History") { }
.padding(.bottom)
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:
raywenderlich.com 118
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ Now to fill in the middle space. Add this layer to the ZStack:
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.
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.
Modifying an Image
➤ Look in Assets.xcassets for the step-up image:
raywenderlich.com 119
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
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. You
might have to close then reopen the inspector panel.
raywenderlich.com 120
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ First, you must add a modifier that lets you resize the image. In the Add Modifier
field, type resiz then select Resizable.
➤ 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:
raywenderlich.com 121
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
HStack(alignment: .bottom)
raywenderlich.com 122
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
You’ve done enough to make it look welcoming. :] In Chapter 10, “Refining Your
App”, you’ll add a few more images.
.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.
The other buttons you’ve created have only text labels. But it’s easy to label a Button
with text and an image.
raywenderlich.com 123
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
➤ 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()
If you start typing Button in the code editor, Xcode will auto-suggest this official
Button signature:
Swift Tip: You can move the last closure argument of a function call outside
the parentheses into a trailing closure.
raywenderlich.com 124
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
If you drag a Button into your view from the canvas library, you get this version of the
Button signature with the label content as 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.
The other buttons you’ve created use an even simpler syntax for Button, where the
button’s label is just a String, and the button’s action is in the trailing closure. For
example:
Button("History") { }
➤ The Label view is another way to label a Button with text and image. Comment
out (Command-/) the Text and Image lines, then write this line in the label
closure:
Note: You can modify a Label with labelStyle to show only the text or only
the image.
raywenderlich.com 125
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.
➤ Just for fun, give this button a border. Add this modifier below padding():
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 2))
raywenderlich.com 126
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.
3. The SF 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.
raywenderlich.com 127
SwiftUI Apprentice Chapter 4: Prototyping Supplementary Views
5. For the three small lines of text, you could use three Text views. Or refer to our
Swift Style Guide bit.ly/30cHeeL to see how to create a multi-line string. Text has
a multilineTextAlignment modifier. This text is colored gray.
raywenderlich.com 128
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.
• 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.
raywenderlich.com 129
5 Chapter 5: Organizing
Your App's Data
By Audrey Tam
In this chapter, you’ll use structures and enumerations to organize your app’s data.
The compiler can then help you avoid errors like using the wrong type value or
misspelling a string.
Your app needs sample data during development. You’ll use a compiler directive to
create this data only during development. And you’ll store your development-only
code and data in Preview Content to exclude them from the release version of your
app.
You’ll learn how to localize your app to expand its audience. You’ll replace user-
facing text with NSLocalizedString instances, generate the development language
(English) Localizable.strings file, then use this as the reference language resource
file for adding another language.
➤ Continue with your project from the previous chapter or open the project in this
chapter’s starter folder.
raywenderlich.com 130
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
For the initial layout of HIITFit, you used two arrays of strings for the exercise names
and video file names. This minimalist approach helped you see exactly what data
each view needs, and this helped to keep the previews manageable.
But you had to manually ensure the strings matched 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.
➤ Create a new Swift file and name it Exercise.swift. Add the following code below
import Foundation:
struct Exercise {
let exerciseName: String
let videoName: String
In the previous chapters, you’ve created and used structures that conform to the
View protocol. This Exercise structure models your app’s data, encapsulating the
exerciseName and videoName properties.
raywenderlich.com 131
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
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 { ... }.
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: ”Sun
Salute” for sunSalute, for example. You access this String with
ExerciseEnum.sunSalute.rawValue, for example.
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")
]
}
raywenderlich.com 132
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
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 just one exercises array no matter how many Exercise
instances you create. You use the type name to access it: Exercise.exercises.
You create the exercises array with an array literal: a comma-separated list of
values, enclosed in square brackets. Each value is an instance of Exercise, supplying
the raw value of an enumeration case and the corresponding video file name.
As the word suggests, an extension extends a named type. The starter project includes
two extensions, in DateExtension.swift and ImageExtension.swift. Date and
Image are built-in SwiftUI types but, using extension, you can add methods and
computed or type properties.
Here, Exercise is your own custom type, so why do you have an extension? In this
case, it’s just for housekeeping, to keep this particular 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. When you organize your code like this, you can more easily see
where to add features or look for bugs.
Instead of a magic number, you use the number of exercises elements as the upper
bound of the ForEach range.
Note: You could pass the whole Exercise item to ExerciseView but, in the
next chapter, you’ll use index to decide when to show SuccessView.
raywenderlich.com 133
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
Exercise.exercises[index].exerciseName
Exercise.exercises[index].videoName
raywenderlich.com 134
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
HistoryView currently uses hard-coded dates and exercise lists to mock up its
display. You need a data structure for storing your user’s activity. And, in the next
chapter, you’ll implement the Done button to add completed exercise names to this
data structure.
Creating HistoryStore
➤ 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] = []
}
raywenderlich.com 135
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
An ExerciseDay has properties for the date and a list of exercise names completed
by your user on that date.
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.
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
])
]
}
}
raywenderlich.com 136
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
This is pretty much the same sample data you had before, now stored in your new
Exercise and ExerciseDay structures. In the next chapter, you’ll add a new
ExerciseDay item, so I’ve moved 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, the exerciseDays property, 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 toolbar button labeled HIITFit. It also
opens the run destination menu alongside. Select Edit Scheme…, then select
the Info tab.
raywenderlich.com 137
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
Refactoring HistoryView
➤ In HistoryView.swift, delete the Date properties and the exercise arrays, then add
this property:
HistoryStore now encapsulates all the information in the stored properties today,
yesterday and the exercises arrays.
raywenderlich.com 138
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
The Form closure currently displays each day in a Section. Now that you have an
exerciseDays array, you should loop over the array.
Form {
ForEach(history.exerciseDays) { day in
Section(
header:
Text(day.date.formatted(as: "MMM d"))
.font(.headline)) {
ForEach(day.exercises, id: \.self) { exercise in
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 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.
raywenderlich.com 139
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
You surely want to maximize the audience for your app. A good way to do this is to
translate it into languages other than English. This is called localization.
You need to complete the following tasks. You can do these in a different order, but
this workflow will save you some time:
2. Decide which user-facing strings you want to localize and replace these with
NSLocalizedString instances.
5. In the Localizable.strings file for the new language, replace English strings with
translated strings.
Getting started
➤ In the Project navigator, select the top-level HIITFit folder. This opens the project
page in the editor:
raywenderlich.com 140
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
If you don’t see the projects and targets list, click the button in the upper left corner.
UIKit projects have a Base.lproj folder containing .storyboard and/or .xib files.
SwiftUI projects with a LaunchScreen.storyboard file also have this folder. These
files are already marked as localized in the development language (English). When
you add another language, they appear in a checklist of resources you want to
localize in the new language. So projects like these have at least one base-localized
file.
If you don’t do anything to localize your app, you won’t have any development-
language-localized files. All the user-facing text in your app just appears the way you
write it. As soon as you decide to add another language, you’ll replace this text with
NSLocalizedString instances. For this mechanism to work, you’ll also have to
localize in the development language.
raywenderlich.com 141
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
You could add another language now, but the workflow you’ll follow below saves you
some time.
Creating en.lproj/Localizable.strings
This step sets up localization for the project’s development language (English). First,
you’ll create a Strings file named Localizable.strings.
➤ To create this file in the HIITFit group, but not in the Views group, select
Assets.xcassets or HIITFitApp.swift in the Project navigator.
➤ Press Command-N to open the new file window, search for string, then select
Strings File:
raywenderlich.com 142
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
➤ Select Localizable.strings in the Project navigator and open the File inspector
(Option-Command-1).
HIITFit/HIITFit/Localizable.strings
➤ Click Localize…. Something very quick happens! If the file inspector goes blank,
select Localizable.strings in the Project navigator again:
HIITFit/HIITFit/en.lproj/Localizable.strings
This new en.lproj folder doesn’t appear in the Project navigator, but here it is in
Finder:
That’s the project-level setup done. The next two steps will populate this
Localizable.strings file with lines like "Start" = "Start";, one for each string you
want to translate into another language.
raywenderlich.com 143
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
Which strings?
The next step starts with deciding which user-facing strings you want to localize.
➤ Now scan your app to find all the text the user sees:
NSLocalizedString(
"Start/Done",
comment: "begin exercise / mark as finished")
raywenderlich.com 144
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
That takes care of everything except the exercise names. You create these in
Exercise.swift, so that’s where you’ll set up the localized strings.
You need to refactor Exercise to use localized strings instead of enumeration raw
values.
➤ Now, in the exercises array, use the description string for exerciseName
instead of the literal string:
raywenderlich.com 145
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
extension Exercise {
static let exercises = [
Exercise(
exerciseName: String(describing: ExerciseEnum.squat),
videoName: "squat"),
Exercise(
exerciseName: String(describing: ExerciseEnum.stepUp),
videoName: "step-up"),
Exercise(
exerciseName: String(describing: ExerciseEnum.burpee),
videoName: "burpee"),
Exercise(
exerciseName: String(describing: ExerciseEnum.sunSalute),
videoName: "sun-salute")
]
}
Your Localizable.strings file needs to contain lines like "Start" = "Start";, but
it’s currently blank. You could type every line yourself, but fortunately Xcode provides
a tool to generate these from your NSLocalizedString instances.
➤ In Finder, locate the HIITFit folder that contains the Assets.xcassets and
en.lproj subfolders:
raywenderlich.com 146
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
➤ Open Terminal, type cd followed by a space, then drag this HIITFit folder into
Terminal:
➤ Press Return. You changed directory to the folder that contains Assets.xcassets
and en.lproj. Enter this command to check:
ls
You use the Xcode command line tool genstrings to scan files in Views and Model
for NSLocalizedString. It generates the necessary strings for the key values and
stores these in your Localizable.strings file.
That’s your comment in comments and the key string assigned to itself. Aren’t you
glad you didn’t have to type all that out yourself? ;]
Adding a language
And here’s the other time-saving step. You’ll add another language, choosing the
existing English Localizable.strings as the reference language resource file. And
automagic happens!
raywenderlich.com 147
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
➤ In the Project navigator, select the top-level HIITFit folder, then the project in the
projects and target list. In the Localizations section, click the + button and select
another language:
Now you get to choose the file and reference language to create your localization:
raywenderlich.com 148
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
This produces several changes. The Localizations section now has a Spanish item,
which already has 1 File Localized.
Localizable.strings group
And the Spanish file has the same contents as the English file!
Translating
Now for the final step: In the Localizable.strings file for the alternate language, you
need to replace English strings with translated strings.
/* exercise */
"Burpee" = "Burpee";
/* invitation to exercise */
"Get Fit" = "Ponte en forma";
/* invitation */
"Get Started" = "Empieza";
raywenderlich.com 149
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
/* exercise */
"Squat" = "Sentadilla";
/* exercise */
"Step Up" = "Step Up";
/* warm up stretch */
"Sun Salute" = "Saludo al Sol";
/* greeting */
"Welcome" = "Bienvenid@";
Note: Often, Spanish-speakers just use the English exercise names. And using
’@’ to mean ’a or o’ is a convenient way to be gender-inclusive.
Before you export, localize any media resources or assets that provide useful context
information to translators.
Resources like the .mp4 files have a Localize button in their file inspector.
raywenderlich.com 150
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
raywenderlich.com 151
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
To export for localization, select the project in the Project navigator, then select
Editor ▸ Export for Localization…:
Export options
Note: The final-loc project exported to Preview Content to keep it with the
project.
raywenderlich.com 152
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
Exported folder
The exported folder has the same name as your project and contains .xcloc folders
for the languages you checked. For each language, the .xliff file is in Localized
Contents, and localized assets and resources are in Source Contents. You can
supply additional context information in the Notes folder.
Note: I had mixed results exporting videos. If you don’t see these in the
exported folder, just copy resources and assets directly to the exported Source
Contents folder.
raywenderlich.com 153
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
➤ Edit the scheme and select Run ▸ Options ▸ App Language ▸ Spanish
Note: The first three letters of November are the same in Spanish, so I
changed the date format to "MMMM d" to display the full month name.
raywenderlich.com 154
SwiftUI Apprentice Chapter 5: Organizing Your App's Data
Key points
• To use a collection in a ForEach loop, it needs to have a way to uniquely identify
each of its elements. The easiest way is to make it conform to Identifiable and
include id: UUID as a property.
• An enumeration is a named type, useful for grouping related values so the compiler
can help you avoid mistakes like misspelling a string.
• 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.
• Localize your app to create a larger audience for your app. Replace user-facing text
with NSLocalizedString instances, generate the English Localizable.strings file,
then use this as the reference language resource file for adding other languages.
raywenderlich.com 155
6 Chapter 6: Adding
Functionality to Your App
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.
• 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.
raywenderlich.com 156
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
• A @State property is a source of truth. One view owns it and passes its value or
reference, known as a binding, to its subviews.
You’ll learn more about these, and other, property wrappers in Chapter 11,
“Understanding Property Wrappers”.
raywenderlich.com 157
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
Navigating TabView
Skills you’ll learn in this section: using @State and @Binding properties;
pinning a preview; adding @Binding parameters in previews.
Here’s your first feature: Set up TabView to use tag values. When a button changes
the value of selectedTab, TabView displays that tab.
Open the starter project. It’s the same as the final no-localization project from the
previous chapter.
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” 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.
raywenderlich.com 158
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
➤ Now replace the entire body closure of ContentView with the following code:
Xcode complains you’re passing an extra argument because you haven’t yet added a
selectedTab property to WelcomeView or ExerciseView. You’ll do that soon.
➤ Before you head off to edit WelcomeView.swift and ExerciseView.swift, click the
pin button to pin the preview of ContentView:
raywenderlich.com 159
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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.
WelcomeView(selectedTab: .constant(9))
raywenderlich.com 160
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
➤ Now that you’ve fixed the errors, you can Resume the preview in
WelcomeView.swift:
Button(action: { selectedTab = 0 }) {
raywenderlich.com 161
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
➤ Now turn on live preview for the pinned ContentView preview, then tap Get
Started.
Note: You can’t preview this action in the WelcomeView preview because it
doesn’t include ExerciseView. Tapping Get Started doesn’t go anywhere.
You’ve used selectedTab to navigate from the welcome page to the first exercise!
➤ 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.
raywenderlich.com 162
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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 create 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.
➤ Refresh live preview for the pinned ContentView preview, then 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.
raywenderlich.com 163
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
Users expect the page numbers in HeaderView to indicate the current page. A
convenient indicator is the fill version of the symbol. In light mode, it’s a white
number on a black background.
2. The Welcome page doesn’t really need a page “number”, so you delete the
"hand.wave" symbol from the HStack.
raywenderlich.com 164
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
3. To accommodate any number of exercises, you create the HStack by looping over
the exercises array.
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 the Group contents with the
following:
HeaderView(
selectedTab: $selectedTab,
titleText: Exercise.exercises[index].exerciseName)
➤ Refresh live preview for the pinned ContentView preview, then 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.
raywenderlich.com 165
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
.onTapGesture {
selectedTab = index
}
This modifier reacts to the user tapping the Image by setting the value of
selectedTab.
➤ Refresh live preview for the pinned ContentView preview, then tap a page
number to navigate to that exercise page:
raywenderlich.com 166
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
In Chapter 8, “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))
raywenderlich.com 167
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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.
➤ Refresh live preview for the pinned ContentView preview, then tap a page
number to navigate to that exercise page. Tap different symbols to see the colors
change:
Rating view
➤ Navigate to other exercise pages and set their ratings, then navigate through the
pages to see the ratings are still the values you set.
raywenderlich.com 168
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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.
Showing HistoryView
One way to show or hide a modal sheet is with a Boolean flag.
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.
raywenderlich.com 169
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
Hiding HistoryView
There are actually two ways to dismiss a modal sheet. This way is the easiest to
understand. You set a flag to true to show the sheet, so you set the flag to false to
hide it.
HistoryView(showHistory: .constant(true))
Button(action: { showHistory.toggle() }) {
raywenderlich.com 170
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
Showing SuccessView
In ExerciseView.swift, you’ll modify the action of the Done button so when the user
taps it on the last exercise, it displays SuccessView.
➤ Then replace the Done button action with an if-else statement and add the
sheet(isPresented:) modifier:
Button("Done") {
if lastExercise {
showSuccess.toggle()
} else {
selectedTab += 1
}
raywenderlich.com 171
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
}
.sheet(isPresented: $showSuccess) {
SuccessView()
}
Hiding SuccessView
The internal workings of this way are complex, but it simplifies your code because
you don’t need to pass a parameter to the modal sheet. And you can use exactly the
same two lines of code in every modal view.
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
presentationMode is specific to the view. It’s a binding to a structure with an
isPresented property and a dismiss() method.
When you’re viewing SuccessView, its isPresented value is true. You want to
change this value to false when the user taps the Continue button.
But the @Environment property wrapper doesn’t let you set an environment value
directly. You can’t write presentationMode.isPresented = false.
Button("Continue") {
presentationMode.wrappedValue.dismiss()
}
raywenderlich.com 172
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
To test showing and hiding SuccessView, you’ll preview the last exercise page.
➤ Refresh the preview and start live preview. You should see Sun Salute. Tap Done:
SuccessView(selectedTab: .constant(3))
raywenderlich.com 173
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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)
➤ 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 .\presentationMode 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 subscribe to a Timer
publisher and rework HistoryStore as an ObservableObject.
raywenderlich.com 174
SwiftUI Apprentice Chapter 6: Adding Functionality to Your App
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(\.presentationMode) as another way to dismiss a modal sheet.
raywenderlich.com 175
7 Chapter 7: 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 Timer publisher and give
some views access to HistoryStore as an EnvironmentObject.
raywenderlich.com 176
SwiftUI Apprentice Chapter 7: Observing Objects
Swift has a Timer class with a class method that creates a Timer publisher. Publishers
are fundamental to Apple’s new Combine concurrency framework, and a Timer
publisher is much easier to work with than a plain old Timer.
Note: For complete coverage of this framework, check out our book Combine:
Asynchronous Programming with Swift at https://bit.ly/3sW1L3I.
➤ Continue with your project from the previous chapter or open the project in this
chapter’s starter folder.
raywenderlich.com 177
SwiftUI Apprentice Chapter 7: Observing Objects
}
}
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.
Note: Run loops are the underlying mechanism iOS uses for asynchronous
event source processing.
raywenderlich.com 178
SwiftUI Apprentice Chapter 7: 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.
➤ Replace this Text view and font modifier with the following code:
if showTimer {
TimerView(timerDone: $timerDone)
}
You call TimerView when showTimer is true, passing it a binding to the State
variable timerDone.
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.
raywenderlich.com 179
SwiftUI Apprentice Chapter 7: Observing Objects
Also, TimerView is showing. This means showTimer is currently true, so you toggle
it back to false, to hide TimerView.
➤ Next, add this modifier to the Button, above the sheet(isPresented:) modifier:
.disabled(!timerDone)
This exercise page provides visible feedback. It responds to tapping Done by showing
SuccessView.
raywenderlich.com 180
SwiftUI Apprentice Chapter 7: Observing Objects
➤ Tap Start Exercise and wait while the timer counts down from three:
➤ Tap Done.
raywenderlich.com 181
SwiftUI Apprentice Chapter 7: 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) {
Button("Start Exercise") { // Move buttons above TimerView
showTimer.toggle()
}
Button("Done") {
timerDone = false
showTimer.toggle()
raywenderlich.com 182
SwiftUI Apprentice Chapter 7: Observing Objects
if lastExercise {
showSuccess.toggle()
} else {
selectedTab += 1
}
}
.disabled(!timerDone)
.sheet(isPresented: $showSuccess) {
SuccessView(selectedTab: $selectedTab)
}
}
.font(.title3)
.padding()
if showTimer {
TimerView(timerDone: $timerDone)
}
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.
➤ Run 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.
raywenderlich.com 183
SwiftUI Apprentice Chapter 7: Observing Objects
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:
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.
raywenderlich.com 184
SwiftUI Apprentice Chapter 7: Observing Objects
Creating an ObservableObject
To dismiss SuccessView, you used its presentationMode 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.
You make HistoryStore a class instead of a structure, then make it conform to the
ObservableObject protocol.
You mark the exerciseDays array of ExerciseDay objects with the @Published
property wrapper. Whenever exerciseDays changes, it publishes itself to any
subscribers, and the system redraws any affected views.
raywenderlich.com 185
SwiftUI Apprentice Chapter 7: Observing Objects
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() {
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.
raywenderlich.com 186
SwiftUI Apprentice Chapter 7: Observing Objects
Using an EnvironmentObject
Now, you need to set up HistoryStore as an EnvironmentObject in the parent view
of ExerciseView. ContentView contains TabView, which calls ExerciseView, so
you’ll create the EnvironmentObject “on” TabView.
.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 no
useful information on what went wrong.
raywenderlich.com 187
SwiftUI Apprentice Chapter 7: Observing Objects
➤ Now add this line at the top of the Done button’s action closure:
history.addDoneExercise(Exercise.exercises[index].exerciseName)
➤ Run live preview, then tap History to see what’s already there:
History: before
➤ 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.
raywenderlich.com 188
SwiftUI Apprentice Chapter 7: Observing Objects
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.
raywenderlich.com 189
SwiftUI Apprentice Chapter 7: Observing Objects
Challenge
To appreciate how well @EnvironmentObject works for this feature, implement it
using State and Binding.
raywenderlich.com 190
SwiftUI Apprentice Chapter 7: Observing Objects
Key points
• Create a timer by subscribing to the Timer publisher created by
Timer.publish(every:tolerance:on:in:options:).
raywenderlich.com 191
8 Chapter 8: 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.
raywenderlich.com 192
SwiftUI Apprentice Chapter 8: 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 at https://bit.ly/
39lo2k3.
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.
➤ Continue with the final project from the previous chapter or open the project in
this chapter’s starter folder.
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.
raywenderlich.com 193
SwiftUI Apprentice Chapter 8: 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.
raywenderlich.com 194
SwiftUI Apprentice Chapter 8: 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;
FileManager; 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 that
means no other app will be able to access your app’s files.
raywenderlich.com 195
SwiftUI Apprentice Chapter 8: 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.
➤ In Xcode, open the Products group at the bottom of the groups list. When you
build your app, Xcode creates a product with the same name as the project.
➤ If your app’s name is in red, build the project to ensure that the compiled project
exists on disk.
App in Finder
You see here Simulator’s debug directory for the app.
raywenderlich.com 196
SwiftUI Apprentice Chapter 8: Saving Settings
• 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 been run. You can then check this flag or version number to see whether
your app needs to do any internal updates.
raywenderlich.com 197
SwiftUI Apprentice Chapter 8: 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(FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask))
}
Your app will run onAppear(perform:) every time the view initially appears.
Here you use the shared file manager object to list the URLs for the specified
directory. There are many significant directories, which you can find in the
documentation at https://apple.co/3pTE3U5. Remember that your app is sandboxed,
and each app will have its own app directories.
➤ Build and run in Simulator. The debug console will print out an array of URLs for
your Documents directory path in the specified domain. The domain here,
userDomainMask, is the user’s home directory.
raywenderlich.com 198
SwiftUI Apprentice Chapter 8: Saving Settings
Swift Tip: The result is an array of URLs rather than a single URL, because you
could ask for an array of domain masks. These domains could include
localDomainMask for items available to everyone on the machine,
networkDomainMask for items available on the network and
systemDomainMask for Apple system files.
Show in Finder
This will open a new Finder window showing Simulator’s user directories:
Simulator directories
➤ The parent directory — in this example, 47887…524CF — 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.
• 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.
raywenderlich.com 199
SwiftUI Apprentice Chapter 8: 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.
raywenderlich.com 200
SwiftUI Apprentice Chapter 8: 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”.
raywenderlich.com 201
SwiftUI Apprentice Chapter 8: 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.
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.
raywenderlich.com 202
SwiftUI Apprentice Chapter 8: Saving Settings
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.
Just as with an array, you can iterate through a string using a for loop:
raywenderlich.com 203
SwiftUI Apprentice Chapter 8: Saving Settings
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 at 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.
Here you hold rating locally and set up ratings to be a string of four zeros.
raywenderlich.com 204
SwiftUI Apprentice Chapter 8: Saving Settings
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 following chapter.
You pass in the exercise index from Preview, so your app should now compile.
// 1
.onAppear {
// 2
let index = ratings.index(
ratings.startIndex,
offsetBy: exerciseIndex)
// 3
let character = ratings[index]
// 4
rating = character.wholeNumberValue ?? 0
}
Swift can be a remarkably succinct language, and there’s a lot to unpack in this short
piece of code:
1. Your app runs onAppear(perform:) every time the view initially appears.
raywenderlich.com 205
SwiftUI Apprentice Chapter 8: Saving Settings
3. Here 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
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.
raywenderlich.com 206
SwiftUI Apprentice Chapter 8: Saving Settings
Note: You can find more information about RangeExpressions in the official
documentation at 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.
AppStorage
raywenderlich.com 207
SwiftUI Apprentice Chapter 8: Saving Settings
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
// 2
let desiredLength = Exercise.exercises.count
if ratings.count < desiredLength {
// 3
ratings = ratings.padding(
toLength: desiredLength,
withPad: "0",
startingAt: 0)
}
}
raywenderlich.com 208
SwiftUI Apprentice Chapter 8: Saving Settings
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 must
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 out, in Simulator, choose Device ▸ Erase All Contents and Settings…
to completely delete the app and clear caches.
When AppStorage creates UserDefaults, it will create a string with fewer characters
than your exercise count.
➤ 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
raywenderlich.com 209
SwiftUI Apprentice Chapter 8: 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 HIITFit target, the General tab and, under Deployment Info, locate
Supports multiple windows.
➤ Ensure this is checked. When unchecked, you won’t be able to have two windows
of either your own app, or yours plus another app, side-by-side.
raywenderlich.com 210
SwiftUI Apprentice Chapter 8: Saving Settings
Non-reactive rating
raywenderlich.com 211
SwiftUI Apprentice Chapter 8: 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.
➤ In onAppear(perform:), highlight:
raywenderlich.com 212
SwiftUI Apprentice Chapter 8: 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
should immediately redraw 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()
...
}
}
}
raywenderlich.com 213
SwiftUI Apprentice Chapter 8: 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.
raywenderlich.com 214
SwiftUI Apprentice Chapter 8: 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.
➤ 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.
raywenderlich.com 215
SwiftUI Apprentice Chapter 8: Saving Settings
➤ 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.
raywenderlich.com 216
SwiftUI Apprentice Chapter 8: 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. You can access your app’s
Documents and Library directories using FileManager.
• 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.
raywenderlich.com 217
9 Chapter 9: 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, a database such as
SQLite or Realm, or Core Data. Since you’ve learned so much about property list files
already, in this chapter, you’ll save the history data to one.
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 for
completion handlers.
• Serialization: Last but not least, you’ll translate your history data into a format
that can be stored.
raywenderlich.com 218
SwiftUI Apprentice Chapter 9: 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.
raywenderlich.com 219
SwiftUI Apprentice Chapter 9: 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:
raywenderlich.com 220
SwiftUI Apprentice Chapter 9: Saving History Data
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.
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.
raywenderlich.com 221
SwiftUI Apprentice Chapter 9: Saving History Data
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.
➤ Enter part of the print output that you expect to see, in this case History, into
Filter at the bottom right of the console:
If you have a number of print statements that you wish to see, you can prefix them
with particular characters, such as >>>.
print(">>>", today)
print(">>> Inserting \(exerciseName)")
You can then enter >>> into Filter and your logs will show up on their own.
Remember to clear your filter when you’re through. It can be frustrating when you
forget to add >>>, and you filter out your own debugging logs.
raywenderlich.com 222
SwiftUI Apprentice Chapter 9: Saving History Data
Debugging HistoryStore
Skills you’ll learn in this section: breakpoints.
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
raywenderlich.com 223
SwiftUI Apprentice Chapter 9: Saving History Data
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 tap in the gutter to the
left of let today = Date() in addDoneExercise(_:) and click. This adds a
breakpoint at that line.
Breakpoint
➤ Without stopping your app, complete a second exercise and tap Done.
Execution paused
raywenderlich.com 224
SwiftUI Apprentice Chapter 9: Saving History Data
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.
➤ 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:
raywenderlich.com 225
SwiftUI Apprentice Chapter 9: Saving History Data
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")
➤ 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. Later in the chapter, you’ll set up HistoryStore so that you’re
sure that the store will initialize only once.
raywenderlich.com 226
SwiftUI Apprentice Chapter 9: Saving History Data
WindowGroup {
ContentView()
.environmentObject(HistoryStore())
...
}
➤ 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.
raywenderlich.com 227
SwiftUI Apprentice Chapter 9: 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 will
always raise 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.
raywenderlich.com 228
SwiftUI Apprentice Chapter 9: Saving History Data
try…catch
When calling a method that throws, you use try. If you don’t need to handle any
errors specifically, you can call the method with try? as, for example, try? load().
This will convert an error result to nil and execution continues. To handle an error
from a throwing method, you use the expression do { try ... } catch { }.
do {
try load()
} catch {
print("Error:", error)
}
You call the throwing method and, if there’s an error, the catch block executes.
➤ Build and run and, in the debug console, you’ll see your printed error: Error:
loadFailure. (Remember to clear your debug console filter if you have one.)
Throwing initializers
You can also throw errors when initializing an object. If your loading of the history
data fails, you could either report a catastrophic error and crash the app or,
preferably, you could report an error but continue with no history and an empty
exerciseDays.
You’ll try to create a HistoryStore using this initializer, but fall back to the default
initializer if necessary.
init() {}
This is your fall-back initializer, which won’t call any loading code.
throw error
This will pass back the error to the object that initializes HistoryStore.
raywenderlich.com 229
SwiftUI Apprentice Chapter 9: Saving History Data
So far you’ve used @State for mutable values. You should only use @State properties
for temporary items, as they will disappear when the view is deleted. @StateObject
will create an observable object which won’t disappear when the view does.
Note: In case you’re confused about all the property wrappers you’ve used so
far, you will review them in Chapter 11, “Understanding Property Wrappers”.
init() {
let historyStore: HistoryStore
do {
historyStore = try HistoryStore(withChecking: true)
} catch {
print("Could not load history data")
historyStore = HistoryStore()
}
}
When ContentView first initializes, you try loading the history. If there is no error,
then historyStore will contain the loaded history data. If the try fails, then you
print out an error message and use HistoryStore’s default initializer.
HistoryStore.init() can’t possibly fail, but will load with empty history data.
You still have to assign the local historyStore to the state object.
raywenderlich.com 230
SwiftUI Apprentice Chapter 9: Saving History Data
As the name suggests, a property wrapper wraps an underlying value or object. You
use the StateObject(wrappedValue:) initializer to set the wrapped value of the
state object and use an underscore prefix to assign the initialized state object to
historyStore.
.environmentObject(historyStore)
Here you use the state object instead of creating HistoryStore, when setting up the
environment object.
➤ Build and run and, because load() still throws an error, you’ll see your error in
the debug console: Could not load history data.
Alerts
Skills you’ll learn in this section: Alert view.
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 HIITFitApp.swift and add a new property to HIITFitApp:
raywenderlich.com 231
SwiftUI Apprentice Chapter 9: Saving History Data
showAlert = true
.alert(isPresented: $showAlert) {
Alert(
title: Text("History"),
message: Text(
"""
Unfortunately we can’t load your past history.
Email support:
support@xyz.com
"""))
}
When showAlert 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.
➤ Build and run. Because HistoryStore’s initializer fails, you set showAlert to true,
which causes your Alert to show.
History Alert
➤ Tap OK. Alert resets showAlert and your app continues with empty history data.
raywenderlich.com 232
SwiftUI Apprentice Chapter 9: Saving History Data
Now that your testing of error checking is complete, open HistoryStore.swift and
remove throw FileError.loadFailure from load().
Note: You can find out more about error handling in our Swift Apprentice book,
which has an entire chapter on the subject. You can find Swift Apprentice at:
https://bit.ly/2MuhHu0.
Saving history
Skills you’ll learn in this section: FileManager.
You’ll first save your history data to disk and then, come back to filling out load()
using the saved data.
➤ Add a new method to HistoryStore to create the URL where you will save the
data:
This method returns an optional URL. The calling method can then decide what to
do if the result of this method is nil.
raywenderlich.com 233
SwiftUI Apprentice Chapter 9: Saving History Data
1. 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. Here you check that
FileManager.default.urls(for:in:).first is not nil and, if it isn’t nil,
assign it to documentsURL.
2. You always provide an else branch with guard where you specify how to leave
the method when the guard conditional test fails. Generally you return from the
method, but you could also use fatalError(_:file:line:) to crash the app.
3. 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 set up your URL. If getURL() returns nil, you throw an error and save() stops
execution.
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.
raywenderlich.com 234
SwiftUI Apprentice Chapter 9: 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:
Closures
Skills you’ll learn in this section: closures; map(_:); transforming arrays.
raywenderlich.com 235
SwiftUI Apprentice Chapter 9: Saving History Data
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.
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:
raywenderlich.com 236
SwiftUI Apprentice Chapter 9: Saving History Data
This is the closure that would perform this conversion for a single ExerciseDay
element:
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.
➤ Replace the previous code from var plistData: [[Any]] = [] to the end of
save() with:
func map<T>(
_ transform: (Self.Element) throws -> T) rethrows -> [T]
raywenderlich.com 237
SwiftUI Apprentice Chapter 9: Saving History Data
• T is a generic type. You’ll discover more about generics in Section 2, but here T is
equivalent to [Any].
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.
raywenderlich.com 238
SwiftUI Apprentice Chapter 9: 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
With filter(_:) you can filter one array to another array, as for example:
The closure takes each element of the array and returns a value of true if the integer
is between one and three. When the closure result is true, the element is added to
the new array. After completing this code, oneToThree contains [2, 3, 1].
reduce(_:) combines all the elements in an array into one value. For example:
raywenderlich.com 239
SwiftUI Apprentice Chapter 9: Saving History Data
runningTotal + value
}
You call reduce(_:_:) with a starting value. Although you can substitute $0 and $1
for the parameters here, the code reads better with explicitly named parameters. The
first parameter is the running total, and you add the second parameter to the first,
resulting in a single value. After this code result will contain 6.
do {
// 1
let data = try PropertyListSerialization.data(
fromPropertyList: plistData,
format: .binary,
options: .zero)
// 2
try data.write(to: dataURL, options: .atomic)
} catch {
// 3
throw FileError.saveFailure
}
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 an error, which you catch by throwing an
error.
raywenderlich.com 240
SwiftUI Apprentice Chapter 9: Saving History Data
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.
• 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.
raywenderlich.com 241
SwiftUI Apprentice Chapter 9: Saving History Data
// 1
guard let dataURL = getURL() else {
throw FileError.urlFailure
}
do {
// 2
let data = try Data(contentsOf: dataURL)
// 3
let plistData = try PropertyListSerialization.propertyList(
from: data,
options: [],
format: nil)
// 4
let convertedPlistData = plistData as? [[Any]] ?? []
// 5
exerciseDays = convertedPlistData.map {
ExerciseDay(
date: $0[1] as? Date ?? Date(),
exercises: $0[2] as? [String] ?? [])
}
} catch {
throw FileError.loadFailure
}
Loading is very similar to saving, but with some type checking to ensure that your
data conforms to the types you are expecting. Going through the code:
1. First set up the URL just as you did with saving the file.
2. 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 as your challenge for this
chapter.
3. Convert the property list format into a format that your app can read.
raywenderlich.com 242
SwiftUI Apprentice Chapter 9: Saving History Data
4. 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
raywenderlich.com 243
SwiftUI Apprentice Chapter 9: Saving History Data
Challenge
➤ Delete history.plist in Finder, and build and run your app. Your loading error
appears because load() fails.
Load error
You’re not first checking to see whether history.plist exists. If it doesn’t,
Data(contentsOf:) throws an error.
Your challenge is to ignore the error, as it’s most likely that in this case the error is
that the file doesn’t exist. Remember that you can use try? to discard an error. When
you’ve completed your mission, your app should load data from history.plist if it
exists and take no action if it doesn’t.
You can find the answer to this challenge in load() in the challenges directory for
this chapter.
raywenderlich.com 244
SwiftUI Apprentice Chapter 9: 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. A common paradigm is to pass a closure as a completion handler to be
executed when an operation completes. Array has a number of methods requiring
closures to transform its elements into a new array.
raywenderlich.com 245
10 Chapter 10: Refining Your
App
By Caroline Begbie
While you’ve been toiling on 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. The designer has moved your elements around and created a
new modal screen for the timer. Essentially, the app works in the same way, though.
iPhone design
raywenderlich.com 246
SwiftUI Apprentice Chapter 10: 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.
This style of design, where the background and controls are one single color, is called
neumorphism. It’s a recent trend in design, and 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
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.
raywenderlich.com 247
SwiftUI Apprentice Chapter 10: 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
The text style on all three raised buttons is the same.
raywenderlich.com 248
SwiftUI Apprentice Chapter 10: Refining Your App
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. You’ll find the list at https://apple.co/3kzvD2e.
Styling text is not on that list, which is why you created your own view modifier.
raywenderlich.com 249
SwiftUI Apprentice Chapter 10: Refining Your App
You’ve already used one of these styles, the built-in PageTabViewStyle, on your
TabView. In the documentation, it appears that there are a number of button styles
available, however most of these apply to specific operating systems. For example
you can only use BorderedButtonStyle on macOS, tvOS and watchOS.
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.
You can use this button style to change all your buttons in a view hierarchy.
.buttonStyle(RaisedButtonStyle())
You tell ContentView that whenever there’s a button in the hierarchy, it should use
your custom style.
raywenderlich.com 250
SwiftUI Apprentice Chapter 10: Refining Your App
➤ The buttons in your app won’t all use the same style so remove
buttonStyle(RaisedButtonStyle()) from HIITFitApp.
.buttonStyle(RaisedButtonStyle())
You can now preview your button style as you change it.
raywenderlich.com 251
SwiftUI Apprentice Chapter 10: Refining Your App
When you set frame(maxWidth:) to .infinity, you ask the view to take up as much
of the 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
raywenderlich.com 252
SwiftUI Apprentice Chapter 10: 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(RaisedButtonStyle())
.padding()
raywenderlich.com 253
SwiftUI Apprentice Chapter 10: Refining Your App
Here you use your new text and button styles to create your new button.
➤ Preview the button and, 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: {
raywenderlich.com 254
SwiftUI Apprentice Chapter 10: Refining Your App
print("Hello World")
})
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 brackets.
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.
➤ Open ExerciseView.swift and create a new property for the Start Exercise
button:
startExerciseButton
raywenderlich.com 255
SwiftUI Apprentice Chapter 10: Refining Your App
Exercise Button
The History button will have an embossed border in the shape of a capsule. This will
be very similar to RaisedButton, except that your embossed button will be able to
contain any content, not just text. For this reason, you’ll only create a new button
style, and not a new button structure.
raywenderlich.com 256
SwiftUI Apprentice Chapter 10: 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.
raywenderlich.com 257
SwiftUI Apprentice Chapter 10: Refining Your App
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
}
raywenderlich.com 258
SwiftUI Apprentice Chapter 10: 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.
raywenderlich.com 259
SwiftUI Apprentice Chapter 10: 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.
.buttonStyle(EmbossedButtonStyle(buttonShape: .round))
raywenderlich.com 260
SwiftUI Apprentice Chapter 10: Refining Your App
➤ Preview the button, and this is the result for Light Mode:
➤ To visualize this, in shape(), click Circle() to view the circle outline in the
preview:
.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)
})
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.
raywenderlich.com 261
SwiftUI Apprentice Chapter 10: Refining Your App
.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.
➤ Preview the button, and 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.
raywenderlich.com 262
SwiftUI Apprentice Chapter 10: Refining Your App
Here you use the other form of Button so that you can format the button contents.
You 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
raywenderlich.com 263
SwiftUI Apprentice Chapter 10: 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
raywenderlich.com 264
SwiftUI Apprentice Chapter 10: 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.
raywenderlich.com 265
SwiftUI Apprentice Chapter 10: Refining Your App
The view here is the result of the content closure that the initializer performed.
Now you can test out your container view in the preview.
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
Obviously, this container view only returns the VStack, so it’s not a lot of use at the
moment. You can make view builders quite complex though. In supporting code in
Chapter 21, “Delightful UX — Final Touches”, you can find a RenderableView view
builder that observes the contained views and takes a screenshot of the views when
triggered.
raywenderlich.com 266
SwiftUI Apprentice Chapter 10: Refining Your App
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.
➤ Preview the view, and your container view is finished. It’s a good idea not to add
unnecessary padding to the actual container view, as that reduces the flexibility. Here
you have padding in the preview. When you use the container view shortly, you’ll
make it go right to the edges.
Finished ContainerView
raywenderlich.com 267
SwiftUI Apprentice Chapter 10: Refining Your App
Designing WelcomeView
Skills you’ll learn in this section: refactoring with view properties; the safe
area.
raywenderlich.com 268
SwiftUI Apprentice Chapter 10: 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”.
➤ Open ContentView.swift and preview your app on both iPhone 12 Pro Max and
iPod Touch. These are the biggest and smallest screens (not taking into account
iPad), and you want to make sure that your app looks great on all iPhones.
raywenderlich.com 269
SwiftUI Apprentice Chapter 10: Refining Your App
When you’re layering background colors, it’s often safe to use the modifier
edgesIgnoringSafeArea(_:). But if you use this modifier on your TabView here,
this will be the result:
As you’re going to be adding a gradient background behind TabView, you can include
the gray background color in the gradient and then ignore safe edges on the layered
background color.
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. You’re going to use a lovely purple to blue gradient, using the predefined
colors in the asset catalog.
raywenderlich.com 270
SwiftUI Apprentice Chapter 10: Refining Your App
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. Because this gradient will only be used as a
background color, you can ignore the safe area, and the gradient will stretch to all
screen edges.
Initial Gradient
raywenderlich.com 271
SwiftUI Apprentice Chapter 10: Refining Your App
➤ You’re also going to cover up the safe area with gray, so include that in the list of
colors:
Gradient(colors: [
Color("gradient-top"),
Color("gradient-bottom"),
Color("background")
])
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. If you want a striped
background, you can achieve this using color stops in this way.
raywenderlich.com 272
SwiftUI Apprentice Chapter 10: Refining Your App
ZStack {
GradientBackground()
TabView(selection: $selectedTab) {
...
}
...
}
➤ Preview your result on several iPhone sizes. Also make sure that you check that
your layout works as far as possible with accessibility dynamic type. With this layout,
iPod Touch is still usable even set to Accessibility Large.
raywenderlich.com 273
SwiftUI Apprentice Chapter 10: Refining Your App
Challenge
Your challenge is to continue styling. First 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.
You may need to build and run to see your changes in Simulator if they don’t show
up in preview.
ExerciseView doesn’t look so hot with the gradient background, so embed this in
ContainerView just as you did in WelcomeView.
raywenderlich.com 274
SwiftUI Apprentice Chapter 10: Refining Your App
The project supplied in the challenge directory contains a fully designed app which
makes full use of the buttons you styled in this chapter. Check out:
• The Get Started button which appears to indent when you tap it.
• The History views. There are two of them, one a list and one a bar chart. To make
the history bar chart, there are some extension methods on Date() to work out an
array for the last seven days, as well as formatting day and month.
• The Timer view, now a modal with a circular cut-out for the title.
raywenderlich.com 275
SwiftUI Apprentice Chapter 10: 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.
raywenderlich.com 276
11 Chapter 11: Understanding
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 the @ObservedObject property wrapper.
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:
Model-View-ViewModel (MVVM).
raywenderlich.com 277
SwiftUI Apprentice Chapter 11: Understanding 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 NavigationView. This gives you the navigation bar
where you display the title and the + button. You’ll learn more about
NavigationView in Section 3.
This project has a ThingStore, like HistoryStore in HIITFit. This app is much
simpler than HIITFit, so you can focus on how you manage the data.
raywenderlich.com 278
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
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:
raywenderlich.com 279
SwiftUI Apprentice Chapter 11: Understanding 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 wrap 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.
raywenderlich.com 280
SwiftUI Apprentice Chapter 11: Understanding 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.
This is what you’ll do in this chapter to consolidate your understanding of how to use
these property wrappers.
In the same chapter, you used @SceneStorage to save and restore the state of scenes
— windows in the iPad simulator, each showing a different exercise.
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.
raywenderlich.com 281
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
You already got lots of practice with @State and @Binding in Chapter 6, “Adding
Functionality to Your App”:
In the challenge for that chapter, 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 + button, and ContentView
owns it.
raywenderlich.com 282
SwiftUI Apprentice Chapter 11: Understanding 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)
}
raywenderlich.com 283
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
➤ You’ll also add a text field, 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)
raywenderlich.com 284
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
Now to get input from your user, you’ll add a TextField to AddThingView.
➤ 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:
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.
raywenderlich.com 285
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
➤ Refresh live-preview in the ContentView preview and 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.
Sometimes the app auto-corrects your acronym: FTW to GET or FOMO to DINO.
.disableAutocorrection(true)
raywenderlich.com 286
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
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)
raywenderlich.com 287
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
Acronyms should appear as all caps, but it’s easy to forget to hold down the Shift key.
You can actually set an environment value to automatically convert text to upper
case.
.environment(\.textCase, .uppercase)
You set uppercase as the default value of textCase for ContentView and all its
subviews.
➤ Refresh live-preview, add acronyms without bothering to keep all the letters upper
case. Just type yolo or fomo. Tap DONE. Notice this label and the placeholder text
are now all uppercase:
Automagic uppercase
Note: If the placeholder text isn’t all upper case, press Shift-Command-K to
clean the build folder.
The environment value applies to all text in your app, which looks a little strange. No
problem — you can override it.
raywenderlich.com 288
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
.environment(\.textCase, nil)
You set the value to nil, so none of the text displayed by this VStack is converted to
uppercase.
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.
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.
raywenderlich.com 289
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
@State and @Binding work well enough to update the ThingStore source of truth
value in ContentView from AddThingView. But ThingStore isn’t the most natural
use of a structure. For the way your app uses ThingStore, a class is a better fit.
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”.
raywenderlich.com 290
SwiftUI Apprentice Chapter 11: Understanding 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. You first created the
HistoryStore object in ContentView, applying the modifier to the TabView:
TabView(selection: $selectedTab) {
...
}
.environmentObject(HistoryStore())
Then, in Chapter 9, “Saving History Data”, you elevated its initialization up one level
to HIITFitApp and declared it as a @StateObject.
raywenderlich.com 291
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
ThingStore is now a class, not a structure, so you can’t use the @State property
wrapper. Instead, you use @StateObject.
AddThingView(someThings: myThings)
You don’t need to create a reference to myThings. As a class object, it’s already a
reference.
Note: If ThingStore had more properties and you wanted to restrict write
access to its things array, you could pass $myThings.things to
AddThingView, which would have a @Binding someThings: [String]
property.
AddThingView(someThings: ThingStore())
raywenderlich.com 292
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
TIL in action
No surprise: The app still works the same as before.
MVVM
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.
Model-View-ViewModel
A view model’s properties can include the current text for a text field or whether a
specific button is enabled. In a view model, you can also specify actions the view can
perform, like button taps or gestures.
raywenderlich.com 293
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
A user action is an event that triggers the view model to update the model. If the
model connects to a back-end database, data can change independently of user
actions. The view model uses these to update the view’s state.
When a view displays a collection of objects or values, its view model manages the
data collection. In simple apps like HIITFit and TIL, this is the view model’s only job.
So the view model’s name often includes the word “Store”.
MVVM in HIITFit
HIITFit’s view 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. The ExerciseView’s tap-Done event updates the exerciseDays
array, which changes the state of HistoryView.
MVVM in TIL
TIL’s view model, ThingStore, saves the user’s array of acronyms. The model is
simply a String and the view model publishes the things array. ContentView and
AddThingView subscribe to ThingStore. The AddThingView tap-Done event updates
the things array, which changes the state of ContentView.
In the Section 3 app, RWFreeView, the view model stores a collection of Episode
instances. It’s responsible for downloading data from raywenderlich.com and
decoding the data into Episodes.
raywenderlich.com 294
SwiftUI Apprentice Chapter 11: Understanding Property Wrappers
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. 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.
raywenderlich.com 295
SwiftUI Apprentice Chapter 11: Understanding 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 Chapter 9, “State & Data Flow – Part II” of SwiftUI by
Tutorials (bit.ly/39bz5vv).
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.
raywenderlich.com 296
SwiftUI Apprentice Chapter 11: Understanding 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, you can refactor your app to accommodate data types that need
to conform to ObservableObject.
raywenderlich.com 297
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.
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.
raywenderlich.com 298
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
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.
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.
raywenderlich.com 299
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
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.
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. Also, this is the 2019 episode. There’s a newer one, but it hasn’t
had time to accumulate the number of views to overtake the original.
raywenderlich.com 300
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
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?
Q: Which should you learn: SwiftUI or UIKit?
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…
raywenderlich.com 301
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
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:
raywenderlich.com 302
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
And in Chapter 22, “Lists & Navigation”, you’ll control the appearance of the
navigation bar and segmented control with UINavigationBarAppearance and
UISegmentedControl.appearance().
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. ;]
raywenderlich.com 303
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
WWDC
Every June, Apple holds the 5-day World Wide Developers Conference. 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 on day 2, 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.
raywenderlich.com 304
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
If you’re a paid-up member of the Apple Developer Program, you can download the
beta versions of Xcode and the operating systems and immediately start exploring all
the new things. Your goal is to include the new features in new or existing apps, all
set to go into the App Store when the new iOS launches.
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
quietly disappear.
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.
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 (apple.co/3qPaen7). For an even more in-
depth look, browse our book Catalyst by Tutorials (bit.ly/32ppGwM).
raywenderlich.com 305
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
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.
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 (bit.ly/39lo2k3) and video courses Beginning Core
Data (bit.ly/2OGjuwG) and Intermediate Core Data (bit.ly/3bE2H6z) to help you
on your way.
raywenderlich.com 306
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
raywenderlich.com 307
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
raywenderlich.com 308
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 (bit.ly/
3tabJxZ) and our video course “Beginning ARKit” (bit.ly/3qJ2a7n). Be prepared for
Apple’s mixed reality headset, predicted for mid-2022 (bit.ly/38rEE8K).
raywenderlich.com 309
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
raywenderlich.com 310
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 the Signing & Capabilities tab, then click
+ Capability:
Capabilities
You’ll use an App Group to share data between your app and its widget in Chapter
26, “Widgets”.
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 on this page: apple.co/
3rOhlNW.
raywenderlich.com 311
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.
• 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 (apple.co/3coUHVU), and it’s probably easier to
just use TestFlight.
raywenderlich.com 312
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 (bit.ly/3al3Hez) or video
course Publishing to the App Store (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 new Xcode
menu item Reveal Build Products Folder.
raywenderlich.com 313
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
➤ 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 ▸ Reveal Build Products
Folder:
➤ Select this folder, then view it as a list and open the Build folder:
raywenderlich.com 314
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
➤ Open Intermediates.noindex and drill down through its .build, Debug and
.build folders to find Objects-normal/x86_64 or, for you lucky M1 owners,
arm64:
raywenderlich.com 315
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
raywenderlich.com 316
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
DerivedData/Index
The Index folder stores data Xcode uses for search, Open Quickly and refactoring.
Again, sometimes the indexes get mixed up, causing strange Xcode behavior. There’s
no menu command to delete the indexes. You just have to delete the whole derived
data folder and let Xcode re-create it.
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.
raywenderlich.com 317
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
rm -rf ~/Library/Developer/Xcode/DerivedData/*
Or, if you’re running Big Sur, you can open Trash and selectively erase the folder.
But Big Sur’s storage management provides an easier way to clear even more space.
➤ In the Apple menu, select About this Mac and click Storage. Then click Manage
and select Developer. Select Xcode Caches and Project Build Data and Indexes
then click Delete…
raywenderlich.com 318
SwiftUI Apprentice Chapter 12: Apple App Development Ecosystem
Many search results will be Stack Overflow or Apple developer forum questions,
hopefully with answers.
The raywenderlich.com 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.
raywenderlich.com 319
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 raywenderlich.com 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 SDK 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.
raywenderlich.com 320
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.
raywenderlich.com 321
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 you’re going build your own collaging app
to 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
raywenderlich.com 322
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.
raywenderlich.com 323
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 12 Pro. Build and run your
app using Command-R to make sure that everything works OK. Your iPhone 12 Pro
simulator should start and show ContentView’s “Hello, world!” text.
Initial screen
You should take these steps every time you create a new app just in case something
in your environment has changed.
raywenderlich.com 324
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.
raywenderlich.com 325
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Placeholder thumbnails
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).
➤ Live Preview the view, and you’ll be able to scroll the list. When you scroll, you can
see an ugly scroll bar by the side of the cards.
raywenderlich.com 326
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ 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) {
raywenderlich.com 327
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
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. 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.
The extracted view is now at the end of the current file and looks like this:
raywenderlich.com 328
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Select the entire CardThumbnailView 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.
➤ A card will have a colored background to which you’ll add photos, stickers and
text.
This color will eventually come from the card data, but for the moment you’ll just
make the card yellow.
A yellow card
raywenderlich.com 329
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
• A modal view. SingleCardView will have a row of buttons that link to modal
views, and it’s not good practice to have modal views inside modal views.
• 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.
In HIITFit, you used a state property toggle to show the History view and passed a
binding when you showed the modal sheet. Navigation in Cards will be spread over
multiple files, so it’s easier to centralize the property with a view state environment
object that will be shared throughout the app.
import SwiftUI
raywenderlich.com 330
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
You’ll add the environment object wherever you need to check the view state.
CardsListView()
.environmentObject(ViewState())
This instantiates the environment object for the preview. If you don’t do this, the
canvas will crash, without a specific error, when your code tries to access viewState.
.onTapGesture {
viewState.showAllCards.toggle()
}
This will toggle the Boolean when you tap a card thumbnail. When showAllCards is
false, you’ll show the selected single card in front of the list of cards.
This view will be the initial view that controls which full screen views currently
show.
import SwiftUI
Here you show CardsListView and set up the environment object viewState.
raywenderlich.com 331
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
if !viewState.showAllCards {
SingleCardView()
}
➤ Live Preview and tap a card. The yellow SingleCardView shows on top of the list
of cards.
Before tackling the button, set up your app to run in Simulator, so that if Live
Preview fails, you can still see your app.
You do this so that viewState persists as long as your app does. If you simply
initialize it as an environment object in CardsApp, occasionally the app will
reinitialize it, and, if you’re editing a card, you’ll mysteriously land back at the first
screen.
raywenderlich.com 332
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
CardsView()
.environmentObject(viewState)
You call the view that will show the list of cards instead of ContentView, making
sure that you put viewState into the app environment.
➤ 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.
Navigation toolbar
Skills you’ll learn in this section: toolbars; NavigationView; navigation
bar; tuples
SingleCardView()
.environmentObject(ViewState())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { viewState.showAllCards.toggle() }) {
Text("Done")
}
}
}
raywenderlich.com 333
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
You place a Done button at the top right of the screen. When the user taps this
button, SingleCardView toggles showAllCards. Because this is a published
property, any view that needs to react to showAllCards will, and CardsView won’t
show SingleCardView any more.
• principal: On iOS, the principal placement is in the center of the navigation bar.
➤ Preview SingleCardView.
No Done button
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 NavigationView.
NavigationView
➤ In SingleCardView, Command-click Color and choose Embed….
raywenderlich.com 334
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
You’re going to add another modifier to Color.yellow, so now’s the time to take the
opportunity to refactor it into a separate view.
➤ Create a new SwiftUI View file called CardDetailView.swift and cut and paste the
extracted CardDetailView structure into CardDetailView.swift, replacing the
boilerplate CardDetailView.
raywenderlich.com 335
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
CardDetailView()
.environmentObject(ViewState())
.navigationBarTitleDisplayMode(.inline)
Other styles include automatic and large. If you want to give the view a title as
well, you can use .navigationTitle("Title goes here").
➤ Tap a card.
raywenderlich.com 336
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
You’ll see that you get a white screen with a Back button. When you tap Back, you
see a portion of SingleCardView with the Done button. Tapping Done takes you
back to the first screen. This is due to the NavigationView which behaves differently
with different size configurations.
.navigationViewStyle(StackNavigationViewStyle())
This navigation view style ensures that you only see a single top view at a time.
raywenderlich.com 337
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ Build and run, and the iPad version of your app will now behave the same way as
the iPhone 12 Pro version.
Note: NavigationView can cause issues when you’re doing your own custom
transitions and animations, so you have to decide whether using
NavigationView is worth it. You could lay out the Done button in ZStack
layers without using a NavigationView as you did in HIITFit.
➤ Set your run destination back to iPhone 12 Pro, as it’s easier to preview in the
canvas.
raywenderlich.com 338
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
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 easy to read and ensure that values are
restricted to those defined in the enumeration.
enum CardModal {
case photoPicker, framePicker, stickerPicker, textPicker
}
Each modal button will use this view, and you’ll style this to be more generic shortly.
raywenderlich.com 339
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Here you create an HStack containing a button that will change the modal state.
You’ll add more toolbar items in a moment to this HStack.
➤ Resume the preview, and you’ll see your Stickers button and icon:
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.
raywenderlich.com 340
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
➤ 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) {
CardBottomToolbar(cardModal: $currentModal)
}
Here you add your new toolbar at the bottom of the screen.
➤ To see the toolbar, either preview SingleCardView.swift or build and run the app:
Bottom toolbar
ToolbarButtonView 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 up CardBottomToolbar.
raywenderlich.com 341
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
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 CardModal 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 8, “Saving Settings”.
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:)
raywenderlich.com 342
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
}
.padding(.top)
}
}
Using your dictionary, you access the text and image name and then use those for the
button instead of the hard coded Stickers values.
HStack {
Button(action: { cardModal = .photoPicker }) {
ToolbarButtonView(modal: .photoPicker)
}
Button(action: { cardModal = .framePicker }) {
ToolbarButtonView(modal: .framePicker)
}
Button(action: { cardModal = .stickerPicker }) {
ToolbarButtonView(modal: .stickerPicker)
}
Button(action: { cardModal = .textPicker }) {
ToolbarButtonView(modal: .textPicker)
}
}
These are the four buttons that your view needs. Each button shows the correct
image and text for the modal, and the action sets the new card modal state.
➤ Resume the preview, or build and run to see your new buttons:
Button Preview
raywenderlich.com 343
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
As of now, the buttons don’t do anything, but over the next few chapters, you’ll
attach a new modal view to each button.
You now have a prototype of the main views of your app and can visualize how they
will fit together. A prototype is useful, even at this early stage, so that you can show
it to other people to find out what they think of it and whether the interface is
intuitive enough for them to navigate without help. It’s better to find out that your
app is not useful as early as possible in its development so that you can either
incorporate feedback or pivot entirely.
raywenderlich.com 344
SwiftUI Apprentice Chapter 13: Outlining a Photo Collage App
Challenge
Challenge: Tidy up
Make it a habit to regularly tidy up the files in your app. Look down the list of files
and see which ones you can group together. Command-click each file that you want
to group together, then Control-click the selected files and choose New Group
from Selection. Name the group. If you miss any files, just drag them into the group
later.
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.
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.
• When you have a button at the top of your screen, whether it’s leading or trailing,
you can choose to use a navigation bar. The NavigationView has the advantage of
being a standard, built-in control, however, it can reduce your options of custom
transitions.
• 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.
raywenderlich.com 345
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 provided gesture recognizers.
raywenderlich.com 346
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 allow any view that you choose to pass in.
2. Use content as the required View in body and apply modifiers to it.
raywenderlich.com 347
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 need to 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.
➤ In the Model group, create a new Swift file called Transform.swift to hold the
transformation data.
raywenderlich.com 348
SwiftUI Apprentice Chapter 14: Gestures
➤ 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, you can leave
the Type off .zero, and the compiler will work out which type to use.
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 be passing the element’s saved transform in, but for now, just hold
the transform locally.
.frame(
width: transform.size.width,
height: transform.size.height)
raywenderlich.com 349
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 her finger, the view will follow her finger. When she lifts
her finger, the view will remain in in 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.
raywenderlich.com 350
SwiftUI Apprentice Chapter 14: Gestures
The gesture will update transform’s offset property as the dragging takes place.
onChanged(_:) has one parameter of type Value, which contains the gesture’s
current touch location and also the translation since the start of the touch.
.offset(transform.offset)
.gesture(dragGesture)
raywenderlich.com 351
SwiftUI Apprentice Chapter 14: Gestures
➤ Add a new state property to ResizableView to hold the transform’s offset before
you start dragging:
raywenderlich.com 352
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’re going to add width to width and at the same
time, add height to height. So you’re going to redefine + with a special 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
raywenderlich.com 353
SwiftUI Apprentice Chapter 14: Gestures
Here you specify what the + operator should do for a CGSize type. The parameters
are left and right, being 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!
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.
raywenderlich.com 354
SwiftUI Apprentice Chapter 14: Gestures
}
.onEnded { _ in
previousRotation = .zero
}
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 out 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.)
raywenderlich.com 355
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.
raywenderlich.com 356
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 project in Chapter 1, “Checking Your Tools”. You’ll need an Apple
developer account set up in Preferences to run the app on a device.
➤ Set your team identifier on the Cards app’s Signing & Capabilities tab.
➤ Build and run and try out 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.
raywenderlich.com 357
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 unless the user is currently scaling. At the end of the scale, you’ll
calculate the new size of the view and set the scale back to 1.
➤ Open ResizableView.swift, and create a state property to hold the current scale:
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 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 the state property to 1.0 ready for the next scale.
.scaleEffect(scale)
raywenderlich.com 358
SwiftUI Apprentice Chapter 14: Gestures
.gesture(SimultaneousGesture(rotationGesture, scaleGesture))
You can now perform the two gestures at the same time.
➤ Try out your three gestures in the live preview. Then, build and run your app and
try them out in Simulator or, if possible, on a device.
Completed gestures
raywenderlich.com 359
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:
raywenderlich.com 360
SwiftUI Apprentice Chapter 14: Gestures
Here, you set up the content that the view should use and add the modifier(_:)
with your custom view modifier.
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! :]
CardsView()
raywenderlich.com 361
SwiftUI Apprentice Chapter 14: Gestures
➤ In ResizableView.swift, resume your live preview and check out your new
modifier.
import SwiftUI
extension View {
func resizableView() -> some View {
return modifier(ResizableView())
}
}
raywenderlich.com 362
SwiftUI Apprentice Chapter 14: Gestures
You’re extending 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.
content
Eventually content will show card elements, but for now you can test out your new
resizable view. Here you test out 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.
raywenderlich.com 363
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.
➤ Remove .font(.largeTitle).
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.lineLimit(1) ensures the text stays on one line and doesn’t wrap around.
raywenderlich.com 364
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()
Here, you grouped the two views together so they combine to a single view.
raywenderlich.com 365
SwiftUI Apprentice Chapter 14: Gestures
Grouped Views
When you resize now, you’re dragging and resizing 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.
raywenderlich.com 366
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)
}
}
raywenderlich.com 367
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.
CGPoint also has a type property: zero, which describes a point at (0, 0). To use
this 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.
raywenderlich.com 368
SwiftUI Apprentice Chapter 14: Gestures
Going back to your hard coded size values, you’ll now create a file that will hold all
your global constants.
➤ In Config, create a new Swift file called Settings.swift and replace the code with:
import SwiftUI
struct Settings {
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 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 just be able to create an extension.
raywenderlich.com 369
SwiftUI Apprentice Chapter 14: Gestures
For example, they could add a new type property like this:
extension Settings {
static let aNewSetting: Int = 0
}
➤ Open CardThumbnailView.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.
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.
raywenderlich.com 370
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.
➤ Open CardThumbnailView.swift.
.foregroundColor(.random())
Here you’re using the static method that you created on Color. Each time you list
the thumbnails, they will use different colors.
➤ Preview CardsView.swift and see your random card colors. Each time you press
Live Preview, the colors change.
Random color
raywenderlich.com 371
SwiftUI Apprentice Chapter 14: Gestures
Challenge
Challenge: Make a new view modifier
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 CardDetailView, if a modifier has a lot of code in it, save yourself some
code reading fatigue, and separate it out 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 modifier code from CardDetailView and paste the
modifier on CardToolbar’s content.
When you’ve completed the challenge, your code should work the same, but, with
this refactoring, CardDetailView is easier to read.
As always, you’ll find the solution in the challenge folder for this chapter.
raywenderlich.com 372
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 does not require a change of state, you can simply
add a method in a View extension and use that 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.
In the next chapter, you’ll start planning how to structure the app data and work out
how to pass data and actions through your view hierarchy.
raywenderlich.com 373
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 and, finally, how
to Delete it. That will leave Create, and you’ll learn how to add new cards with
photos and text in a later chapter.
• PreviewData.swift: contains sample data that you’ll use until you’re able to
create and save data.
➤ If you are continuing with your own project, be sure to copy these files into your
project.
raywenderlich.com 374
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Data structure
Take another look at the back of the napkin sketch:
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.
raywenderlich.com 375
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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.
raywenderlich.com 376
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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:
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.
raywenderlich.com 377
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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. You’d make those a
structure. However, when you hold a data store that you’re going to use throughout
your app, that would be a good candidate for a class. In addition, if your piece of data
has publisher properties, it must conform to ObservableObject, where the
requirement is that the data type is a class.
Now you’ll get started creating your data model, beginning at the bottom of the data
hierarchy, with the element.
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.
➤ Create a new Swift file called Card.swift and replace the code with:
import SwiftUI
You set up Card to conform to Identifiable, with the protocol’s required property
id. Later, you can use this unique id to locate a card and to iterate through the cards.
You’re also holding a background color for the card and an array of elements for all
the images and text that you’ll place on the card.
raywenderlich.com 378
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ 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.
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
}
raywenderlich.com 379
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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.
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
raywenderlich.com 380
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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. 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 that 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.
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.
raywenderlich.com 381
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
TextElement also conforms to CardElement and holds a string for text, the text
color and the font.
With protocols, you are future-proofing 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()
}
But sometimes you want a default method that is the same across all conforming
types. For example, in your app, a card is going to hold an array of card elements.
Later, you’re going to want to find the index for a particular card element.
raywenderlich.com 382
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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 in the optional Int. 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.
Now that you have your views and your 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 /* */. If this code weren’t commented, it wouldn’t have compiled
until you built your data model.
raywenderlich.com 383
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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 out new views. The card
elements are positioned for portrait orientation on an iPhone 12 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 file, 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.
You use @StateObject to ensure that the data store persists throughout the app.
➤ Add a modifier to CardsView() 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))
raywenderlich.com 384
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
.environmentObject(CardStore(defaultData: true))
You can now access the data store with the preview data in CardsListView.
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)
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:
raywenderlich.com 385
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
CardThumbnailView(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 toggle viewState.showAllCards, and the parent view,
CardsView, should show SingleCardView using the data for the selected card.
Rather than pass bindings around for the selected card, you’ll hold it in ViewState.
Any view with access to the environment object ViewState can now find out what
the currently selected card is.
raywenderlich.com 386
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
viewState.selectedCard = card
Whenever you assign a new value to showAllCards, didSet will observe the change.
You take action when you set showAllCards to true and set selectedCard to nil. If
you need to access the previous value of showAllCards within the didSet closure,
you can use oldValue.
Swift Tip: There’s another property observer called willSet. This fires before
the change is made whereas didSet fires after the change is made. Within the
willSet closure, the property contains the old value, but you can access
newValue, which contains the new value of the property after the closure.
raywenderlich.com 387
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
...
}
.navigationViewStyle(StackNavigationViewStyle())
}
Here you check that selectedCard isn’t nil before showing NavigationView.
CardDetailView(card: selectedCard)
You pass the card to CardDetailView. You’ll get a compile error until you update
CardDetailView to take in a card property.
But wait! Is selectedCard mutable? You’ll want to add stickers and text to the card
later on, so it does need to be mutable.
The answer, of course, is that you created selectedCard 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. You can then pass that to CardDetailView as a binding.
This finds the first card in the array that matches the selected card’s id and returns
the array index, if there is one.
.environmentObject(CardStore(defaultData: true))
Remember that store is your single source of truth for all data that your views
display.
raywenderlich.com 388
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Here you use the selected card’s id to locate the index for the card in store’s cards
array. You can then use the index to pass the card as a mutable object to
CardDetailView.
CardDetailView(card: $store.cards[index])
Now, you’re passing a mutable property to CardDetailView, where you can add a
binding to receive it.
CardDetailView(card: .constant(initialCards[0]))
This creates a binding from initialCards[0]. Your app should now compile.
➤ Build and run, or live preview CardsView, and make sure that everything is still
working.
A working app
raywenderlich.com 389
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Convenience initializer
➤ Open SingleCardView.swift.
The preview no longer works. This is because when you initialize ViewState in the
preview, selectedCard is nil, so the view doesn’t show. For this preview, you’ll have
to initialize ViewState with the selectedCard.
All classes and structures have a designated initializer. Generally this is init(), and
if nothing needs to be initialized, then you don’t have to include it.
Swift allows you to create a convenience initializer — one or more, actually — that
calls the designated initializer, but can also take extra parameters.
You take in a specific card, use the designated initializer to instantiate a ViewState
and, then, fill the properties with the values you require.
The initializer has the keyword convenience. Try leaving this off and see how many
compile errors you get when the compiler thinks that this is ViewState’s designated
initializer.
.environmentObject(ViewState(card: initialCards[0]))
raywenderlich.com 390
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
You initialize ViewState with its convenience initializer, and the preview now works.
Here, you use the background color from the card. The safe area of a device screen is
where navigation bars and toolbars might be. When you supply a background color to
a view, the view renderer does not color these areas. You can override this by
specifying which edges of the safe area you want to ignore. In this case, you ignore
all the safe area and color the whole screen.
raywenderlich.com 391
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ 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:
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.
raywenderlich.com 392
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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 call 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.
When presented with a CardElement, you can find out whether it’s an image or text
depending on its type.
raywenderlich.com 393
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Here you show the first element which contains a hedgehog image. To test the text
view, change the parameter to initialElements[3].
Always be aware of whether your data is mutable or not. Here element is immutable,
but instead of passing a mutable element, you’ll update element data at a later point.
raywenderlich.com 394
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ Preview the view and see the elements all in the center of the view:
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.
raywenderlich.com 395
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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. viewState is an environment object of type ViewState, 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:
raywenderlich.com 396
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
// 1
func bindingTransform(for element: CardElement)
-> Binding<Transform> {
// 2
guard let index = element.index(in: card.elements) else {
fatalError("Element does not exist")
}
// 3
return $card.elements[index].transform
}
3. Return a binding transform for the correct element in the card’s array. card is a
binding in this view,and is connected to the store environment object, which is
the source of truth.
You send ResizableView the transform binding from the current element. You’ll get
a compile error until you have updated all the dependent code.
raywenderlich.com 397
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
.resizableView(transform: .constant(Transform()))
You’re taking in a binding that is a Transform and passing it on to the view modifier.
Your code should now compile.
raywenderlich.com 398
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ Build and run, and choose the first green card. You’ll see that the card elements
are now in their correct position. Any changes you make in position or size will save
to the data store.
.onAppear {
previousOffset = transform.offset
}
When the view first appears, you initialize previousOffset. This will happen only
once.
raywenderlich.com 399
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ 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.
Deletion
Skills you’ll learn in this section: Context menu; deletion; remove from
array
You have now achieved both Read and Update in the CRUD functions. Next you’ll
tackle Deletion.
raywenderlich.com 400
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
Here you retrieve the index of the card element. You then remove the element from
the array using the index.
➤ Build the app, and you get a compile error: Cannot use mutating member on
immutable value: ’self’ is immutable. Even though you have created
elements with a var, for any method inside Card, the properties are immutable.
Fortunately, all you have to do is tell the compiler that you really do want to change
one of the properties by marking the method as mutating.
.contextMenu {
Button(action: { card.remove(element) }) {
Label("Delete", systemImage: "trash")
}
}
This context menu will pop up when you perform a long press on a card element. You
can have multiple buttons in a context menu, but yours will just have one, with the
trash SFSymbol next to “Delete”.
raywenderlich.com 401
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
➤ Build and run, choose the first card, and long press on an element. You’ll see the
context menu pop up. Tap Delete to delete the element, or tap away from the menu if
you decide not to delete it.
raywenderlich.com 402
SwiftUI Apprentice Chapter 15: Structures, Classes & Protocols
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 so that you can delete a card.
Swift Tip: When using a class, you don’t add mutating to a method. In a
class, all properties declared with var are mutable.
2. In CardsListView, add a new context menu to a card with a delete option that
calls your new method to remove the card.
You’ll find the solution to this challenge in the challenge folder for this chapter.
raywenderlich.com 403
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. When you create a value type, you are always copying the
data. Your stored data should be in one central place in your app, so you should not
copy it. Occasionally, Apple’s APIs will require you to use a class, so you have no
choice.
• 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 get familiar with them.
• When designing an app, consider how you’ll implement CRUD. In this chapter, you
implemented Read, Update and Delete. Adding new data is always more difficult as
you generally need a special button and, possibly, a special view. You’ll add photos
from your photos collection to your cards later on.
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
states “I don’t do object-oriented”.
raywenderlich.com 404
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.
raywenderlich.com 405
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
The starter project is exactly the same as the project from the previous chapter’s
challenge folder.
Asset catalog
Skills you’ll learn in this section: Managing images in asset catalogs; app
icons; screen resolution; vector vs bitmap
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.
The asset catalog also holds the app icon, the launch screen image and launch screen
background color.
raywenderlich.com 406
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
This is where you can specify which icon to use for your app. You can choose to hold
the icons in folders instead of asset catalogs, but it’s much easier to keep them in the
asset catalog as Apple intended.
➤ Click the arrow to the right of the drop-down. This will take you to the app icon
set in the asset catalog. The iOS app template created this empty icon set when you
first created your project.
If you’re lucky enough to have a designer for your app, as we are, they will distribute
a design file, not code. This might be Sketch files or, as in our case, a Figma file.
raywenderlich.com 407
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
This design includes the app icon. This is a single size, but for the app icon, you’ll
need multiple sizes for the different devices. Fortunately, Figma is a vector app, and
you can export the icon design to various sizes of PNG format. You can also create
icons from a single image at https://appicon.co.
Apple resolved this scaling difficulty with an @ suffix. An image that is scaled by 2
has a suffix of @2x, and one scaled by 3 has @3x.
➤ In Finder, open the assets folder for this chapter and open the App Icon
subfolder. This holds the icons in PNG format, exported from Figma.
➤ With Xcode open on the app icon and Finder open at the App Icon folder, drag
each icon to its correct spot. Where iPad and iPhone use the same pixel and point
size, you can use the same icon image.
raywenderlich.com 408
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Note: Even though there are now no supported non-retina devices, for iPad
you should still provide 1x icons. iPadOS sometimes uses these when running
iPhone apps scaled to iPad.
App icons
The image above has the 29 pt icons swapped around, showing a yellow exclamation
mark as an error. If you see errors, you should correct them. This usually happens
when you drag multiple assets at the same time and Xcode doesn’t know where to
put them. Consider dragging them one by one or in small groups.
➤ Build and run, and swipe up from the bottom to exit your app. You’ll see your new
icon takes the place of the old placeholder icon.
raywenderlich.com 409
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Vector vs bitmap
You imported bitmap PNG images for the icons. 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, open your assets folder for this chapter and drag in error-image.svg to
the asset catalog panel under AppIcon.
Error image
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.
raywenderlich.com 410
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Single scale
Xcode removes the 2x and 3x options in the center panel. When you build for iPhone
12 Pro, which is 2x resolution, 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.
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 shows while your app is launching. You’ve already come across Info.plist
in Chapter 8, “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.
raywenderlich.com 411
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
You can add new items to this either by right-clicking an entry and choosing Add
Row or by selecting an object and clicking the + sign that appears. You can delete
items by clicking the - sign.
Info.plist
➤ Under Launch Screen, add items for Image Name and for Background color.
You’ll probably need to resize the columns to show Value.
LaunchImage
LaunchColor
➤ 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.
raywenderlich.com 412
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
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’re going to use a single scale SVG image.
Launch Image
This SVG with a transparent background has a native size of 200x500px. Xcode will
create the appropriately scaled bitmap image from this and display it in the center of
the screen. Landscape iPhones need an image with a smaller height, so you’ll use size
classes to decide which image to load.
raywenderlich.com 413
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
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 at https://
apple.co/348lVx0 under the section Size Classes.
Size classes
For height in portrait mode, all devices fit into the regular height section. For height
in landscape mode, all iPhones fit into the compact height section. In landscape,
most iPhones fall into the compact width section. The max size iPhones and iPhone
11 and 12 use regular width in landscape.
iPads are always regular width and regular height. However, you still have to take
into account size classes on iPad, because of split screen. 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.
raywenderlich.com 414
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 does not 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.
raywenderlich.com 415
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
There’s one thing that an asset catalog does not allow you to do, and that is
enumerate through all the images contained in it.
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.
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.
A solution to this is to use reference folders. Instead of using an asset catalog, you’ll
keep your sticker folder outside of your project and access it from the project as a
reference folder.
Camping stickers
Note: These stickers are from Pixabay: https://bit.ly/3vojAJf. There are several
sites, such as https://unsplash.com and https://www.pexels.com, where
creators share their work and allow reuse of images. Before adding an image to
your app, always check that the license allows commercial use and follow the
license instructions. The stickers use Pixabay’s license: free for commercial
use with no attribution.
raywenderlich.com 416
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ Open CardDetailView.swift.
You’ll get a compile error because sheet(item:) requires its parameter to be a type
that conforms to Identifiable. This seems reasonable, as under the hood, the
system should track which modal you’re currently using.
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.
raywenderlich.com 417
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Instead of a stored property, this var is a computed property. Now, when you create a
CardModal object, each object will have a different id calculated from the
enumeration’s hash value.
➤ Open SingleCardView.swift and try out your Stickers button in the live preview.
Swipe down to dismiss the modal.
raywenderlich.com 418
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Reference folders
Skills you’ll learn in this section: Groups; reference folders; loading images
from files; lazy loading
When you look in your Project navigator, currently all your groups, except for the
asset catalogs, have yellow folder icons.
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.
raywenderlich.com 419
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
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.
If your current file is in a logical group, the yellow icon has a small triangle at the
bottom left.
Groups
On the other hand, Xcode does not 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’re going to treat this folder as the master folder for your sticker assets. Any
stickers that your artists create should go into this folder.
Generally, when you import files, you’ll check Copy items if needed, and you’ll
choose Create groups rather than Create folder references. Just for this time,
you’ll uncheck Copy items if needed and choose Create folder references.
raywenderlich.com 420
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.
raywenderlich.com 421
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
If you tried this with yellow groups (don’t!), Xcode wouldn’t be able to find any files
you moved in Finder.
An 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 StickerPicker.swift.
raywenderlich.com 422
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
StickerPicker()
➤ Open SingleCardView.swift, preview it, and pin the preview, so that 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.
raywenderlich.com 423
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
"/Stickers/Camping/fire.png") {
Image(uiImage: image)
} else {
EmptyView()
}
}
}
3. Load the UIImage using the full name and path of the sticker and use the
uiImage parameter for creating the Image view.
➤ Resume the live preview, tap Stickers, 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.
raywenderlich.com 424
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.
raywenderlich.com 425
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
This will perform the loop in exactly the same way, but the given code is more
succinct.
Now, you’ll iterate through all the theme directories and retrieve the file names
inside.
return stickerNames
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.
raywenderlich.com 426
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
ScrollView {
ForEach(stickerNames, id: \.self) { sticker in
Image(uiImage: image(from: sticker))
.resizable()
.aspectRatio(contentMode: .fit)
}
}
.onAppear {
stickerNames = loadStickers()
}
}
Instead of just showing one sticker, you’re iterating through all the sticker names
and creating an Image from the UIImage. Using onAppear(perform:), you can load
the stickers when StickerPicker first loads.
➤ 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
raywenderlich.com 427
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ To get the stickers to load lazily, in body, Command-click ForEach and embed it
in a VStack.
LazyVStack {
➤ 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.
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’re still using 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.
raywenderlich.com 428
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
➤ To visualize this, in the design canvas, stop the live preview, if there is one, and
scroll down to Sticker Picker Previews. In code, place the cursor just after the Image
modifiers. 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).
StickerPicker()
.previewLayout(PreviewLayout.fixed(width: 896, height: 414))
Grid in landscape
raywenderlich.com 429
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
Swift Tip: As well as adaptive, GridItem.size can be fixed with a fixed size,
or flexible, which sizes to the available space.
raywenderlich.com 430
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
The parent of the modal will pass in a state property to hold the selected image.
.onTapGesture {
stickerImage = image(from: sticker)
presentationMode.wrappedValue.dismiss()
}
When the user taps an image, you’ll update the bound sticker image and dismiss the
modal.
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.
➤ Locate the sheet(item:) with the stickerPicker case. This will have a compile
error as you’re not yet passing the state property to StickerPicker.
StickerPicker(stickerImage: $stickerImage)
Now that you’ve bound the state property, your app should compile again.
.onDisappear {
if let stickerImage = stickerImage {
raywenderlich.com 431
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
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 written
addElement(uiImage:).
➤ Open Card.swift in the Model group and add this new method:
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 the orange second card and add some stickers to it. Resize
and reposition the stickers as you want and create a masterpiece :].
raywenderlich.com 432
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 also 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.
raywenderlich.com 433
SwiftUI Apprentice Chapter 16: Adding Assets to Your App
Key points
• Asset catalogs are where you should be managing your images and colors most of
the time.
• 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.
raywenderlich.com 434
17 Chapter 17: Interfacing
With UIKit
By Caroline Begbie
Sometimes you’ll need a user interface feature that is not available in SwiftUI. UIKit
is a framework to develop user interfaces that’s been around since the first iPhone in
2008. Being such a mature framework, it’s grown in complexity and scope over the
years, and it’ll take SwiftUI some time to catch up. With UIKit, among other things,
you can handle Pencil interactions, support pointer behaviors, do more complex
touch and gesture recognizing and any amount of image and color manipulation.
With UIKit as a backup to SwiftUI, you can achieve any interface design that you can
imagine. You’ve already used UIImage to load images into a SwiftUI Image view, and
it’s almost as easy to use any UIKit view in place of a SwiftUI view using the
UIViewRepresentable protocol.
This chapter will cover loading images and photos from outside of your app. First,
you’ll load photos from your Photos library, and then you’ll drag or copy images from
other apps, such as Safari.
raywenderlich.com 435
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
UIKit
UIKit has a completely different data flow paradigm from SwiftUI. With SwiftUI, you
define Views and a source of truth. When that source of truth changes, the views
automatically update. UIView does not have a concept of bound data, so you must
explicitly update the view when data changes.
UIKit vs SwiftUI
Whenever you can for new apps, you should stick with SwiftUI. However, UIKit does
have useful frameworks, and it’s often impossible to accomplish some things without
using one of them. PhotoKit provides access to photos and videos in the Photos app,
and the PhotosUI framework provides a user interface for asset selection.
raywenderlich.com 436
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
To get started with UIViewRepresentable, you’ll first show a basic, colored UIView
on top of a simple SwiftUI View.
➤ In the group Card Modal Views, create a new Swift file named
PhotoPicker.swift.
import SwiftUI
Here you create a structure that will conform to the protocol UIViewRepresentable.
PhotoPicker()
raywenderlich.com 437
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Compare previews
Whereas the SwiftUI text view takes up just enough space to show itself, a UIKit view
will take up the whole of the available space.
Delegate pattern
raywenderlich.com 438
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Using delegation, you can change the behavior of the table without having to
subclass UITableView.
Picking photos
As the system photo picker is a subclass of UIViewController, your Representable
structure will be a UIViewControllerRepresentable.
import PhotosUI
raywenderlich.com 439
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
func updateUIViewController(
_ uiViewController: UIViewControllerType,
context: Context
) {
}
}
// 1
var configuration = PHPickerConfiguration()
configuration.filter = .images
// 2
configuration.selectionLimit = 0
// 3
let picker =
PHPickerViewController(configuration: configuration)
return picker
2. Specify the number of photos a user is allowed to pick. Use 0 to allow selection of
multiple photos.
The class doesn’t need to be internal to PhotoPicker, but you probably won’t want
to use it on its own and making it internal means that it can only be in scope inside
PhotoPicker.
raywenderlich.com 440
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
func picker(
_ picker: PHPickerViewController,
didFinishPicking results: [PHPickerResult]
) {
}
When the user taps Add after selecting photos, PHPickerViewController will call
this delegate method and pass the selected photos as UIImages in an array of
PHPickerResult objects. You’ll process each of these images.
CardDetailView will pass an empty array to PhotoPicker, and the delegate method
will fill this array with the picked results.
The preview won’t be acting on the images array, so pass a constant array to the
binding.
init(parent: PhotoPicker) {
self.parent = parent
}
raywenderlich.com 441
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
picker.delegate = context.coordinator
NSItemProvider
You’ve now set up the interface between the SwiftUI PhotoPicker and the UIKit
PHPickerViewController. All that’s left is to load the images array from the modal
results.
Swift Tip: Using map with the key path \.itemProvider is syntactic sugar for
let itemProviders = results.map { $0.itemProvider }
raywenderlich.com 442
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Any class with the NS prefix is an Objective-C class which inherits from NSObject. So
NSItemProvider ultimately inherits from NSObject. When you want to transfer data
around your app, or between apps, you use item providers. You can ask the item
provider whether it can load a particular type and then asynchronously load it. Later
in this chapter, you’ll be dragging images from Safari into your app — again using
item providers.
➤ Inside the for loop, add the code to load the image:
// 1
if item.canLoadObject(ofClass: UIImage.self) {
// 2
item.loadObject(ofClass: UIImage.self) { image, error in
// 3
if let error = error {
print("Error!", error.localizedDescription)
} else {
// 4
DispatchQueue.main.async {
if let image = image as? UIImage {
self.parent.images.append(image)
}
}
}
}
}
2. Load the UIImage. The closure parameters provide an object that conforms to
NSItemProviderReading and an error object.
3. If the error is not nil, print out the description. In a full app, you would provide
error handling.
4. Ensure that the passed object is a UIImage and add the image to PhotoPicker’s
image array asynchronously. You must do so on the main queue because it will
cause an update to the UI. All NSItemProvider completion closures execute on
an internal system queue in the background.
With the images loading, you should dismiss the system modal.
raywenderlich.com 443
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
parent.presentationMode.wrappedValue.dismiss()
This will tell the environment to close the modal. PhotoPicker is now all ready for
use in SwiftUI.
➤ Live preview PhotoPicker to see how the system photo picker works. You can
select multiple photos and also select from your photo albums. You can also show all
the selected images.
raywenderlich.com 444
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
case .photoPicker:
PhotoPicker(images: $images)
.onDisappear {
for image in images {
card.addElement(uiImage: image)
}
images = []
}
Here you show the modal, passing in the array to hold the photos the user will select.
When the modal disappears, you process the array and add the photos to the card
elements. Finally, you clear the images array to make it ready for the next time.
➤ Build and run, and choose the second orange card. Add a couple of photos using
the system photo picker. The app adds the photos to the card elements so that you
can resize and reposition them.
Added photos
Note: At the time of writing, the simulator pink flowers photo causes an error.
This appears to be an Apple bug, but it does give you the chance to make sure
that your PhotoPicker error checking works. In the console, you should see
Error! Cannot load representation of type public.jpeg.
raywenderlich.com 445
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
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. With your
mouse cursor just touching the black bevel at the bottom of the app, drag upward
slowly to show the dock. One of the apps on the dock should be Safari.
➤ Hold down the Safari icon and drag it off the dock to the right of the Cards app. A
space will open up for you to drop the icon.
Drop Safari
➤ Use the bar in the middle to resize each app to take up half the iPad screen area.
raywenderlich.com 446
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
➤ In the Cards app, tap the orange card. In Safari, Google your favorite animal and
tap Images. Long press an image until it gets slightly larger and drag it onto your
orange card.
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.
Uniform Type Identifiers, or UTIs, identify file types. For example, PNG is a
standard UTI, with the identifier public.png. It’s a subtype of the image data base
type public.image.
raywenderlich.com 447
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Note: 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 UTI and include it in
Info.plist. For this app, however, you only need to use public.image to receive any
image format.
// 1
.onDrop(of: [.image], isTargeted: nil) {
// 2
itemProviders, _ in
// 3
return true
}
1. This is where you specify the identifier of the file type you wish to process; in
your case .image. There are several onDrop... modifiers. This one takes an array
of UTTypes and a Boolean binding to indicate whether there is a drag and drop
operation currently happening.
2. The closure presents the dropped items in an array of NSItemProviders and the
drop location. For the moment you won’t use the location, so you replace the
parameter with _.
3. Returning true indicates to the system that the drop was successful.
This time, even though the drop does nothing, you get the plus sign.
Drop is active
raywenderlich.com 448
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
This code is almost exactly the same as the code you wrote earlier for the photo
picker. Iterate through the items and load them as a UIImage. Here, you add the
image directly to the card’s elements.
➤ Build and run. Repeat dragging an image on to the orange card and this time the
drop action saves the image to the card elements.
A tower of giraffes
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. When you’ve collected all the images, drag them to Cards.
Currently, no matter where you drop images, the card adds the new elements at the
center. You can use the drop location to place the element where you dropped it.
raywenderlich.com 449
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
However, 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”.
import SwiftUI
You create a new structure that will conform to DropDelegate and receive the card
that the drop delegate should update. You need to implement one required method
to conform to DropDelegate.
At the start of the method, you extract the item providers from the drop info, and
then the rest of the code is the same as you have in CardDetailView.
raywenderlich.com 450
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
If you need to have complete control of where in the screen your user is dragging
items, then implement these methods.
Here you still use the image UTI, but you offload the code into CardDrop.
With this one line of code, you have reduced the apparent complexity. The CardDrop
code is difficult to read, and you don’t need to be viewing it every time you’re
updating your card detail code. It’s a good idea to reduce brain overload whenever
you can. :]
➤ Build and run and your app works the same as it did before.
raywenderlich.com 451
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Challenge
Now that you know how to host UIKit views in SwiftUI, you have access to a wide
range of Apple frameworks. One fun framework is PencilKit where you can draw into
a canvas.
Your challenge is to write a few lines of code and run a live preview in which you can
scribble.
• Create a new View and import PencilKit. Create a PKCanvasView state property.
Pass this property to a UIViewRepresentable object.
• There’s only two extra lines of code needed. In makeUIView(context:), set the
canvas drawingPolicy to anyInput to allow input from both finger and Pencil and
return the canvas.
If you have any difficulty, you’ll find the solution to this challenge in the challenge
folder for this chapter in the file PencilView.swift.
raywenderlich.com 452
SwiftUI Apprentice Chapter 17: Interfacing With UIKit
Key points
• SwiftUI and UIKit can go hand in hand. Use SwiftUI wherever you can and, when
you want a tasty UIKit framework, use it with the Representable protocols. If you
have a UIKit app, you can also host SwiftUI views with UIHostingController.
• Using Uniform Type Identifiers and the onDrop modifier, you can support drag and
drop in your app.
raywenderlich.com 453
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.
Currently when you tap Frames, a modal pops up with an EmptyView. You’ll replace
this modal view with a FramePicker view where you’ll be able to select a shape.
raywenderlich.com 454
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.
raywenderlich.com 455
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
Circle()
Capsule()
Ellipse()
}
.padding()
}
These are the five built-in shapes, which fill as much space as they can.
raywenderlich.com 456
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.
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.
raywenderlich.com 457
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
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(_:).
raywenderlich.com 458
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
➤ Preview 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 with the available size, you must
use relative coordinates, rather than absolute values.
raywenderlich.com 459
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
raywenderlich.com 460
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.
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.
raywenderlich.com 461
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
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
raywenderlich.com 462
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.
The lens shape will consist of two quadratic curves, like an ellipse with a point at
each end.
raywenderlich.com 463
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
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 above diagram, and the second curve
mirrors it.
raywenderlich.com 464
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
raywenderlich.com 465
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.
raywenderlich.com 466
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
As well as displaying a shape view, you can use a shape to clip another view. You’re
going to list all your shapes in a modal so that the user can select a photo and clip it
to a chosen shape.
➤ In the Card Modal Views group, create a new SwiftUI View file called
FramePicker.swift. This will be very similar to StickerPicker.swift, but will load
your custom shapes into a grid instead of stickers.
First, you’ll set up an array of all your shapes for the modal to iterate through.
raywenderlich.com 467
SwiftUI Apprentice Chapter 18: Paths & Custom 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
raywenderlich.com 468
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 }
}
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.
raywenderlich.com 469
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
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.
Unfortunately, there isn’t a built-in AnyShape for your array of Shapes, but it’s quite
easy to make one, when you know what the requirements for a Shape are.
import SwiftUI
AnyShape conforms to Shape with the required path(in:). You’ll get a compile error
until you return a path from the method.
To convert your custom shape to an AnyShape, you’ll use an initializer which takes in
a generic Shape. This initializer will create a closure that uses this shape to create a
path. You’ll store this closure as a property, and when a view calls for the path, you’ll
perform the closure.
If you need to review closures, take a look at Chapter 9, “Saving History Data”.
You’ll perform the custom shape’s path(in:) when it’s required. path(in:) takes in
a CGRect and returns a Path.
raywenderlich.com 470
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
// 1
init<CustomShape: Shape>(_ shape: CustomShape) {
// 2
self.path = { rect in
// 3
shape.path(in: rect)
}
}
You take in the custom shape when you create the structure. To explain the code:
3. When you execute the closure, it calls the shape’s path(in:) using the supplied
rect.
path(rect)
You call your path closure supplying the current rect as the parameter. The method
now returns the custom shape’s path as the Path.
Your code now compiles, and AnyShape is ready to convert any custom shape to
itself.
extension Shapes {
static let shapes: [AnyShape] = [
AnyShape(Circle()), AnyShape(Rectangle()),
AnyShape(Cone()), AnyShape(Lens())
]
}
This holds a type-erased list of all your defined shapes. When you create more
shapes, add them to this array.
raywenderlich.com 471
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
// 1
@Binding var frame: AnyShape?
private let columns = [
GridItem(.adaptive(minimum: 120), spacing: 10)
]
private let style = StrokeStyle(
lineWidth: 5,
lineJoin: .round)
This is almost exactly the same code as you wrote for StickerPicker. The
exceptions are:
raywenderlich.com 472
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
4. You need to 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 you tap the shape, you update frame and dismiss the modal.
Shapes Listing
raywenderlich.com 473
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
case .framePicker:
FramePicker(frame: $frame)
.onDisappear {
if let frame = frame {
card.update(
viewState.selectedElement,
frame: frame)
}
frame = nil
}
Here you call the modal and then update the card element with the frame. As you
haven’t written update(_:frame:) yet, you’ll get a compile error.
This will hold the element’s frame. You add it only to ImageElement, because the
frame will only clip images.
Here you pass in the element and the frame. Because element is immutable and you
need to update its frame, you create a new mutable copy and update elements with
this new instance.
The modifier you’ll add is .clipShape(_:), but you only want to add it if the
element’s frame is not nil. Surprisingly, it’s not easy to add a conditional modifier in
SwiftUI, but the following is a solution when the existing code is quite simple.
raywenderlich.com 474
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
You recreate body and use bodyMain in both parts of the conditional. If there is a
frame, add the modifier.
➤ Build and run the app, and choose the green card. Tap the giraffe and choose
Frames. Select a frame and the giraffe photo gets clipped to that shape.
Clipped giraffe
raywenderlich.com 475
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:
raywenderlich.com 476
SwiftUI Apprentice Chapter 18: Paths & Custom Shapes
3. When the image has a frame, replace the border modifier with an overlay of the
stroked frame.
4. When you tap the space outside the frame, but within the original unclipped
image, SwiftUI still thinks you’re tapping the image. After the overlay, add the
modifier .contentShape(frame). This will clip the tap area to the frame.
Check your changes out by running the app or by live previewing SingleCardView.
A selected giraffe
raywenderlich.com 477
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.
raywenderlich.com 478
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. There are a number of ways to save data
that you could choose.
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 text 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.
raywenderlich.com 479
SwiftUI Apprentice Chapter 19: Saving Files
In the first challenge for this chapter, you’ll be storing 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.
raywenderlich.com 480
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 CardDetailView 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 struct. 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 an overhead. 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")
}
You’ll come back to this method to perform the saving later in this chapter.
raywenderlich.com 481
SwiftUI Apprentice Chapter 19: Saving Files
.onDisappear {
card.save()
}
➤ Build and run, tap a card, then tap Done. You’ll see “Saving data” appear in the
console.
Saving data
raywenderlich.com 482
SwiftUI Apprentice Chapter 19: Saving Files
➤ Add a new modifier to content before the .onDisappear you added earlier:
➤ Build and run, tap a card to open it, and exit your app by swiping up from the
bottom. You should see the console message “Saving data”.
Saving data
➤ Return to the app in the simulator. It will resume inside the card where you left it.
raywenderlich.com 483
SwiftUI Apprentice Chapter 19: Saving Files
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 find out 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.
➤ Open CardsApp.swift and add this code to the end of the file:
raywenderlich.com 484
SwiftUI Apprentice Chapter 19: Saving Files
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.
raywenderlich.com 485
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(FileManager.documentURL ?? "")
}
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",
"Manda"
],
"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 very easy to store data.
raywenderlich.com 486
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()
}
raywenderlich.com 487
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?
Before continuing, remove the sample Team code that you created.
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:).
raywenderlich.com 488
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.
raywenderlich.com 489
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.
raywenderlich.com 490
SwiftUI Apprentice Chapter 19: Saving Files
Encoding ImageElement
➤ Open CardElement.swift and take a look at ImageElement.
When saving an image element, you don’t need to save the UUID, as it will get
reconstructed when you load up the element. You’ll save the transform, which is now
Codable. Image and AnyShape, however, are not. At the point of loading the Image,
you have access to the UIImage, and it’s quite easy to save that to a file and record
the filename.
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 provided code 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 both the loaded Image and the string filename.
You’ll also need to remove the image file from disk when the user deletes the
element.
raywenderlich.com 491
SwiftUI Apprentice Chapter 19: Saving Files
You check that the element is an ImageElement and use the provided method 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.
You’ll save the transform, filename and frame to disk. It’s unnecessary to store the
image element’s id, as that can be generated when you load the element. You’ll also
recreate the Image from the stored image file when you load the element.
raywenderlich.com 492
SwiftUI Apprentice Chapter 19: Saving Files
2. Decode the image filename. This is an optional and, if you try and decode
something that does not exist, it will throw an error. Check if it exists using
decodeIfPresent(_:forKey:).
4. If there’s an error loading the image, use the error image in Assets.xcassets.
Here you’re encoding the transform and the filename. frame is not yet Codable, so
you’ll add that in a moment.
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.
raywenderlich.com 493
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.
var id = UUID()
Here you encode the id as a UUID string. You also extract all the image elements
from elements using compactMap(_:)
When code is more complex than the above, you can replace the closure with:
raywenderlich.com 494
SwiftUI Apprentice Chapter 19: Saving Files
func save() {
do {
// 1
let encoder = JSONEncoder()
// 2
let data = try encoder.encode(self)
// 3
let filename = "\(id).rwcard"
if let url = FileManager.documentURL?
.appendingPathComponent(filename) {
// 4
try data.write(to: url)
}
} catch {
print(error.localizedDescription)
}
}
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.
raywenderlich.com 495
SwiftUI Apprentice Chapter 19: Saving Files
➤ Add:
save()
• remove(_:)
• addElement(uiImage:)
• update(_:frame:)
You’re already calling save() when the user presses the Done button and, also,
when he exits the app and the 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 the simulator, choose the green 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 this in TextEdit — you should just be able to double click it
to open it.
{"id":"6D924181-ABFC-457A-A771-984E7F3805BD","imageElements":
[{"imageFilename":null,"transform":{"offset":[4,-137],"size":
[412,296],"rotation":{"degrees":-6.0000000000000009}}},
{"imageFilename":"8808F791-E832-465D-911A-A250B91A5141",
"transform":{"offset":[0,0],"size":[250,180],"rotation":
{"degrees":0}}}]}
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 two imageElements. The first element will have null in the filename as it
was provided by the preview data and never saved to a file. The second element will
have the added photo with the name of the saved file.
If you want to make the output more human readable, in save(), after initializing
encoder, you can add:
encoder.outputFormatting = .prettyPrinted
raywenderlich.com 496
SwiftUI Apprentice Chapter 19: Saving Files
Loading Cards
Skills you’ll learn in this section: File enumeration; Equatable
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
guard let path = FileManager.documentURL?.path,
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
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
}
}
raywenderlich.com 497
SwiftUI Apprentice Chapter 19: Saving Files
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.
Here you create a new card with a random background color, add it to the array of
cards and save it to disk.
raywenderlich.com 498
SwiftUI Apprentice Chapter 19: Saving Files
➤ Open CardsView.swift.
VStack {
Button(action: {
viewState.selectedCard = store.addCard()
viewState.showAllCards = false
}, label: {
Text("Add")
})
CardsListView()
}
You set up a temporary button to add a card. When you tap the 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
➤ Tap Add to add a new card. 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 underneath the Add button.
raywenderlich.com 499
SwiftUI Apprentice Chapter 19: Saving Files
When you re-run your app, any cards you create will show up just as you created
them.
Adding a card
Your app is in great shape now. There are still a couple of problems that you may
have noticed. You’re not yet storing the card’s background color between sessions, so
it reverts to the card background’s default yellow. You’re also not persisting any clip
frames. Neither Color nor AnyShape conforms to Codable, and they are a little
harder to persist than the previous types.
if let index =
Shapes.shapes.firstIndex(where: { $0 == frame }) {
try container.encode(index, forKey: .frame)
}
Here you’re finding the first shape which is equal to your element’s frame. You’ll get
an error because AnyShape doesn’t conform to Equatable, which means that you
can’t compare the shape to the frame.
raywenderlich.com 500
SwiftUI Apprentice Chapter 19: Saving Files
➤ Compile and click the red dot next to the compile error. Click Fix to add protocol
stubs.
This required method defines the == operator, with the left hand side and right hand
side as parameters. The returned Boolean indicates whether the result is equal or
not.
You create the path of the two shapes in a small rectangle. The size of the rectangle
doesn’t matter as long as it’s not zero. You then compare the two paths to see if they
are the same.
Your app will now compile, and you can compare two AnyShapes.
➤ Open CardElement.swift where you set up the encoding. You’ll now do the
decoding.
if let index =
try container.decodeIfPresent(Int.self, forKey: .frame) {
frame = Shapes.shapes[index]
}
raywenderlich.com 501
SwiftUI Apprentice Chapter 19: Saving Files
Here you decode the index, if there is one, and set up the frame using the index.
➤ Build and run and test that your frames are being saved:
Challenges
Challenge 1: Save the background color
As mentioned before, 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:
• colorComponents() separates out a Color into red, green, blue and alpha
components. These are returned in an array of four CGFloats. CGFloat conforms
to Codable, so you’ll be able to store the color.
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,
raywenderlich.com 502
SwiftUI Apprentice Chapter 19: Saving Files
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.
1. Create a new SwiftUI file for your text entry modal. You will need to hold a
TextElement binding property sent from CardDetailView to hold the text data
temporarily, just as you’ve done for your other picker modals with frame and
stickerImage. This time, though, in CardDetailView, 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 file, add an environment presentationMode property as you did for
your other modals and replace body contents with:
let onCommit = {
presentationMode.wrappedValue.dismiss()
}
TextField(
"Enter text", text: $textElement.text, onCommit: onCommit)
raywenderlich.com 503
SwiftUI Apprentice Chapter 19: Saving Files
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.
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.
raywenderlich.com 504
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 that 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 it needs the memory.
• JSON format is a standard for transmitting text 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 want to.
raywenderlich.com 505
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.
• 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.
• 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.
raywenderlich.com 506
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
• 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.
• CardDrop has size and frame properties that you’ll use in the Challenge.
View Hierarchy
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
through all these views through bindings.
raywenderlich.com 507
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
App Design
The top segmented controller will swap the display between a grid list view and a
carousel. The bottom button is wide. This is the design that you’ll attempt to
duplicate.
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.
➤ Open CardsView.swift.
.background(
Color("background")
.edgesIgnoringSafeArea(.all))
raywenderlich.com 508
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
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 edgesIgnoringSafeArea(_:), 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.
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.
raywenderlich.com 509
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
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.
.background(Color.red)
➤ Preview the view. 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.
raywenderlich.com 510
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Here you create a horizontal stack with two Text views. The second Text has
padding.
raywenderlich.com 511
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
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:).
.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.
raywenderlich.com 512
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
GeometryReader
Skills you’ll learn in this section: GeometryReader; use given view size to
layout child views
However, when you need to know the size of the parent so that you can lay out child
views with more precision, there’s another flexible view that takes up the whole
available space and gives you the size in points. GeometryReader is a container view
that returns its preferred size. Later, you’ll use GeometryReader to determine the
size of card thumbnails based upon the width of the available space.
GeometryReader { proxy in
HStack {
...
}
.frame(maxWidth: .infinity)
.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.
raywenderlich.com 513
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.
➤ Open CardsListView.swift.
GeometryReader { proxy in
raywenderlich.com 514
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
ScrollView(showsIndicators: false) {
...
}
}
ScrollView in GeometryReader
Notice the side effects of using GeometryReader. CardsListView’s parent is ZStack
in CardsView.swift. GeometryReader takes all the available space and passes that
back to ZStack. That means that ZStack’s light gray background color now fills the
entire screen.
The second side effect is that the central alignment of ScrollView is lost. You’ll fix
this when you add a grid view shortly.
raywenderlich.com 515
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Within ScrollView, you now have access to proxy.size which gives you the entire
available space of CardsListView’s parent.
You now pass the size to the thumbnail, which can take appropriate action. You’ll get
a compile error until you fix CardThumbnailView.
➤ Open CardThumbnailView.swift.
The thumbnail size will scale to 12 percent of the final size of the card. When the size
offered has a width or height greater than 500 points, then you scale to 20 percent of
the final size.
.frame(
width: Settings.thumbnailSize(size: size).width,
height: Settings.thumbnailSize(size: size).height)
Now that you know the screen space available to the card list, you calculate the
thumbnail’s frame accordingly.
raywenderlich.com 516
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Build and run on both iPad and iPhone simulators and compare the two thumbnail
sizes.
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.
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.
raywenderlich.com 517
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
GeometryReader { proxy in
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns(size: proxy.size), spacing: 30) {
ForEach(store.cards) { card in
...
}
}
}
}
➤ Build and run the app on various simulators and switch from portrait to landscape
to see how the columns vary.
Note: If you want to visualize how much space views take up, try
adding .background(Color.red) as a modifier to the various views.
➤ Open CardsView.swift.
➤ In CardsView, remove VStack and its contents, so that ZStack only contains
SingleCardView and its conditional:
ZStack {
if !viewState.showAllCards {
raywenderlich.com 518
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
SingleCardView()
}
}
.background...
You don’t always have to create new structures for views. Sometimes, if it’s a simple
view and you’re only using it once, it’s easier to keep track of views as properties or
methods.
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
viewState.selectedCard. You set viewState.showAllCards to false, so that
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.
CardsListView()
VStack {
Spacer()
createButton
}
raywenderlich.com 519
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
VStack and Spacer will place the button at the bottom of the screen.
ZStack {
CardsListView()
VStack {
Spacer()
createButton
}
if !viewState.showAllCards ...
}
➤ Build and run (or use Live Preview) and test out your new button.
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(action: {
...
}) {
raywenderlich.com 520
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Build and run again. The button looks the same but is tappable all the way across.
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.
This is a very subtle outline color, but if your designer tells you to add it, trust the
designer. :]
Color(UIColor.systemBackground)
As the card color is now the same as the screen’s background color you’ll be able to
see the shadow.
raywenderlich.com 521
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Preview the view, switching between Dark and Light color schemes in Inspect
Preview.
card.backgroundColor
Outline Colors
raywenderlich.com 522
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.
raywenderlich.com 523
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
The Create button text is now black and doesn’t show on the black bar.
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.
➤ Live preview the view in both Light and Dark color schemes:
Accent color
Throughout the app, text takes on AccentColor in Assets.xcassets except for where
you specify accentColor(_:) on specific views.
raywenderlich.com 524
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
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.
➤ Open CardDetailView.swift.
These methods calculate the size and scale of the card view with the correct aspect
ratio using a given size. This size will come from a GeometryReader’s
GeometryProxy.
You can now calculate the frame of content using the geometry reader proxy size.
raywenderlich.com 525
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
// 1
.frame(
width: calculateSize(proxy.size).width ,
height: calculateSize(proxy.size).height)
// 2
.clipped()
// 3
.frame(maxWidth: .infinity, maxHeight: .infinity)
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 content takes up all of the space available to it. This will center
the card view in the geometry reader.
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.
.resizableView(
transform: bindingTransform(for: element),
viewScale: calculateScale(size))
When ResizableView transforms the size of each element, it now uses the scale
calculated using the proxy size.
Unfortunately your app fails to compile, because proxy’s size isn’t available to
content.
This changes content to be a method instead of a property, so that you can pass the
geometry proxy size.
raywenderlich.com 526
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
content(size: proxy.size)
➤ 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.
raywenderlich.com 527
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Build and run, and now you can add elements that are appropriate to the size of
the device.
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.
raywenderlich.com 528
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
HStack(alignment: .top) {
raywenderlich.com 529
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
HStack(alignment: .bottom) {
Escaping buttons
In Chapter 16, “Adding Assets to Your App”, you used size classes for compact and
regular to determine which launch image to use. Here, you’ll check the size class of
the device in code and use a different 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.
func regularView(
_ imageName: String,
_ text: String
) -> some View {
VStack(spacing: 2) {
Image(systemName: imageName)
raywenderlich.com 530
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Text(text)
}
.frame(minWidth: 60)
.padding(.top, 5)
}
This is the regular size class view that shows both image and text.
This is the compact size class view that shows only the image.
This system environment property holds whether the vertical size class is currently
compact or regular.
This will show the correct view for the correct size class.
raywenderlich.com 531
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
➤ Build and run the app on an iPhone simulator and open a card. When you rotate
the simulator, both text and images show in portrait but only the images show in
landscape.
raywenderlich.com 532
SwiftUI Apprentice Chapter 20: Delightful UX — Layout
Challenge
In Chapter 17, “Interfacing With UIKit”, you implemented drag and drop. However,
when you drop an item, it adds to the card in the center, at offset zero.
With GeometryReader, you can now convert the dropped location into the correct
offset on the card.
To illustrate what a coordinate space is, all card offsets are saved with the origin
being at the center of the card. The origin is location (0, 0). However, info.location
is in screen coordinates, where the origin is at the top left of the screen. So you must
convert from “screen space” to “card space”.
If you need a reminder on how to do drag and drop, take another look at Chapter 17,
“Interfacing With UIKit”.
Try out drag and drop on iPad, and you have an infinite number of Google images to
decorate your card.
raywenderlich.com 533
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. There’s a great Apple WWDC video that goes into
SwiftUI’s layout system in depth at: https://apple.co/39uamSx
raywenderlich.com 534
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.
• This project has an additional group called Supporting Code. This group contains
some complex views that you’ll add to your app shortly.
• Card contains two extra properties. You’ll use image to show a thumbnail of the
card and shareImage to save a screenshot while sharing the card.
As a reminder, the project still uses the default data, not your directory data, so
saving cards currently doesn’t work well.
raywenderlich.com 535
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
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
raywenderlich.com 536
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 CardsView. At the moment, you never set showSplash to false, so
CardsView 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 your splash animation is perfected.
.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.
raywenderlich.com 537
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.
Here you create the view with the letter “C” and the name of a color set up in your
asset catalog.
raywenderlich.com 538
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
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.
raywenderlich.com 539
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
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.
raywenderlich.com 540
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.
raywenderlich.com 541
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Build and run the app again to see the animation in slow motion.
ZStack {
Color("background")
.edgesIgnoringSafeArea(.all)
card(letter: "S", color: "appColor1")
.modifier(SplashAnimation(finalYPosition: 240, delay: 0))
card(letter: "D", color: "appColor2")
.modifier(SplashAnimation(finalYPosition: 120, delay: 0.2))
card(letter: "R", color: "appColor3")
.modifier(SplashAnimation(finalYPosition: 0, delay: 0.4))
card(letter: "A", color: "appColor6")
.modifier(SplashAnimation(finalYPosition: -120, delay: 0.6))
card(letter: "C", color: "appColor7")
.modifier(SplashAnimation(finalYPosition: -240, delay: 0.8))
}
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.
raywenderlich.com 542
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
When you use withAnimation(_:_:), you can specify what sort of Animation you
want to use. You can specify the timing of the animation, the duration and whether it
has a delay.
withAnimation(Animation.default.delay(delay)) {
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.
raywenderlich.com 543
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
This animation lasts for 1.5 seconds and slows gradually at the end of the animation.
➤ 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.interpolatingSpring(
mass: 0.2,
stiffness: 80,
damping: 5,
initialVelocity: 0.0)
.delay(delay)) {
animating = false
}
raywenderlich.com 544
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 these 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
raywenderlich.com 545
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
For implicit animation, you animate any view with an animatable parameter
automatically.
.onAppear {
animating = false
}
.animation(
Animation.interpolatingSpring(
mass: 0.2,
stiffness: 80,
damping: 5,
initialVelocity: 0.0)
.delay(delay))
This adds an implicit animation to the view. Whenever any animatable property
affects the view, you describe the animation to use for this view.
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.
raywenderlich.com 546
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 CardsView. SwiftUI makes this
easy with built-in transition effects, but you can also have complete control about
how the view transitions.
➤ Open AppLoadingView.swift.
.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.
raywenderlich.com 547
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 CardsView():
.transition(.slide)
➤ Build and run to see the slide transition over the specified five second duration.
Slide transition
raywenderlich.com 548
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
raywenderlich.com 549
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
When you tap a card in the scrolling list of cards, the transition is very abrupt.
.transition(.move(edge: .bottom))
This transition will slide the new view in from the bottom edge. If you build and run
the application, no transition animation takes place yet, because you haven’t
configured which property to animate.
withAnimation {
viewState.showAllCards = false
}
withAnimation {
viewState.showAllCards = false
}
➤ In the Views/Single Card Views group, open CardToolbar.swift and locate the
Done button.
raywenderlich.com 550
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
withAnimation {
viewState.showAllCards = true
}
➤ Build and run and choose a card. Although the initial slide transition takes place,
the transition when you press Done does not work well. The card view transitions
behind the list of cards. You can slow the simulator animations to see this better.
.zIndex(1)
zIndex controls the order of views when they are on top of each other. A view with
zIndex of 1 will show in front of a view with zIndex of 0.
The transition moves the new view behind the old view, so to keep the card view in
front, you change SingleCardView’s zIndex to higher than CardsListView’s.
raywenderlich.com 551
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Build and run, and the transition animation from card to list now takes place in
front.
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.
raywenderlich.com 552
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
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
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 CardsView.swift, create a new SwiftUI View file named
ListSelectionView.swift.
raywenderlich.com 553
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 selection with the specified value.
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.
VStack {
ZStack {
...
}
}
raywenderlich.com 554
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
if viewState.showAllCards {
ListSelectionView(selection: $viewState.cardListState)
}
If you’re showing all the cards, show the picker so that you can decide how to view
them.
switch viewState.cardListState {
case .list:
CardsListView()
case .carousel:
Carousel()
}
You show the scrolling list or the carousel depending on the view state.
raywenderlich.com 555
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
At the moment, when you create a card, you’re the only person that 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 view
controller that provides standard services, commonly called a Share view, for sharing
to other apps such as email or your Photos library.
To make it easy to keep track of the sharing state, the starter project added two new
properties.
Currently in SwiftUI, there’s not an easy way to create a screenshot, so you’ll use a
pre-made RenderableView with code in the starter project’s Supporting Code
group.
RenderableView(card: $card)
raywenderlich.com 556
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
You embed the card content view in RenderableView, and this view will take a
screenshot when viewState.shouldScreenshot is true. RenderableView will also
save a thumbnail image of the card to disk when the view disappears. You’ll use this
thumbnail later in this chapter.
➤ Locate:
.modifier(CardToolbar(currentModal: $currentModal))
.cardModals(card: $card, currentModal: $currentModal)
➤ Cut these two lines and paste them at the end of body, so that they are modifiers
on RenderableView rather than on content(size:).
Shortly, you’ll create a Share button in CardModalViews, which you call from
cardModals(card:currentModal:). You should generally create modal views from
as high a level as possible. In this case, if you leave the modifiers on
content(size:), the system will get confused and present a second share sheet on
top of the first share sheet. You’ll also get an uncomfortable message in the debug
console: Presenting view controller from detached view controller…is
discouraged.
➤ In the Model group, open CardModal.swift and add a new case to CardModal:
case shareSheet
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
viewState.shouldScreenshot = true
currentModal = .shareSheet
}) {
Image(systemName: "square.and.arrow.up")
}
}
Here you create a share button on the leading edge of the navigation bar. When the
user taps this button, viewState.shouldScreenshot triggers a screenshot in
RenderableView, which saves the screenshot in card.shareImage. The button also
sets the current modal to be a share sheet.
raywenderlich.com 557
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
case .shareSheet:
if let shareImage = card.shareImage {
ShareSheetView(
activityItems: [shareImage],
applicationActivities: nil)
.onDisappear {
card.shareImage = nil
}
}
Here you pass the screenshot to ShareSheetView and show the modal. This view
controls a UIActivityViewController inside a UIViewControllerRepresentable
and is created for you in ShareSheetView.swift in Supporting Code.
➤ Open SingleCardView.swift and preview the view to see your share button.
raywenderlich.com 558
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
Whenever your app first adds an image to the photo library, you must get permission
from the user and let them know how you will use the library data.
➤ In the Project navigator, choose the top Cards group. Choose the target Cards
and Info along the top.
raywenderlich.com 559
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
➤ Build and run the app again and choose a card. Share the card and save the image
to the photo library again. This time, the app asks for permission to save to photos,
showing the message you entered in the Info key.
raywenderlich.com 560
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.
raywenderlich.com 561
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.
raywenderlich.com 562
SwiftUI Apprentice Chapter 21: Delightful UX — Final Touches
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.
Your challenge is to add this view to the modal view TextPicker 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.
Run the app in Simulator to test the view, as the text element does not update with
the font and color in preview.
raywenderlich.com 563
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.
• You can animate explicitly with withAnimation(_:_:) or implicitly per view with
the animation(_:) modifier.
• 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.
A great example of an app with complex layout and animation is Apple’s Fruta
sample app at https://apple.co/2XE8tNF. This is a fully featured app where “Users
can order smoothies, save favorite drinks, collect rewards, and browse recipes.” Fruta
also has various features, such as widgets, which you’ll learn about in Section 3.
Download the app and see if you can work out how it all fits together.
raywenderlich.com 564
Section III: Your third app:
RWFreeView
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 RWFreeView, an app that allows you to view all the free
video episodes on raywenderlich.com. Along the way, you’ll:
• Learn how to build lists of information and navigate between views using SwiftUI.
raywenderlich.com 565
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 and/or horizontally. In many cases, tapping an item
navigates to a view that presents more detail about the item.
In this section, you’ll create the RWFreeView app. It fetches information about free
raywenderlich.com video episodes and streams them for playback in the app. Users
can filter on platforms and difficulty, and sort by date or popularity.
Getting started
Open the RWFreeView app in the starter folder. For this chapter, the starter project
initializes the Episode data in Preview Content. In Chapter 24, “Downloading
Data”, you’ll fetch this data from api.raywenderlich.com.
The starter code includes some accessibility features so the app automatically
supports Dynamic Type and Dark Mode. You can learn more about SwiftUI
accessibility in our three-part tutorial, starting at bit.ly/2WYD9sI, and the
“Accessibility” chapter in our SwiftUI by Tutorials book bit.ly/32oFTCs.
raywenderlich.com 566
SwiftUI Apprentice Chapter 22: Lists & Navigation
List
The SwiftUI List view 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 episodes, then you’ll add a
header view above the episode items.
You initialize EpisodeStore, which creates a sample episodes array. Then you tell
List to loop over episodes and you provide an id. Like ForEach, List expects each
item to have an identifier, so it knows which item is in which row. The argument
\.name tells List that each item is identified by that property value.
raywenderlich.com 567
SwiftUI Apprentice Chapter 22: Lists & Navigation
You specify the colors that make up the gradient. You can use as many colors as you
like. For this small icon, two colors are enough.
.fill(
LinearGradient(
gradient: gradientColors,
startPoint: .leading,
endPoint: .trailing))
You supply an array of gradient colors. This is a LinearGradient, so you supply start
and end points. These values apply the gradient along the icon’s horizontal axis,
grading from dark on the leading edge to light on the trailing edge.
There are two other types of gradient: RadialGradient grades from the start radius
to the end radius, and AngularGradient grades from the start angle to the end angle.
raywenderlich.com 568
SwiftUI Apprentice Chapter 22: Lists & Navigation
EpisodeView also uses AdaptingStack to switch from HStack to VStack when the
user selects Larger Text in Settings. AdaptingStack comes from code presented in
WWDC 2019 Session 412: Debugging in Xcode 11 (apple.co/3u0kr2z).
.preferredColorScheme(.dark)
raywenderlich.com 569
SwiftUI Apprentice Chapter 22: Lists & Navigation
NavigationView
In Chapter 15, “Structures, Classes & Protocols”, you used NavigationView so you
could add toolbar buttons to CardDetailView. Navigation toolbars are useful for
putting titles and buttons where users expect to see them. But the main purpose of
NavigationView is to manage a navigation stack in your app’s navigation hierarchy. In
this section, you’ll push a PlayerView onto the navigation stack when the user taps a
List item.
NavigationView {
List(store.episodes, id: \.name) { episode in
EpisodeView(episode: episode)
}
.navigationTitle("Videos")
}
raywenderlich.com 570
SwiftUI Apprentice Chapter 22: Lists & Navigation
init() {
// 1
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor(named: "top-bkgd")
appearance.largeTitleTextAttributes =
[.foregroundColor: UIColor.white]
appearance.titleTextAttributes =
[.foregroundColor: UIColor.white]
// 2
UINavigationBar.appearance().tintColor = .white
// 3
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
// 4
UISegmentedControl.appearance()
.selectedSegmentTintColor = UIColor(named: "list-bkgd")
}
raywenderlich.com 571
SwiftUI Apprentice Chapter 22: Lists & Navigation
4. You’ll soon add a header view with a segmented control. Here, you set the color
of the selected segment to match the color you’ll use for the list background.
raywenderlich.com 572
SwiftUI Apprentice Chapter 22: Lists & Navigation
You embed the content view of the List row in a NavigationLink and set the
destination to PlayerView.
raywenderlich.com 573
SwiftUI Apprentice Chapter 22: Lists & Navigation
ContentView is currently the only view in the navigation stack. When you tap an
item, NavigationView pushes PlayerView onto the navigation stack: It’s now the
top view on the stack, so it’s the view that’s visible.
NavigationView gives you a “back” button, labeled the same as the root view’s
navigationTitle. Because you set UINavigationBar.appearance().tintColor to
white, the back button’s arrow and “Videos” label are both white.
AVPlayer takes care of streaming the video from its remote location. The episode’s
title slide appears when the video is ready to play.
.navigationTitle(episode.name)
.navigationBarTitleDisplayMode(.inline)
You use the episode’s name as the title for this view, and you specify a centered
normal size title to override the default large title.
If you want the preview of PlayerView to display the navigation title, embed it in
NavigationView.
NavigationView {
PlayerView(episode: store.episodes[0])
}
raywenderlich.com 574
SwiftUI Apprentice Chapter 22: Lists & Navigation
The Link control opens its destination URL in the associated app. You create the URL
from the Episode computed property linkUrlString, which is just a redirect URL:
raywenderlich.com 575
SwiftUI Apprentice Chapter 22: Lists & Navigation
The associated app is Safari (in a simulator) or your device’s default browser.
➤ Build and run in the simulator or on your device. Tap an item to open the video’s
web page in Safari or your device’s default browser:
Note: If you see a note that a newer version exists, it’s because this older
version has been around longer, so has had more views.
➤ Comment out or delete the Link(...) { ... } code and uncomment the
NavigationLink code.
raywenderlich.com 576
SwiftUI Apprentice Chapter 22: Lists & Navigation
.toolbar {
ToolbarItem {
Button(action: { }) {
Image(systemName: "line.horizontal.3.decrease.circle")
.accessibilityLabel(Text("Shows filter options"))
}
}
}
Just like you did in Chapter 15, “Structures, Classes & Protocols”, you add a Button
as a ToolbarItem to the toolbar. The button uses the default placement on the
trailing side of the toolbar. You’ll soon fill in the button’s action.
The button’s label is an SF Symbol that represents a filter, but the systemName gives
no indication of this purpose. You could write a comment to remind yourself what it
is, but it’s just as easy to supply the information as an accessibility label for
VoiceOver to read out.
➤ Live-preview ContentView: You should see a filter icon in the upper right corner:
raywenderlich.com 577
SwiftUI Apprentice Chapter 22: Lists & Navigation
Now for some action! The starter project already has a FilterOptionsView, and you
know the drill to make the button present it as a modal sheet.
showFilters.toggle()
.sheet(isPresented: $showFilters) {
FilterOptionsView()
}
➤ Live-preview ContentView. Tap the filter button to see the filter options:
Filter options
Selecting a button changes its color to green. You’ll implement these filters in
Chapter 24, “Downloading Data”.
raywenderlich.com 578
SwiftUI Apprentice Chapter 22: Lists & Navigation
Header view
Apps that download and display results from a server often include features like
these:
2. Display any filters the user has set, and let the user remove one or all of them
without showing FilterOptionsView.
A common solution is to add a header above the list. It would be natural to use a
VStack for this:
VStack {
HeaderView(count: store.episodes.count)
List(store.episodes, id: \.name) { episode in
raywenderlich.com 579
SwiftUI Apprentice Chapter 22: Lists & Navigation
.navigationViewStyle(StackNavigationViewStyle())
raywenderlich.com 580
SwiftUI Apprentice Chapter 22: Lists & Navigation
Not great. HeaderView is way too big. You could try to address this issue with
modifiers, but there’s a much easier way.
Remember this List feature? You can display individual views and loop over arrays
within the same List. The trick is to use ForEach to loop over episodes.
List {
HeaderView(count: store.episodes.count)
ForEach(store.episodes, id: \.name) { episode in
NavigationLink(destination: PlayerView(episode: episode)) {
EpisodeView(episode: episode)
}
}
}
List can show any list of views, but inside a List, you need ForEach to iterate over
the episodes array. You’ll soon see that ForEach lets you customize each row too.
raywenderlich.com 581
SwiftUI Apprentice Chapter 22: Lists & Navigation
Your list and navigation are working. There’s just one last feature to add.
Menu("\(Image(systemName: "filemenu.and.cursorarrow"))") {
Button("10 results/page") { }
Button("20 results/page") { }
Button("30 results/page") { }
Button("No change") { }
}
Menu is like the contextMenu you used in Chapter 15, “Structures, Classes &
Protocols”, to delete a card element — in fact, it uses contextMenu under the hood —
but it’s a button. The user doesn’t have to long-press it.
raywenderlich.com 582
SwiftUI Apprentice Chapter 22: Lists & Navigation
raywenderlich.com 583
SwiftUI Apprentice Chapter 22: Lists & Navigation
Custom design
Now it’s time to customize the list to match the Figma design.
Figma design
The Figma design configures each List row as a “card” with rounded corners and a
shadow. There’s a small space between cards, but no list separator. And there are no
disclosure indicators.
raywenderlich.com 584
SwiftUI Apprentice Chapter 22: Lists & Navigation
Creating a card
➤ In EpisodeView.swift, add these modifiers to the top-level HStack to make it look
like a card:
.padding(10)
.background(Color.itemBkgd)
.cornerRadius(15)
.shadow(color: Color.black.opacity(0.1), radius: 10)
You add padding around the text and set the background color to white (Any
Appearance) or a dark gray (Dark Appearance). This will make it, and its shadow,
stand out against the List background, which you’ll soon set to light gray (Any
Appearance) or almost black (Dark Appearance).
List of cards
That’s a good start. The items look like cards, so now you don’t need the separator
lines.
raywenderlich.com 585
SwiftUI Apprentice Chapter 22: Lists & Navigation
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .leading)
.listRowInsets(EdgeInsets())
.padding(.bottom, 8)
.padding([.leading, .trailing], 20)
.background(Color.listBkgd)
Note: This is very similar to the code that extends the background color of
HeaderView to the edges of the List row.
You expand the frame of each row and set all EdgeInsets to zero. Then, you add
padding to separate the cards from each other and move them in from the sides.
Finally, you set the List background to gray.
raywenderlich.com 586
SwiftUI Apprentice Chapter 22: Lists & Navigation
Now that you’ve customized the List row, it no longer changes color when the user
taps it. But the disclosure indicator on the trailing edge of each List row shows it’s
tappable, so it’s not too much of a problem.
ZStack {
NavigationLink(destination: PlayerView(episode: episode)) {
}
EpisodeView(episode: episode)
}
raywenderlich.com 587
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Live-preview ContentView and tap an item to make sure the navigation link still
works.
EpisodeView(episode: episode)
.opacity(0.2)
raywenderlich.com 588
SwiftUI Apprentice Chapter 22: Lists & Navigation
.opacity(0)
raywenderlich.com 589
SwiftUI Apprentice Chapter 22: Lists & Navigation
1. Your future self, or someone who takes over your code, might wonder why there’s
nothing in the NavigationLink closure and might move EpisodeView(episode:
episode) back inside.
➤ To solve the first issue, add this view inside the NavigationLink closure:
EmptyView()
You explicitly display an empty view, so you know you did it on purpose.
➤ For the second issue, add this modifier to NavigationLink( ... ) { ... }:
.buttonStyle(PlainButtonStyle())
You apply PlainButtonStyle(), which shows a tiny visual effect when you tap a
List row.
➤ Live-preview your app and try it out to make sure everything still works.
raywenderlich.com 590
SwiftUI Apprentice Chapter 22: Lists & Navigation
.navigationViewStyle(StackNavigationViewStyle())
PlayerView(episode: store.episodes[0])
raywenderlich.com 591
SwiftUI Apprentice Chapter 22: Lists & Navigation
But for RWFreeView, you’ll prevent your app from using this default style.
.navigationViewStyle(StackNavigationViewStyle())
You tell the app to always use stack navigation on iPads and Max iPhones. This is the
default navigation style for iPhones in portrait orientation and for non-Max iPhones
in landscape orientation.
raywenderlich.com 592
SwiftUI Apprentice Chapter 22: Lists & Navigation
➤ Build and run again to see the List, just like on an iPhone. Then rotate to
landscape:
@Environment(\.verticalSizeClass) var
verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var
horizontalSizeClass: UserInterfaceSizeClass?
var isIPad: Bool {
horizontalSizeClass == .regular &&
verticalSizeClass == .regular
}
You check the device’s vertical and horizontal size classes. If both are regular, the
device is an iPad.
raywenderlich.com 593
SwiftUI Apprentice Chapter 22: Lists & Navigation
You set width to 644 if the device is an iPad. Otherwise, you let the view set its own
width.
➤ Build and run again on an iPad and check portrait and landscape orientations:
raywenderlich.com 594
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. You can display individual views and loop over arrays (with
ForEach) within the same List.
• A NavigationView can contain alternative root views. You modify each with its
own navigationTitle and toolbars.
• It’s easy to open a web link in the device’s default browser using Link.
raywenderlich.com 595
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 chapters, where
you’ll implement RWFreeView’s server downloads.
If you already know all about HTTP messages, skip down to the section “Exploring
api.raywenderlich.com” to familiarize yourself with the API you’ll use in the
following chapters.
raywenderlich.com 596
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Apps like Safari and RWFreeView 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.
raywenderlich.com 597
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
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.
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:
raywenderlich.com 598
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
There are many many HTTP response status codes. You’ll find a fun representation
of them at http.cat. For example:
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.
Note: The 1998 HTCPCP (bit.ly/3olM42q) April Fools’ joke was inspired by the
Trojan Room coffee pot (bit.ly/3pkCDSb), the subject of the world’s first web
cam. It was set up in 1991, long before the Internet of Things (IoT).
raywenderlich.com 599
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
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.
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:
raywenderlich.com 600
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
In the next chapter, you’ll set up RWFreeView to communicate with the REST API
api.raywenderlich.com. In this chapter, you’ll explore this API’s documentation at
raywenderlich.docs.apiary.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.raywenderlich.com/library
raywenderlich.com 601
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
This is the endpoint of the raywenderlich.com library. 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 (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”.
raywenderlich.com 602
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
There are lots more request examples at GitHub’s Getting started with the REST API
(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.
curl -i https://api.github.com/zen
HTTP/2 200
server: GitHub.com
date: Tue, 27 Apr 2021 01:40:56 GMT
content-type: text/plain;charset=utf-8
...
x-ratelimit-limit: 60
x-ratelimit-remaining: 58
x-ratelimit-reset: 1619491224
x-ratelimit-used: 2
accept-ranges: bytes
x-github-request-id: EF14:706C:A3D763:B0E977:60876BA7
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
raywenderlich.com 603
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Lines that start with < are response headers, and lines that start with * are additional
information provided by cURL.
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://api.raywenderlich.com/api/contents
This is a request to the API you’ll use for RWFreeView. The response is pretty mind-
numbing:
raywenderlich.com 604
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 the first value "data" is an array of dictionaries. You can use a
tool like codebeautify.org/jsonviewer to format this so it’s easier to read.
Exploring api.raywenderlich.com
Apps like RESTed let you create HTTP requests by filling in fields and selecting from
drop-down menus. You can pretty-print responses and use syntax highlighting.
➤ In a browser, open apple.co/3cb5CnP, click View in Mac App Store and install the
app.
raywenderlich.com 605
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Requesting contents
➤ Open RESTed and replace http://localhost:3000/ with this URL:
https://api.raywenderlich.com/api/contents
You set the resource endpoint. How do you know what to ask for? Look through the
table of contents sidebar at raywenderlich.docs.apiary.io: In the References
section, /contents sounds like the most general, highest level of data.
How do you know what to write in front of /contents? Select /contents then scroll
down to find this gray field with 200 OK and a disclosure indicator:
raywenderlich.com 606
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ Back in RESTed, leave the GET method selected. Open preferences and check the
boxes for Pretty-print response and Apply syntax highlighting:
raywenderlich.com 607
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
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.
Each dictionary in the array contains the attributes for one content item. bit.ly/
3sMFjdy describes these attributes. In the next chapter, you’ll use JSONDecoder to
extract the attributes you want and store them in the Episode structure so you can
display them in RWFreeView.
raywenderlich.com 608
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Media URLs
➤ Notice that card_artwork_url is a URL. Go ahead and copy-paste one of these
URLs in RESTed and send the request.
You’ve already used the uri attribute to open an episode in a browser from
RWFreeView.
You’ll use the video_identifier value to fetch the video url to play in PlayerView.
For example, the video_identifier of “SwiftUI vs. UIKit” is 3021.
https://api.raywenderlich.com/api/videos/3021/stream
"url": "https://player.vimeo.com/external/357115704.m3u8?
s=19d68c614817e0266d6749271e5432675a45c559&oauth2_token_id=89771
1146"
raywenderlich.com 609
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ This is the link you’ll pass to PlayerView. RESTed isn’t able to play it, so paste it
into Safari to see it load the video.
Sorting
The API documentation for /contents says you can sort on either popularity or
released_at and tells you which attributes you can filter on.
https://api.raywenderlich.com/api/contents
➤ Scan the released_at values for the first few array items, and you’ll see the
default sort order is reverse chronological order. The first item was released most
recently, and later items were released earlier.
So change the request to show you the most popular items first.
raywenderlich.com 610
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ In the parameter section, click + to add Parameter Name sort with Parameter
Value -popularity:
raywenderlich.com 611
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Filtering
➤ Scroll down to the bottom of the response body to find the meta key with value
total_result_count.
In the API documentation, the You can filter on: list includes domain_ids, which
sounds like a possibility for a solution.
➤ In the sidebar of the API web page, click /domains then click its 200 OK button:
raywenderlich.com 612
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ Click Select Language… (click the text; the disclosure arrow on this button
doesn’t do anything):
➤ Select Swift:
raywenderlich.com 613
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
➤ Close the /domains sidebar and go back to the /contents documentation to see
how to create the filter endpoint.
raywenderlich.com 614
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
• Name: filter[content_types][]
• Value: episode
And
• Name: filter[subscription_types][]
• Value: free
raywenderlich.com 615
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
With these parameters set up in RESTed, you can easily turn any of them off by
unchecking its checkbox. Or, you can change a parameter value to retrieve a different
domain or content type.
Apiary console
The sidebar has a feature that looks wonderful but doesn’t quite work.
➤ Open the /contents sidebar and click Try console to get a form where you can add
the same parameters:
raywenderlich.com 616
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
But when you Send request, the response is We encountered an error. Please try later.
URL-encoding
You probably noticed some strange symbols when RESTed or Apiary combined the
parameters into a query string:
https://api.raywenderlich.com/api/contents?
filter%5Bsubscription_types%5D%5B%5D=free&
filter%5Bdomain_ids%5D%5B%5D=1&
filter%5Bcontent_types%5D%5B%5D=episode&
sort=-popularity
Note: I indented these lines for readability. This string won’t work as a URL.
RESTed and Apiary URL-encoded the square brackets you used in the parameter
names to %5B and %5D. URLs sent over the internet can contain only letters, digits
and these punctuation marks: -, _, . and ~.
raywenderlich.com 617
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
When / and ? are delimiters in the URL, they don’t get encoded.
Challenge
Challenge: Changing the page size
➤ Scroll down to the bottom of the response body to find the links item.
Paging is controlled by two query terms that you didn’t set. For example, the next
URL includes these parameters:
page%5Bnumber%5D=2&page%5Bsize%5D=20
One of the features you’ll implement in the next chapter is letting the user change
the page size. You’re probably pretty sure you’ll need to set the page[size]
parameter.
raywenderlich.com 618
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Your challenge is to send a RESTed request that changes the page size to 5 (it’ll
be easier to count a small number of response items).
RWFreeView only needs to GET resources from the server and, because it gets only
free items, 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. Consider using Sign In
with Apple. Follow our tutorial Sign in with Apple Using SwiftUI bit.ly/3iHqNix.
To try out a POST request, you’ll use RESTed to send something like this GitHub
curl example:
curl -i -H \
"Authorization: token
raywenderlich.com 619
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
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.
➤ If you haven’t saved a plain-text copy of your GitHub personal access token,
generate a new one at bit.ly/2Y71Ofh.
➤ In RESTed, add Header Field Authorization with Header Value token. Paste
your personal access token after “token ” to complete this value:
raywenderlich.com 620
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Note: If you don’t see both the parameter fields and the HTTP body, make sure
both buttons are active.
Instead of appending to the endpoint, POST request parameters appear in the body
field.
raywenderlich.com 621
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
raywenderlich.com 622
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
raywenderlich.com 623
SwiftUI Apprentice Chapter 23: Just Enough Web Stuff
Key points
• Client apps send HTTP requests to servers, which send back responses.
• An HTTP response 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
RESTed to create and send requests and inspect responses.
raywenderlich.com 624
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. Your RWFreeView 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 RWFreeView needs to work with
the raywenderlich.com API and its JSON structure, which is deeply nested. So in this
chapter, you’ll learn two ways to work with nested JSON.
Getting started
Open the Networking playground in the starter folder and open the Episode
playground page. If the editor window is blank, show the Project navigator
(Command-1) and select Episode there.
raywenderlich.com 625
SwiftUI Apprentice Chapter 24: Downloading Data
Playgrounds are useful for exploring and working out code before moving it into your
app. You can quickly inspect values produced by methods and operations, without
needing to build a user interface or search through a lot of debug console messages.
Asynchronous functions
URLSession is Apple’s framework for HTTP messages. Most of its 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. When you call the method, you supply a completion
handler. This runs when the network task completes to process the response from the
server.
Note: URLSession and the broader topic of concurrency have their own video
course at bit.ly/3x6Z8hN, and there’s also a book, Concurrency by Tutorials, at
bit.ly/3n0BSgF.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
https://api.raywenderlich.com/api/contents?
filter%5Bsubscription_types%5D%5B%5D=free&filter%5Bdomain_ids%5D
%5B%5D=1&filter%5Bcontent_types%5D%5B%5D=episode&sort=-
popularity
raywenderlich.com 626
SwiftUI Apprentice Chapter 24: Downloading Data
This URL lists query parameter names and values after the ? separator.
In this playground, you’ll create this REST request for free iOS & Swift episodes,
sorted by popularity. Your approach will be flexible, so you can easily change the
query parameter values.
URLComponents
The URLComponents structure 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. The url itself gives you access to URL components like
lastPathComponent.
You set the URL string for the API’s base endpoint and add the contents endpoint to
create a URLComponents instance. Then, you create an array of URLQueryItem values.
The URLQueryItem parameters are the parameter names and values you used in the
previous chapter to construct your RESTed request.
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.
raywenderlich.com 627
SwiftUI Apprentice Chapter 24: Downloading Data
➤ 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 and resize the display window
that appears below the code line:
"https://api.raywenderlich.com/api/contents/?
filter%5Bsubscription_types%5D%5B%5D=free&filter%5Bcontent_types
%5D%5B%5D=episode"
Thanks to urlComponents, your queries are safely URL-encoded and appended to the
base URL.
➤ 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:
raywenderlich.com 628
SwiftUI Apprentice Chapter 24: Downloading Data
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 etc.
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.
The name and value arguments of URLQueryItem look like dictionary key and value
items, so it’s easy to create a dictionary of parameter names and values, then
transform this dictionary into a queryItems array. It’s especially easy when Alfian
Losari has already done it in bit.ly/3pRtT6t. :] It’s in Networking/Sources/
URLComponentsExtension.swift in this playground.
var baseParams = [
"filter[subscription_types][]": "free",
"filter[content_types][]": "episode",
"sort": "-popularity",
"page[size]": "20",
"filter[q]": ""
]
urlComponents.setQueryItems(with: baseParams)
You create a dictionary whose keys are query parameter names. The first two are
fixed: You always want to download free episodes.
raywenderlich.com 629
SwiftUI Apprentice Chapter 24: Downloading Data
You include sort, page size and q (search term) because these are single-value
options. They have default values, and this dictionary lets you easily change their
values. For example:
urlComponents.queryItems! +=
[URLQueryItem(name: "filter[domain_ids][]", value: "1")]
You don’t include this query parameter in baseParams because you can add more
than one domain_id query item to a RESTED request URL.
URLSessionDataTask
➤ Add this code below the absoluteString line:
// 2
raywenderlich.com 630
SwiftUI Apprentice Chapter 24: Downloading Data
URLSession.shared
.dataTask(with: contentsURL) { data, response, error in
defer { PlaygroundPage.current.finishExecution() } // 3
if let data = data,
let response = response as? HTTPURLResponse { // 4
print(response.statusCode)
// Decode data and display it
}
// 5
print(
"Contents fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
.resume() // 6
2. You create a dataTask with contentsURL. For simple requests like this, the
shared session with default configuration works fine. You can create a session
with a custom configuration. For example, here’s how you create a session that
waits 300 seconds for a network connection:
3. The defer statement stops playground execution when the dataTask handler
completes. This is convenient when the code is the last thing executed in a
playground page.
4. You supply a completion handler. When the task completes, this handler receives
three arguments. You usually name them data, response and error. All three
are optionals, so you must unwrap them. Completion handlers usually check
response.statusCode then decode data.
6. URLSession tasks are created in a suspended state, so you must call resume()
method to start them. This step is easy to forget, even for experienced iOS
developers. ;]
raywenderlich.com 631
SwiftUI Apprentice Chapter 24: Downloading Data
What about that comment Decode data and display it after you print the status
code? Most of the rest of this chapter helps you do that. If you run this code now,
you’ll get “Unknown error” because you haven’t decoded data yet.
One of the contents item attributes is video_identifier. It’s an integer like 3021.
You’ll use it to fetch the URL string of the item’s video.
You create the query URL and the dataTask code to send it, then print the response
status code. Then, you need to Decode response and display it.
The JSON response for this query is simpler than for the contents query, so you’ll
decode this first.
raywenderlich.com 632
SwiftUI Apprentice Chapter 24: Downloading Data
Decoding JSON
If there’s a good match between your data model and a JSON value, the default
init(from:) of JSONDecoder is poetry in motion, letting you decode complex
structures of arrays and dictionaries in a single line of code. Unfortunately,
api.raywenderlich.com sends a deeply-nested JSON structure that you probably
won’t want to replicate in your app. So, it’s time to learn more about CodingKey
enumerations and custom init(from:) methods.
Note: Dive deeper into JSON with our tutorial Encoding and Decoding in Swift
bit.ly/3bqsrBY.
JSON values sent by real-world APIs rarely match the way you want to name or
structure your app’s data.
If the JSON structure matches your app’s data model, but the JSON names use
snake_case while your property names use camelCase, you just tell the decoder to
do the translation:
This takes care of translating JSON names like released_at and video_identifier
to property names releasedAt and videoIdentifier.
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, Episode has a description
property, but the matching JSON item’s name is description_plain_text:
raywenderlich.com 633
SwiftUI Apprentice Chapter 24: Downloading Data
Even the videos request has this problem. All you want is the "url" value, but it’s
buried in a nested JSON structure.
➤ Send this request with RESTed, or use the Try console of the /videos/
{video_id}/stream endpoint at raywenderlich.docs.apiary.io.
https://api.raywenderlich.com/api/videos/3021/stream
{
"data": {
"id": "32574",
"type": "attachments",
"attributes": {
"url": "https://player.vimeo.com/external/357115704...",
"kind": "stream"
}
}
}
The JSON value contains a dictionary named "data". The value of its "attributes"
key is another dictionary, and the value of the "url" key is the URL string you want
to store in your Episode instance.
raywenderlich.com 634
SwiftUI Apprentice Chapter 24: Downloading Data
You’ll do it the first way to see how nifty automatic JSON decoding can be. Then
you’ll do it the second way, because that’s how you’ll do it in your app. In exchange
for more decoding work, you’ll get sensible data structures that are easier and more
natural to work with.
You create separate structures with data, attributes and url properties. The
relationships between these structures match the nesting of the JSON value’s
"data", "attributes" and "url".
➤ In the completion handler for the queryURL dataTask, replace the Decode
response and display it comment with this code:
1. You don’t need to configure JSONDecoder for this task, so you just create one
inline.
2. You decode the top level ResponseData, and this gives you access to its
data.attributes.url property.
3. You run the print statement on the main queue. This isn’t really necessary in a
playground, but is essential in an app. You usually do something in the dataTask
handler to update the user interface, and all UI updates must run on the main
queue.
raywenderlich.com 635
SwiftUI Apprentice Chapter 24: Downloading Data
Note: You must run the whole playground if you added the ResponseData and
other structures below the URLSession code. They’re not “visible” if you click
the run button on the resume() line.
➤ Option-click url:
raywenderlich.com 636
SwiftUI Apprentice Chapter 24: Downloading Data
The downside of this approach comes when you need to use url in your app. You
don’t really want to have to create a ResponseData instance for every video. It’s
much more natural to include it as one of the Episode properties.
{
"data": {
"id": "32574",
"type": "attachments",
"attributes": {
"url": "https://player.vimeo.com/external/357115704...",
"kind": "stream"
}
}
}
raywenderlich.com 637
SwiftUI Apprentice Chapter 24: Downloading Data
struct VideoURLString {
// data: attributes: url
var urlString: String
This time, you mirror the JSON hierarchy in the CodingKey enumerations you create
for the top-level and "data" containers. The cases are the JSON keys you care about:
"data" in the top-level container and "attributes" in the "data" container.
Then, you create a structure to hold the "attributes" item you care about: url.
To flatten the JSON structure to fit your data model, you must write your own
initializer.
raywenderlich.com 638
SwiftUI Apprentice Chapter 24: Downloading Data
Note: You always put this decoding initializer in an extension. If you put it in
the main VideoURLString structure, you lose the default initializer that Swift
creates for structures.
You drill down into the JSON value, using the coding keys to create containers and
access their contents.
1. First, you get the top-level container. The case data in CodingKeys matches
something in this top-level container.
2. Next, you grab the nested container that matches case data in CodingKeys. The
case attributes in DataKeys matches something in dataContainer.
4. Finally, you reach the "url" value in the JSON hierarchy! You assign it to the
top-level urlString property of this VideoURLString instance.
This is more work than just mirroring the JSON hierarchy in your data model
structures, but it has two advantages:
1. Your data model structures are flatter and easier to understand. They match your
mental model of your app’s data.
2. If the JSON hierarchy changes, you only need to modify CodingKey enumerations
and your custom init(from:). You don’t need to refactor your data model
structures.
raywenderlich.com 639
SwiftUI Apprentice Chapter 24: Downloading Data
{
"data": [
{
"id": "5117655",
"attributes": {
"uri": "rw://betamax/videos/3021",
"name": "SwiftUI vs. UIKit",
"released_at": "2019-09-03T13:00:00.000Z",
"difficulty": "beginner",
"description_plain_text": "Learn about...\n",
"video_identifier": 3021,
...
},
"relationships": {
"domains": {
"data": [
{
"id": "1",
"type": "domains"
}
]
},
...
}
...
raywenderlich.com 640
SwiftUI Apprentice Chapter 24: Downloading Data
},
...
}
The top-level container is a dictionary. One of its keys is "data", which is an array of
dictionaries. Most of the data you need to store for each Episode instance is in the
"attributes" value.
And the episode’s domains (platforms) are listed by "id" in the "relationships"
value.
raywenderlich.com 641
SwiftUI Apprentice Chapter 24: Downloading Data
Note: This date format is an international standard defined by ISO 8601 bit.ly/
3aJJSwR. You can find date format patterns and a table of date field symbols at
bit.ly/3oZTuZu.
This is all you need to do to enable the decoder to convert the "released_at" string
into a Date value. Then later, you’ll use episodeDateFormatter to display the
month and year of this date in EpisodeView.
raywenderlich.com 642
SwiftUI Apprentice Chapter 24: Downloading Data
➤ In the completion handler for dataTask, replace the comment Decode data and
display it with this code:
You check the Date value created by decoder by converting it to the String you’ll
display in EpisodeView.
➤ In the Episode playground page, replace the attributes property of the Episode
structure with all the properties you need:
raywenderlich.com 643
SwiftUI Apprentice Chapter 24: Downloading Data
// 2
var domain = "" // relationships: domains: data: id
1. Declare the properties you’re going to map from the JSON response. Fetched
items that aren’t episodes don’t have "difficulty" values, so this is optional.
2. Most of these properties match items in the "attributes" container, but the
domain "id" value is nested deep in the "relationships" dictionary.
4. In case you want to use Link to open a browser, you compute linkURLString
from uri.
➤ First, delete Attributes. You’re going to flatten it into the top level of Episode.
raywenderlich.com 644
SwiftUI Apprentice Chapter 24: Downloading Data
You create CodingKey enumerations DataKeys, AttrsKeys and RelKeys for the
"data", "attributes" and "relationships" containers and create a structure to
hold the "domains" item you care about: data.
Note: Swift enumeration case identifiers can include the underscore character,
but raywenderlich.com code uses SwiftLint github.com/realm/SwiftLint,
which allows only letters and digits. And the convertFromSnakeCase key
decoding strategy only works during automatic JSON decoding.
raywenderlich.com 645
SwiftUI Apprentice Chapter 24: Downloading Data
To flatten the JSON structure into Episode, you must write your own initializer in an
extension.
➤ Start by decoding the JSON items. Add this extension in the Episode playground
page:
extension Episode {
init(from decoder: Decoder) throws {
let container = try decoder.container( // 1
keyedBy: DataKeys.self)
let id = try container.decode(String.self, forKey: .id)
2. You grab the nested container that matches case attributes in DataKeys, then
decode the six values you want to store in Episode.
3. For the releasedAt coding key, you decode the String, then convert it to a Date
with your millisecond-handling iso8601 formatter.
raywenderlich.com 646
SwiftUI Apprentice Chapter 24: Downloading Data
4. Similarly, you get the "relationships" container and decode the "domains"
item.
5. Finally, you get a domainId value and convert it to a platform name. The
"domains" item is an array because an episode could be relevant to more than
one domain. You take the first array item. This is an optional because an array
can be empty. The value of the "id" key is also an optional, in case there’s no
such key. If you can unwrap these optionals, you look up the matching platform
name in domainDictionary and assign it to the domain property.
You’ll use these decoded values to initialize each Episode. For most properties,
you’ll just assign the decoded value to the property. But you’ll convert the
releaseDate Date to a String value, and you need a way to use videoIdentifier
to fetch a video URL string.
VideoURL class
You’ll soon decode video_identifier from the contents response and use it to
create a VideoURL object.
➤ In the VideoURL playground page, remove the code below let videoId = 3021,
but not the structures and extension, if you added them there.
class VideoURL {
var urlString = ""
init(videoId: Int) {
let baseURLString =
"https://api.raywenderlich.com/api/videos/"
let queryURLString =
baseURLString + String(videoId) + "/stream"
guard let queryURL = URL(string: queryURLString) // 1
else { return }
URLSession.shared
.dataTask(with: queryURL) { data, response, error in
defer { PlaygroundPage.current.finishExecution() }
if let data = data,
let response = response as? HTTPURLResponse {
print("\(videoId) \(response.statusCode)")
if let decodedResponse = try? JSONDecoder().decode(
VideoURLString.self, from: data) {
DispatchQueue.main.async {
self.urlString = decodedResponse.urlString // 2
print(self.urlString)
}
return
raywenderlich.com 647
SwiftUI Apprentice Chapter 24: Downloading Data
}
}
print(
"Videos fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
.resume()
}
}
This moves the code you had before inside the VideoURL class with a couple of small
changes:
1. Now that you’re in a method, you can exit if something goes wrong, so you create
queryURL safely in a guard statement instead of force-unwrapping the URL.
2. Assign the decoded JSON to the urlString property of this VideoURL object,
then print that value.
VideoURL(videoId: 3021)
➤ Run the playground and check it’s still fetching the video URL:
VideoURL(videoId: 3021)
raywenderlich.com 648
SwiftUI Apprentice Chapter 24: Downloading Data
➤ Copy VideoURL, the structures and extension to the Episode playground page.
Comment out or delete the defer statement in init(videoId:):
//defer { PlaygroundPage.current.finishExecution() }
You don’t want to stop playground execution after decoding the first video URL!
➤ Add the following code to the end of the init(from:) method in the Episode
extension:
self.id = id
self.uri = uri
self.name = name
self.released = DateFormatter.episodeDateFormatter.string( //
1
from: releaseDate)
self.difficulty = difficulty
self.description = description
if let videoId = videoIdentifier { // 2
self.videoURL = VideoURL(videoId: videoId)
}
When you write your own decoder initializer, you lose JSONDecoder’s automatic
operation. So you must initialize every property, even if you just assign a decoded
value to a property.
Two properties require more work than just assigning the local value to the property:
➤ Now scroll to the URLSession code, where the compiler is complaining ‘Episode’
has no member ‘attributes’. Replace all the code in the dataTask handler’s main
queue closure with the following:
print(decodedResponse.episodes[0].released)
print(decodedResponse.episodes[0].domain)
raywenderlich.com 649
SwiftUI Apprentice Chapter 24: Downloading Data
raywenderlich.com 650
SwiftUI Apprentice Chapter 24: Downloading Data
Key points
• Playgrounds are useful for working out code. You can quickly inspect values
produced by methods and operations.
• URLComponents query items help you create URL-encoded URLs for REST requests.
• Use URLSession dataTask to send an HTTP request and process the HTTP
response.
• Decode nested JSON values either by mirroring the JSON structure in your app’s
data model, or by flattening the JSON structure into your data model.
raywenderlich.com 651
25 Chapter 25: Implementing
Filter Options
By Audrey Tam
So far in this part, you’ve created a quick prototype, implemented the Figma design,
explored the raywenderlich.com REST API and worked out the code to send a REST
request and decode its response. In this chapter, you’ll copy and adapt the
playground code into your app. Then, you’ll build on this to implement all the filters
and options that let your users customize which episodes they fetch. Your final result
will be a fully-functioning app you can use to sample all our video courses.
Getting started
Open the RWFreeView starter project. It contains code you’ll use to keep the filter
buttons synchronized between FilterOptionsView and HeaderView. And
EpisodeStore is now an EnvironmentObject, used by ContentView,
FilterOptionsView, HeaderView and SearchField.
raywenderlich.com 652
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Swift playground
Open the Networking playground in the starter folder or continue with your
playground from the previous chapter. You’ll adapt code from the Episode
playground into a fetchContents() method in EpisodeStore.swift and replace the
old prototype Episode with the new Episode structure and extension. And, you’ll
create a new Swift file — VideoURL.swift — for the VideoURL class and make it
conform to ObservableObject.
Some of the new Episode properties are slightly different, so you’ll fix a few errors
that appear in EpisodeView.swift and PlayerView.swift.
func fetchContents() {}
init() {
fetchContents()
}
raywenderlich.com 653
SwiftUI Apprentice Chapter 25: Implementing Filter Options
// 1
let baseURLString = "https://api.raywenderlich.com/api/contents"
var baseParams = [
"filter[subscription_types][]": "free",
"filter[content_types][]": "episode",
"sort": "-popularity",
"page[size]": "20",
"filter[q]": ""
]
// 2
func fetchContents() {
guard var urlComponents = URLComponents(string: baseURLString)
else { return }
urlComponents.setQueryItems(with: baseParams)
guard let contentsURL = urlComponents.url else { return }
}
➤ Now copy the URLSession code into fetchContents() and modify what happens
on the main queue:
URLSession.shared
.dataTask(with: contentsURL) { data, response, error in
// defer { PlaygroundPage.current.finishExecution() } // 1
if let data = data,
let response = response as? HTTPURLResponse {
print(response.statusCode)
if let decodedResponse = try? JSONDecoder().decode( // 2
EpisodeStore.self, from: data) {
DispatchQueue.main.async {
self.episodes = decodedResponse.episodes // 3
}
return
}
}
print(
raywenderlich.com 654
SwiftUI Apprentice Chapter 25: Implementing Filter Options
2. You create a default JSONDecoder. You don’t need to configure it because you’ll
be providing a custom init(from:).
// 1
enum CodingKeys: String, CodingKey {
case episodes = "data" // array of dictionary
}
// 2
init(from decoder: Decoder) throws {
let container = try decoder.container(
keyedBy: CodingKeys.self)
episodes = try container.decode(
[Episode].self, forKey: .episodes)
}
When you add the init(from:) initializer, an Xcode error might tell you to mark it
as required. This keyword indicates that every subclass of EpisodeStore must
implement this initializer. You won’t be subclassing EpisodeStore, so you apply the
final keyword to the class to make this fact explicit. This gets rid of the error
message.
raywenderlich.com 655
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Having added CodingKeys and init(from:), you can now declare that
EpisodeStore conforms to Decodable.
Instead of the simple class and var in the playground, VideoURL in your app is an
ObservableObject. It publishes urlString because there’s a network delay between
initializing a VideoURL object and assigning a non-empty value to urlString.
➤ Now copy the init(videoId:) method from the playground into VideoURL and
modify the dataTask completion handler:
init(videoId: Int) {
let baseURLString =
"https://api.raywenderlich.com/api/videos/"
let queryURLString =
baseURLString + String(videoId) + "/stream"
guard let queryURL = URL(string: queryURLString)
else { return }
URLSession.shared
.dataTask(with: queryURL) { data, response, error in
if let data = data,
let response = response as? HTTPURLResponse {
// 1
if response.statusCode != 200 {
print("\(videoId) \(response.statusCode)")
raywenderlich.com 656
SwiftUI Apprentice Chapter 25: Implementing Filter Options
return
}
if let decodedResponse = try? JSONDecoder().decode(
VideoURLString.self, from: data) {
// 2
self.urlString = decodedResponse.urlString
}
} else {
print(
"Videos fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
}
.resume()
}
1. To reduce the number of debug messages, you only print the status code if it’s not
200 OK. The status code is 404 Not found if an item doesn’t have a video URL.
As there’s no data to decode, you exit, leaving urlString with the value "".
Text(String(episode.difficulty).capitalized)
➤ In EpisodeView.swift, click the red error button to see Xcode’s suggestions and
select the first fix “Coalesce using ‘??’…”.
Text(String(episode.difficulty ?? "").capitalized)
raywenderlich.com 657
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ The videoURLString in the simple Episode structure of Chapter 22, “Lists &
Navigation”, is now videoURL?.urlString. It’s an optional, so replace this line
using the same coalescing trick:
➤ Build and run. If it runs very slowly in a simulator, install it on an iOS device.
Then, scroll down and examine the Introduction episodes:
If you send the same request in RESTed, you’ll see the same number of Introduction
episodes, but they’re all different. So how can you see what’s happening in your app?
raywenderlich.com 658
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Breakpoints to the rescue! In Chapter 9, “Saving History Data”, you learned how to
insert a breakpoint on a line of code where you want execution to pause while you
inspect the current values. This time, you’ll just print out values every time that line
executes without pausing the app.
In this case, it’s useful to see the videoIdentifier and description values of each
decoded Introduction episode.
Breakpoint window
➤ In the breakpoint window, click Add Action and set the Action to Log Message.
In the Condition field, type name == “Introduction” and, in the message field, type
@videoIdentifier@ @description@. Finally, check the box to Automatically
continue after evaluating actions.
raywenderlich.com 659
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Breakpoint messages
The debug console displays videoIdentifier and description for several episodes,
and they’re all different. So, there’s nothing wrong with the server response or with
your app’s decoding.
Notice the first Introduction episode is the one that gets repeated in the running
app.
Your app decodes several different Introduction episodes into your episodes array,
but displays only the first one, again and again. This is the work of the loop in
ContentView.swift, so this is the next place to look for the problem.
Oh! id: \.name means every episode with the same name is the same episode, so the
first Introduction episode is the episode.
ForEach(store.episodes) { episode in
Episode now has an id property, which ForEach and List use by default, unless you
specify some other value for the id argument. This id property is different for each
episode, even if they have the same name.
raywenderlich.com 660
SwiftUI Apprentice Chapter 25: Implementing Filter Options
RWFreeView running!
Much better!
raywenderlich.com 661
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Challenge
Challenge: Displaying parentName
➤ Take another look at those Introduction episodes. Even if a user reads the
description, it doesn’t always tell them enough to decide whether to play the video.
Sometimes, there are several Conclusion episodes, too. Can you add more
information to these episodes?
In the raywenderlich.com API, the attributes key parent_name tells you the course
an episode is in. You can improve your users’ experience by adding a parentName
property to Episode, then display it when name is "Introduction" or
"Conclusion".
Try this exercise on your own before reading my list of steps below or looking at the
final project.
➤ Still in init(from:), set the property with the decoded value self.parentName
= parentName.
if episode.name == "Introduction" ||
episode.name == "Conclusion" {
Text(episode.parentName ?? "")
.font(.subheadline)
.foregroundColor(Color(UIColor.label))
.padding(.top, -5.0)
}
raywenderlich.com 662
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ Build and run, then scroll down to see the Introduction episodes now display
more information:
Indicating activity
The list is blank while the dataTask is running. Users expect to see an activity
indicator until the list appears.
loading = true
➤ And in the dataTask handler, add this at the beginning, where you had the defer
closure to finish playground execution:
defer {
DispatchQueue.main.async {
raywenderlich.com 663
SwiftUI Apprentice Chapter 25: Implementing Filter Options
self.loading = false
}
}
You set loading to true before starting dataTask and set it to false after receiving
and decoding the network response. Using the defer block ensures you hide the
activity indicator in both success and failure cases.
Now you’ve set the value of loading in all the necessary places, you’ll use its value
to show or hide ActivityIndicator().
if store.loading { ActivityIndicator() }
raywenderlich.com 664
SwiftUI Apprentice Chapter 25: Implementing Filter Options
It turns out these placeholders shouldn’t be included in results, and they’ve been
deleted now. But you should still check for a video URL. You might, for example,
decide to allow non-episode content types, which don’t have videos (but forget to
provide an appropriate viewer).
In PlayerView.swift, you’ll display a “No video” message when there’s no video URL.
} else {
PlaceholderView()
}
"filter[content_types][]": "article"
raywenderlich.com 665
SwiftUI Apprentice Chapter 25: Implementing Filter Options
"filter[content_types][]": "episode"
OK, your app’s basic download function is working well, delivering a great user
experience. Now, it’s time to implement all those options and filters, so your users
can customize their query results.
raywenderlich.com 666
SwiftUI Apprentice Chapter 25: Implementing Filter Options
In this section, you’ll implement the last three actions, which correspond to the last
three keys in the baseParams dictionary in EpisodeStore:
var baseParams = [
"filter[subscription_types][]": "free",
"filter[content_types][]": "episode",
"sort": "-popularity",
"page[size]": "20",
"filter[q]": ""
]
For each of these three user actions, you’ll write code to change the appropriate
value and send a new request.
You’ll pass the user’s search term to the baseParams dictionary in EpisodeStore.
TextField(
"",
text: $queryTerm,
onEditingChanged: { _ in },
onCommit: {
store.baseParams["filter[q]"] = queryTerm
store.fetchContents()
}
)
When the user taps the keyboard’s return key, the onCommit code runs. You set the
value of the query filter to the user’s search term, then call fetchContents().
raywenderlich.com 667
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Button("10 results/page") {
store.baseParams["page[size]"] = "10"
store.fetchContents()
}
Button("20 results/page") {
store.baseParams["page[size]"] = "20"
store.fetchContents()
}
Button("30 results/page") {
store.baseParams["page[size]"] = "30"
store.fetchContents()
}
Button("No change") { }
raywenderlich.com 668
SwiftUI Apprentice Chapter 25: Implementing Filter Options
Depending on the selected button, you set the value of the page size key, then call
fetchContents().
This value doesn’t match either segment tag, so neither segment shows as selected
until the user taps one.
raywenderlich.com 669
SwiftUI Apprentice Chapter 25: Implementing Filter Options
.onChange(of: sortOn) { _ in
store.baseParams["sort"] = sortOn == "new" ?
"-released_at" : "-popularity"
store.fetchContents()
}
When the sortOn value changes, you set the baseParams value for the "sort" key,
then call fetchContents().
➤ Build and run. Notice the date of the first item is Sep 2019, then select New in the
picker:
You’ve implemented every HeaderView option except clearing query filters. Before
you can clear a query filter, you need a way to add them to HeaderView. So first,
you’ll implement query filters in FilterOptionsView.
raywenderlich.com 670
SwiftUI Apprentice Chapter 25: Implementing Filter Options
There are two types of query filters: Platforms (called domains in the API) and
Difficulty. Users can select one or more of each type — Android & Kotlin and Flutter,
Beginner and Intermediate — so you can’t store their selections in a dictionary like
baseParams, where each key is a unique query parameter name.
A query filter dictionary value is true if the user has selected the query filter
matching that key. In the starter project, the iOS & Swift domain and beginner
difficulty are already selected but not yet implemented.
Tapping a query filter button in FilterOptionsView toggles its value in one of these
query filter dictionaries. For example:
raywenderlich.com 671
SwiftUI Apprentice Chapter 25: Implementing Filter Options
This value also switches the color of the query filter button in FilterOptionsView:
green when true, gray when false.
.buttonStyle(
FilterButtonStyle(
selected: store.domainFilters["1"]!, width: nil))
When your users tap query filter buttons to make their selections, then tap the X or
Apply button, here’s what your code needs to do.
➤ In FilterOptionsView.swift, add this line to the actions of the xmark and Apply
buttons, before the line that dismisses this sheet:
store.fetchContents()
That’s all! You’ll soon update fetchContents() to combine all the user’s selections
into a single query URL.
store.clearQueryFilters()
func clearQueryFilters() {
domainFilters.keys.forEach { domainFilters[$0] = false }
difficultyFilters.keys.forEach {
difficultyFilters[$0] = false
}
}
You only need to set all the values to false in both query filter dictionaries. You
created a method to do this because you’ll also call it in HeaderView.
raywenderlich.com 672
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ Build and run, show the filter options view, then select some query filters. The
buttons turn green. Now tap Clear All to see them turn gray.
urlComponents.queryItems! += domainQueryItems
urlComponents.queryItems! += difficultyQueryItems
raywenderlich.com 673
SwiftUI Apprentice Chapter 25: Implementing Filter Options
You filter for domainFilters keys with value true, producing a collection of domain
keys. Then you call the queryDomain(_:) method on each key, producing an array of
URLQueryItem. You do the same for difficultyFilters, also producing an array of
URLQueryItem. Then you append each array to urlComponents.queryItems to
create your contents query URL.
➤ Build and run. Notice all the results are for iOS & Swift Beginner, even though
the header view doesn’t display these buttons.
➤ Show the filter options sheet and select or deselect some query filters. Tap Apply
or the xmark:
Apply filters
These filter buttons work. Now to get the HeaderView buttons in sync.
raywenderlich.com 674
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ In HeaderView.swift, add this code as the action for the Clear all button:
queryTerm = ""
store.baseParams["filter[q]"] = queryTerm
store.clearQueryFilters()
store.fetchContents()
You empty the search TextField value and set the value of the query parameter to
this empty string. Then, you clear the domain and difficulty query filters and call
fetchContents().
let threeColumns = [
GridItem(.flexible(minimum: 55)),
GridItem(.flexible(minimum: 55)),
GridItem(.flexible(minimum: 55))
]
➤ Clear all is one of the buttons in this grid, so replace its enclosing HStack with
the following:
HStack {
LazyVGrid(columns: threeColumns) { // 1
Button("Clear all") {
queryTerm = ""
store.baseParams["filter[q]"] = queryTerm
store.clearQueryFilters()
raywenderlich.com 675
SwiftUI Apprentice Chapter 25: Implementing Filter Options
store.fetchContents()
}
.buttonStyle(HeaderButtonStyle())
ForEach(
Array(
store.domainFilters.merging( // 2
store.difficultyFilters) { _, second in second
}
.filter { // 3
$0.value
}
.keys), id: \.self) { key in
Button(store.filtersDictionary[key]!) { // 4
if Int(key) == nil { // 5
store.difficultyFilters[key]!.toggle()
} else {
store.domainFilters[key]!.toggle()
}
store.fetchContents() // 6
}
.buttonStyle(HeaderButtonStyle())
}
}
Spacer()
}
1. A LazyVGrid fills in items horizontally, row by row. The first button is always
Clear all.
2. The dictionary method merging merges the two query filter dictionaries into a
new, temporary dictionary. You specify _, second in second to resolve any key
clashes in favor of the second dictionary. (You know there won’t be any key
clashes, but Xcode doesn’t.) To use ForEach, you create an Array from the
resulting collection of keys.
3. You filter for query filter keys with value true, just like in fetchContents().
4. For each selected key, you create a Button. To display the correct label,
filtersDictionary is Episode.domainDictionary plus difficulty items.
5. You can create an Int from a domainFilters key but not from a
difficultyFilters key, so this test tells you which query filter dictionary to
update.
raywenderlich.com 676
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ Build and run. Now the header view displays buttons for Beginner and iOS &
Swift, and these match the green buttons in FilterOptionsView:
raywenderlich.com 677
SwiftUI Apprentice Chapter 25: Implementing Filter Options
When the app launches, store.episodes is empty while the app decodes the initial
request data. After the initial download, it’s possible to select filter options that
return 0 episodes, so you keep store.loading in the condition to stop the spinner
even when there aren’t any episodes to show.
.environmentObject(store)
You pass the environment object to the modal sheet explicitly. When you present a
view as a modal sheet, it isn’t actually in the view tree of ContentView. In spite of
this, FilterOptionsView works pretty well, until it doesn’t. It’s possible to create
conditions for a run-time error, complaining the modal view doesn’t have the
environment object from an ancestor view. The app crashes. Passing store explicitly
prevents this problem.
While your app is decoding the response data into the episodes array, you display a
placeholder view for each item. This replaces text with rounded rectangles of the
same size and color. When loading finishes, you remove the reason for redaction, so
your items appear as normal.
As a final touch, make sure the PlayButtonIcon for each icon isn’t redacted.
raywenderlich.com 678
SwiftUI Apprentice Chapter 25: Implementing Filter Options
➤ Build and run and wait for the list to load. Then, change any query option to see
your redacted placeholders:
Your RWFreeView is now a fully-functional real live app. Install it on your iOS device
and enjoy exploring all our free episodes. One of the joys of writing this chapter was
watching Ray’s “SwiftUI vs. UIKit” video multiple times — just to make sure the app
was still working, of course. ;] And check out the next (final) chapter to create an
RWFreeView widget.
Key points
• Published properties aren’t Decodable, so you must explicitly decode at least one
of them to make an ObservableObject conform to Decodable.
• After adding a breakpoint to a line of code, you can edit it to print out values
without pausing the app every time that line executes.
• Remember to let ForEach and List use the id property of an Identifiable type.
• Look for opportunities to improve your users’ experience and head off “huh?”
moments.
raywenderlich.com 679
26 Chapter 26: 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 RWFreeView, providing convenient, but low-key, notification of
free episodes at raywenderlich.com. And, it gives your users quick access to 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 WWDC demos. You might still experience some instability.
That said, Widgets are cool and a ton of fun!
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.
raywenderlich.com 680
SwiftUI Apprentice Chapter 26: Widgets
And, you define a view for each size of widget — small, medium, large — you want to
support.
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.
3. Decide whether to support all three widget sizes. Create small, medium and/or
large views to display one or more data model values.
raywenderlich.com 681
SwiftUI Apprentice Chapter 26: Widgets
raywenderlich.com 682
SwiftUI Apprentice Chapter 26: Widgets
@main // 1
struct RWFreeViewWidget: Widget {
let kind: String = "RWFreeViewWidget"
1. The @main attribute means this is the widget’s entry point. The structure’s name
and its kind property are the name you gave it when you created it.
4. In this structure, you only need to customize the name to RW Free View and the
description to View free raywenderlich.com video episodes. Your users will
see these in the widget gallery.
raywenderlich.com 683
SwiftUI Apprentice Chapter 26: 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 RWFreeView folder. Use your organization instead of “com.raywenderlich” 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 RWFreeView 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.
➤ Tap + in the upper left corner, then scroll down to find RWFreeView:
raywenderlich.com 684
SwiftUI Apprentice Chapter 26: Widgets
If you’ve installed the app on a device, your gallery looks something like this:
raywenderlich.com 685
SwiftUI Apprentice Chapter 26: Widgets
Your widget works! Now, you simply have to make it display information from
RWFreeView.
➤ 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.
raywenderlich.com 686
SwiftUI Apprentice Chapter 26: Widgets
An Xcode error appears, because the widget doesn’t know about Episode. You need
to add Episode.swift to the widget target.
➤ In Project navigator, select Episode.swift. Show the File inspector and check the
Target Membership box for RWFreeViewWidgetExtension:
Now the error messages in RWFreeViewWidget.swift are the expected ones about
Missing argument for parameter 'episode' in call. The missing Episode
arguments are for creating SimpleEntry instances in placeholder(in:),
getSnapshot(in:completion:), getTimeline(in:completion:) and down in the
preview.
raywenderlich.com 687
SwiftUI Apprentice Chapter 26: Widgets
The widget doesn’t actually need uri, but the default Episode initializer requires
this parameter.
➤ Now fix the errors one by one, or use this handy shortcut for Editor ▸ Fix All
Issues: Control-Option-Command-F. Then replace all the Episode placeholders in
Provider with sampleEpisode and replace the one in RWFreeViewWidget_Previews
with Provider().sampleEpisode.
To display your widget for the first time, WidgetKit calls placeholder(in:) and
applies the same redacted(reason: .placeholder) modifier you used at the end
of the previous chapter 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.
You’ll use this to customize the widget view for small, medium and large widget
sizes.
raywenderlich.com 688
SwiftUI Apprentice Chapter 26: Widgets
if family != .systemSmall {
Text(entry.episode.description)
.lineLimit(2)
}
}
.padding(.horizontal)
.background(Color.itemBkgd)
.font(.footnote)
.foregroundColor(Color(UIColor.systemGray))
This is just a mini-version of your app’s EpisodeView, allowing more space for the
description. The small widget size doesn’t have much space, so you only display the
episode name and released properties.
Again, you need to add some app files to your widget target, to get rid of the error
messages.
raywenderlich.com 689
SwiftUI Apprentice Chapter 26: Widgets
Widget sizes
➤ Now preview your widget.
Note: Don’t worry if the playback button icon doesn’t look right. I experienced
an intermittent preview bug that displayed just an orange gradient. It looked
fine in a simulator or on a device.
Not bad, but it looks a little crowded, and a longer title wouldn’t fit at all. Try the
medium size.
raywenderlich.com 690
SwiftUI Apprentice Chapter 26: Widgets
If you think one of the sizes looks best, or if you definitely don’t want to support one
of the sizes, you can restrict your widget to specific size(s).
For RWFreeView, the medium size looks best, so you’ll only support that size.
.supportedFamilies([.systemMedium])
➤ Build and run, then close the app. If you had a small or large widget installed
before this, it’s now gone. And when you add a widget, the only choice now is
medium size.
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.
raywenderlich.com 691
SwiftUI Apprentice Chapter 26: Widgets
This code creates each entry with the same sampleEpisode. You’ll modify the
method so it displays items in the episodes array. Waiting an hour between entries
is no good for testing purposes, so you’ll shorten the interval to a few seconds.
let interval = 3
for index in 0 ..< store.episodes.count {
let entryDate = Calendar.current.date(
byAdding: .second,
value: index * interval,
to: currentDate)!
let entry = SimpleEntry(
raywenderlich.com 692
SwiftUI Apprentice Chapter 26: Widgets
date: entryDate,
episode: store.episodes[index])
entries.append(entry)
}
import WidgetKit
WidgetCenter.shared.reloadTimelines(ofKind: "RWFreeViewWidget")
➤ Build and run, then close the app. Look for your widget and add it. Then watch it
display your 20 free popular episodes:
raywenderlich.com 693
SwiftUI Apprentice Chapter 26: Widgets
➤ Tap the widget to reopen your app. Select New, wait for the list to reload, then
close the app. Your widget is still displaying popular episodes:
2. Allow the user to set different query options for the widget.
Later in this chapter, you’ll implement a deep link from the widget into your app to
open the player 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.
raywenderlich.com 694
SwiftUI Apprentice Chapter 26: Widgets
Xcode Tip: App group containers allow apps and targets to share resources.
Whenever the user changes a query option in your app, fetchContents() downloads
and decodes a new episodes array. To share this array with your widget, you’ll create
an app group. Then, in EpisodeStore.swift, you’ll write a file to this app group,
which you’ll read from in RWFreeViewWidget.swift.
➤ If you haven’t signed the targets yet, do it now. In the Project navigator, select the
top level RWFreeView folder. For each target, change the bundle identifier prefix to
your organization instead of “com.raywenderlich” and set the team.
➤ Now select the RWFreeView target. In the Signing & Capabilities tab, click +
Capability, then drag App Groups into the window. Click + to add a new container.
➤ Now select the RWFreeViewWidgetExtension target and add the App Groups
capability. If necessary, scroll through the App Groups to find and select
group.your.prefix.RWFreeView.episodes.
raywenderlich.com 695
SwiftUI Apprentice Chapter 26: Widgets
extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier:
"group.your.prefix.RWFreeView.episodes"
)!
}
}
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 contents
response into the episodes array. To write an array to a file, you JSON-encode it.
Then the widget JSON-decodes the file contents. But you can’t reuse the JSON
decoding code you’ve built into Episode because that’s expecting the nested JSON
structure sent by the API server.
Your widget only needs a few Episode properties, so you’ll create a MiniEpisode
type for it to use.
➤ In Episode.swift, add this code at the end of the file, outside of all other curly
braces:
Every property is a String so you don’t need any custom encoding or decoding code.
func writeEpisodes() {
raywenderlich.com 696
SwiftUI Apprentice Chapter 26: Widgets
Here, you convert your array of MiniEpisode values to JSON and save it to the app
group’s container.
self.miniEpisodes = self.episodes.map {
MiniEpisode(
id: $0.id,
name: $0.name,
released: $0.released,
domain: $0.domain,
difficulty: $0.difficulty ?? "",
description: $0.description)
}
self.writeEpisodes()
You map your array of Episode values into an array of MiniEpisode values, then
write this array into your app group file. The existing call to WidgetCenter now tells
the widget to reload its timeline whenever your app has downloaded and decoded a
new array of episodes.
raywenderlich.com 697
SwiftUI Apprentice Chapter 26: Widgets
MiniEpisode contains only the parameters the widget needs, in a slightly different
order.
Text(String(entry.episode.difficulty)
.capitalized)
Now, you can read your episodes array from the app group file.
This reads the MiniEpisode values from the file fetchContents() saved into the
app group’s container.
raywenderlich.com 698
SwiftUI Apprentice Chapter 26: Widgets
You read the episodes array from the app group file and use it instead of
store.episodes.
Note: If you changed the bundle identifier, you’ll end up having two apps.
Delete the old one before running the project.
➤ Build and run, then close the app. Look for your widget and add it. Watch it display
a few of your 20 free popular episodes, then tap the widget to reopen your app. Select
New, wait for the list to reload, then close the app. Your widget is now displaying
recent episodes:
raywenderlich.com 699
SwiftUI Apprentice Chapter 26: Widgets
Your widget’s 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 display the PlayerView for the current widget entry.
2. Modify the top level container view of your widget view with widgetURL(_:).
For this app, the id property of Episode uniquely identifies it. So the URL to open
“SwiftUI vs. UIKit” is simply:
URL(string: "rwfreeview://5117655")
And you can access this id value as the host property of the URL. So simple!
raywenderlich.com 700
SwiftUI Apprentice Chapter 26: Widgets
In your widget
➤ In RWFreeViewWidget.swift, in RWFreeViewWidgetEntryView, add this modifier
to the top-level VStack:
.widgetURL(URL(string: "rwfreeview://\(entry.episode.id)"))
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 RWFreeViewApp, or to the top level
view of the root view. For RWFreeView, you’ll attach this to the NavigationView in
ContentView, because the perform closure must assign a value to a @State property
of ContentView.
First, you need to trigger NavigationLink programmatically. You’ll use its tag-
selection initializer to activate it when you set a value for the selection argument.
➤ Then, replace the ZStack in the ForEach closure with the following:
ZStack {
NavigationLink(
destination: PlayerView(episode: episode),
tag: episode,
selection: $selectedEpisode) {
EmptyView()
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
EpisodeView(episode: episode)
.onTapGesture {
selectedEpisode = episode
}
}
raywenderlich.com 701
SwiftUI Apprentice Chapter 26: Widgets
Here you made Episode conform to Hashable by implementing the equatable static
function, ==(_:_:), and hash(into:).
.onOpenURL { url in
if let id = url.host,
let widgetEpisode = store.episodes.first(
where: { $0.id == id }) {
selectedEpisode = widgetEpisode
}
}
You extract the id value from the widget URL, then find the first episode with the
same id value.
raywenderlich.com 702
SwiftUI Apprentice Chapter 26: Widgets
➤ Build and run, wait for the list to load, then close the app and add your widget. Tap
an entry to see it open the PlayerView with that video:
Note: This doesn’t work every time. Often, when a deep link doesn’t open
PlayerView, tapping the item in the app doesn’t open PlayerView either. This
happens on a device as well as in the simulator. NavigationLink has a history
of buggy behavior.
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. The new
timeline doesn’t start immediately. See for yourself.
raywenderlich.com 703
SwiftUI Apprentice Chapter 26: Widgets
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 RWFreeView. 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 fetchContents() at the same time every
day, and this would also refresh your widget’s timeline.
Note: The project in the final folder still displays every three seconds.
You’re restoring the template code’s original timing. Now, your widget will display
episodes one hour apart. You can add it to your device’s home screen with no worries
about excessive battery use.
You can also remove the declaration of interval as Xcode so helpfully suggests
since you’re no longer using it.
raywenderlich.com 704
SwiftUI Apprentice Chapter 26: Widgets
Key points
• WidgetKit is a 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.
raywenderlich.com 705
27 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 raywenderlich.com 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 raywenderlich.com 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.raywenderlich.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 raywenderlich.com
possible, and we truly appreciate it!
raywenderlich.com 706