Professional Documents
Culture Documents
Apress Pro Iphone Development
Apress Pro Iphone Development
Development
with Swif tUI
Design and Manage Top-Quality Apps
—
Fourth Edition
—
Wallace Wang
Pro iPhone Development
with SwiftUI
Design and Manage Top-Quality Apps
Fourth Edition
Wallace Wang
Pro iPhone Development with SwiftUI: Design and Manage Top-Quality Apps
Wallace Wang
San Diego, CA, USA
iii
Table of Contents
iv
Table of Contents
v
Table of Contents
vi
Table of Contents
Index��������������������������������������������������������������������������������������������������������������������� 395
vii
About the Author
Wallace Wang is a former Windows enthusiast who took one look at Vista and realized
that the future of computing belonged to the Mac. He’s written more than 40 computer
books, including Microsoft Office for Dummies, Beginning Programming for Dummies,
Steal This Computer Book, Beginning ARKit for iPhone and iPad, and Beginning iPhone
Development with SwiftUI. He also wrote a book on game design called The Structure
of Game Design, which explains how to create all types of games including board,
card, and video games. In addition to programming the Mac and iPhone/iPad, he
also performs stand-up comedy, having appeared on A&E’s An Evening at the Improv
and having performed in Las Vegas at the Riviera Comedy Club at the Riviera Hotel
and Casino. When he’s not writing computer books or performing stand-up comedy,
he enjoys blogging about screenwriting at his site, The 15 Minute Movie Method
(https://15minutemoviemethod.com), where he shares screenwriting tips with other
aspiring screenwriters who all share the goal of breaking into Hollywood.
ix
About the Technical Reviewer
Massimo Nardone has more than 22 years of experience
in security, web and mobile development, cloud, and IT
architecture. His true IT passions are security and Android.
He has been programming and teaching how to program
with Android, Perl, PHP, Java, VB, Python, C/C++, and
MySQL for more than 20 years.
Massimo also holds a master of science degree in
computing science from the University of Salerno, Italy.
He has worked as a project manager, software engineer,
research engineer, chief security architect, information
security manager, PCI/SCADA auditor, and senior lead IT security/cloud/SCADA
architect for many years.
His technical skills include security, Android, cloud, Java, MySQL, Drupal, Cobol,
Perl, web and mobile development, MongoDB, D3, Joomla, Couchbase, C/C++, WebGL,
Python, Pro Rails, Django CMS, Jekyll, Scratch, etc.
He currently works as Chief Information Security Officer (CISO) for Cargotec Oyj.
He worked as visiting lecturer and supervisor for exercises at the Networking
Laboratory of the Helsinki University of Technology (Aalto University). He holds four
international patents (PKI, SIP, SAML, and Proxy areas).
xi
CHAPTER 1
Organizing Code
Programs are rewritten and modified far more often than they are ever created. That
means most of the time developers must change and modify existing code either written
by someone else or written by you sometime in the past. Since you may be writing code
that you or someone else will eventually modify in the future, you should organize your
code to make it easy to understand.
While every developer has their own programming style and no two programmers
will write the exact same code, programming involves writing code that works and
writing code that’s easy to read.
Writing code that works is hard. Unfortunately, once developers get their code to
work, they rarely clean it up and optimize it. The end result is a confusing mix of code
that works but isn’t easy to understand. To modify that code, someone has to decipher
how it works and then rewrite that code to make it cleaner to read while still working
as well as the original code. Since this takes time and doesn’t add any new features, it’s
often ignored.
Since few developers want to take time to clean up their code after they get it to work,
it’s best to get in the habit of writing clear, understandable code right from the start. That
involves several tasks:
• Writing code in a consistent and understandable style
• Making the logic of your code clear so anyone reading it later can
easily understand how it works
When writing code, focus on clarity and readability. It’s possible to write code that
works but is hard to understand. That makes modifying that code difficult. Many times, it
can be easier to rewrite code from scratch rather than waste time trying to figure out how
it works.
1
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_1
Chapter 1 Organizing Code
Swift lets you choose any name for variables and constants. However, it’s a good
idea to use descriptive names to help you understand what type of data that variable or
constant can hold. Consider the following variable names:
var x: Int = 8
var dgie83: Double = 13.48
var FLdkjep: String = "Right"
While valid, these names don’t make it clear what type of data they hold. A far better
solution is to use descriptive names like this:
Single word variable names can be fine, but you may want to use multiple words to
make a variable or constant name even more descriptive. When combining multiple
words to form a variable or constant name, Swift programmers commonly use
camelCase, which uses lowercase letters for the first word and an uppercase letter for the
first letter of each succeeding word like this:
Just as variable and constant names can be too short, they can also be too long.
Ideally, use descriptive names that get their meaning across using as few words and
characters as possible.
When declaring variables or constants, you can optionally define the data type they
hold by adding a prefix or suffix that identifies the type of data they contain such as
2
Chapter 1 Organizing Code
Note The idea of adding data type prefixes to variable and constant names
is known as Hungarian Notation, which was invented by Charles Simonyi, who
worked at Xerox PARC and Microsoft.
The ultimate goal is to write self-documenting code that makes it easy for anyone
to understand at first glance. One huge trap that programmers often make is assuming
they’ll be able to understand their own code months or even years later. Yet even after
a few weeks, your own code can seem confusing because you’re no longer familiar with
your assumptions and logic that you had when you wrote the code originally.
If you can’t even understand your own code months or even weeks later, imagine
how difficult other programmers will find your code when they have to modify it in your
absence. Good code doesn’t just work, but it’s easy for other programmers to understand
how it works and what it does as well.
When developing your own programming style, strive for consistency and
organization. Consistency means you use the same convention for writing code whether
it’s naming variables with prefixes or suffixes that identify the data type or indenting
code the same way to highlight specific steps.
Organization means using spacing and storing related code together such as putting
variables and functions in the same location consistently. This can group chunks of code
in specific places to make code easier to understand as shown in Figure 1-1.
3
Chapter 1 Organizing Code
Figure 1-1. Grouping related code together makes it easy to know where to look
for certain information
The specific placement of code is arbitrary, but what’s important is that you organize
code in a consistent and predictable manner so it’s easy to find. The clearer your code,
the easier it will be to fix and modify it later.
5
Chapter 1 Organizing Code
The two // symbols define a comment. The MARK: text tells Xcode to create a pull-
down menu category. The descriptive text can be any arbitrary text you want to identify
the code that appears underneath.
Once you’ve defined one or more //MARK: comments, you can quickly jump to any
of them by clicking the last item displayed above Xcode’s middle pane to open a pull-
down menu as shown in Figure 1-3.
Figure 1-3. Displaying Xcode’s pull-down menu that lists all //MARK: comments
Use the //MARK: comment generously throughout each .swift file. This will make it
easy to jump to different parts of your code to modify or study later.
6
Chapter 1 Organizing Code
• SwiftUI View
• Swift File
Figure 1-4. The two most common types of .swift files in a project
SwiftUI View files define user interfaces that appear on an iOS screen.
7
Chapter 1 Organizing Code
The Swift File option creates a blank .swift file for storing and isolating code such as
defining a list of variables, data structures, or classes.
The more .swift files you add to a project, the harder it can be to find any particular
file. To help organize all the files that make up a project, Xcode lets you create folders.
By using folders, you can selectively hide or display the contents of a folder as shown in
Figure 1-5.
To create an empty folder, choose File ➤ New ➤ Group. Once you’ve created an
empty folder, you can drag and drop other folders or files into that empty folder.
Another option is to select one or more files and/or folders by holding down the
Command key and clicking a different file and/or folder. Then choose File ➤ New ➤
Group from Selection. This creates a new folder and automatically stores your selected
items into that new folder.
8
Chapter 1 Organizing Code
You can also right-click in the Navigator pane to display a popup menu with the New
Group or New Group from Selection commands as shown in Figure 1-6.
Note If the Group or Group from Selection commands are grayed out, click a file
to select it before choosing the File ➤ New ➤ Group or File ➤ New ➤ Group from
Selection command.
Once you’ve created a folder, you can always delete that folder afterward. To delete a
folder, follow these steps:
Figure 1-7. Xcode alerts you if you’re deleting a folder that contains files
Note Deleting a folder also deletes its contents, which can include other folders
and files.
3. Click the Move to Trash button to delete the files completely (or
click Remove References to keep the file and disconnect the file
from your project but without deleting it).
10
Chapter 1 Organizing Code
11
Chapter 1 Organizing Code
12
Chapter 1 Organizing Code
Figure 1-10. The Create Code Snippet command for adding your own code to the
Code Snippet library
13
Chapter 1 Organizing Code
3. Click in the text field and type a descriptive name for your code
snippet. You may also want to edit your code or modify other
options as well such as typing a description of the code in the
Summary text field.
4. Click Done. From now on, you’ll be able to use your custom code
snippet in any Xcode project.
14
Chapter 1 Organizing Code
5. Click the Edit button. Now you can modify the code, and when
you’re finished, click Done. (Or click the Delete button. When
Xcode asks if you really want to delete the code snippet, click
Delete.)
15
Chapter 1 Organizing Code
Besides taking up space with duplicate code, another problem is if you want to
change one or more modifiers such as changing the font size or the background color.
With duplicate modifiers, you need to modify every copy, increasing the chance you’ll
miss one or more copies.
A better solution is to store commonly used groups of modifiers together in a
separate structure defined as a ViewModifier. Then you can apply this ViewModifier
structure to multiple views. Now if you need to change these modifiers, you can change
them in one place rather than in multiple places throughout your code.
In the preceding example, it makes sense to store the modifiers within a
ViewModifier structure like this:
This ViewModifier structure encloses all the modifiers, which can then be applied to
any view by using .modifier followed by the name of your structure like this:
16
Chapter 1 Organizing Code
Just as functions let you isolate and reuse code, so can ViewModifiers let you isolate
and reuse modifiers for different views.
17
Chapter 1 Organizing Code
To solve this problem, developers rely on version control systems such as Git.
Version control systems offer several advantages:
Storing source code in one place ensures that everyone can access the latest version
of the project. Version control systems store source code on a server that allows any team
member to access from anywhere in the world.
Once people edit source code, the version control system can track each person’s
changes. This allows everyone to see who made what changes on what date. Then
people can selectively reverse any changes (if they don’t work) or continue editing the
source code with their own changes.
Most importantly, version control systems allow two or more people to work on the
same file simultaneously for greater efficiency. Then they can merge their changes back
into the main project. Although there are dozens of version control systems available,
Xcode offers built-in support for Git, one of the more popular version control systems.
Note Learning Git, or any version control system, takes time and requires a
separate book (or two) on its own.
To see how Xcode offers built-in support for Git, follow these steps:
3. Click the General tab and you can see all the options available as
shown in Figure 1-12. The most important option is to select the
Enable Source Control check box.
18
Chapter 1 Organizing Code
4. Click in the Default Branch Name text field and type a name. The
most common branch name for years has been “master,” but
many programmers have switched to calling their branch name
“main” instead.
19
Chapter 1 Organizing Code
6. Click in the Author Name text field and type your name.
7. Click in the Author Email text field and type your email address.
Your name and email address will track your changes and allow
others to contact you if they have questions about your code.
The preceding steps simply allow Xcode to use Git, but you’ll still need to create a
repository either locally (on your own computer) or remotely (on a server on the Internet
such as GitHub or GitLab).
To connect to a remote repository, you’ll need to set up an account on that repository
and then tell Xcode how to connect to that remote repository by following these steps:
2. Click the + icon in the bottom-left corner to open a dialog that lets
you choose a remote repository as shown in Figure 1-14.
20
Chapter 1 Organizing Code
When you’re creating a new Xcode project, you must give your project a name. When
Xcode displays a dialog to let you choose where to store your project, select the Create
Git repository on my Mac check box as shown in Figure 1-15.
21
Chapter 1 Organizing Code
If you’ve already created a project, you can add a Git repository by following
these steps:
1. Make sure your Xcode project is open.
22
Chapter 1 Organizing Code
Figure 1-16. Creating a new Git repository for an existing Xcode project
23
Chapter 1 Organizing Code
3. Click Create.
At this point, you’ll have just set up Xcode to work with a Git repository. To actually
work with a Git repository, you’ll need to learn how to share code, track changes, merge
changes, and create separate branches. Just keep in mind that learning a version control
system like Git is crucial for software development, so start creating Git repositories for
your Xcode projects and practice using Git before you actually need to use it on a critical
project.
S
ummary
Writing iOS apps involves writing new code and modifying existing code. To do both
tasks, you need to understand how any existing code works so you don’t accidentally
duplicate or break it. In many cases, you’ll have to edit other people’s code, which may or
may not have been written in a clear, understandable manner.
Although you can’t control how other programmers write code, you can control how
you write code. The general principle is to write code that’s easy to understand. This can
involve adding comments (especially //MARK: comments to make it easy to jump to
specific parts of your code). You should also use descriptive variable names and organize
24
Chapter 1 Organizing Code
the related code in logical groups. You can do that by storing different parts of your code
together. You can also organize code by storing code in separate files that you can group
in folders.
To ensure you write common Swift statements in a consistent manner, you can use
code snippets to insert the basic Swift code for you. Then you just have to customize
it with your own data. For more flexibility, store your own code in the Code Snippet
window. That way, you can reuse your own code between multiple projects in Xcode.
Organizing code is never necessary, but since most programs are modified multiple
times, proper organization ahead of time can make modifying code much easier. Always
assume that someone else will modify your code and make it easy on that person for the
future, especially because that person could be you.
For large projects, always use some form of version control such as Git to help
you track changes and collaborate with others. Version control is optional but highly
recommended for all projects that you cannot afford to lose.
25
CHAPTER 2
Debugging Code
In the professional world of software, you’ll actually spend more time modifying existing
programs than you ever will creating new ones. When writing new programs or editing
existing ones, it doesn’t matter how much experience or education you might have
because even the best programmers can make mistakes. In fact, you can expect that you
will make mistakes no matter how careful you may be. Once you accept this inevitable
fact of programming, you need to learn how to find and fix your mistakes.
In the world of computers, mistakes are commonly called “bugs,” which gets
its name from an early computer that used physical switches to work. One day, the
computer failed, and when technicians opened the computer, they found that a moth
had been crushed within a switch, preventing the switch from closing. From that point
on, programming errors have been called bugs, and fixing computer problems has been
known as debugging.
Three common types of computer bugs are
Syntax errors are the easiest to find and fix because they’re merely misspellings
of variable names that you created or misspelling of Swift commands that Xcode can
help you identify. If you type a Swift keyword such as “var” or “let,” Xcode displays that
keyword in magenta (or whatever color you specify for displaying keywords in the Xcode
editor).
27
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_2
Chapter 2 Debugging Code
Now if you type a Swift keyword and it doesn’t appear in its usual identifying color,
then you know you probably typed it wrong somehow. By coloring your code, Xcode’s
editor helps you visually identify common misspellings or typos.
Besides using color, the Xcode editor provides a second way to help you avoid
mistakes when you need to type the name of a method or class. As soon as Xcode
recognizes that you might be typing a known item, it displays a popup menu of possible
options. Now instead of typing the entire command yourself, you can simply select
a choice in the popup menu and press the TAB or ENTER key to let Xcode type your
chosen command correctly as shown in Figure 2-1.
Figure 2-1. Xcode displays a menu of possible commands you might want to use
Syntax errors often keep your program from running at all. When a syntax error
keeps your program from running, Xcode can usually identify the line (or the nearby
area) of your program where the misspelled commands appears so you can fix it as
shown in Figure 2-2.
Figure 2-2. Syntax errors often keep a program from running, which allows Xcode
to identify the syntax error
28
Chapter 2 Debugging Code
Logic errors are much harder to find and detect than syntax errors. Logic errors
occur when you use Swift code correctly, but it doesn’t do what you want it to do. Since
your code is actually valid, Xcode has no way of knowing that it’s not working the way
you intended. As a result, logic errors can be difficult to debug because you think you
wrote your code correctly but you (obviously) did not.
How do you find a mistake in code that you thought you wrote correctly? Finding
your mistake can often involve starting from the beginning of your program and
exhaustively searching each line all the way until the end. (Of course, there are faster
ways than searching your entire program, line by line, which you’ll learn about later in
this chapter.)
Finally, the hardest errors to find and debug are runtime errors. Syntax errors usually
keep your program from running, so if your program actually runs, you can assume that
you have eliminated most, if not all, syntax errors in your code.
Logic errors can be tougher to find, but they’re predictable. For example, if your
program asks the user for a password but fails to give the user access even though the
user types a correct password, you know you have a logic error. Each time you run your
program, you can reliably predict when the logic error will occur.
Runtime errors are more insidious because they don’t always occur predictably. For
example, your app may run perfectly well on an iPhone, but the moment you run the
same app on an iPad (or vice versa), the app fails. That’s because conditions between
two different iOS devices will never be exactly the same.
The problem is that unexpected, outside circumstances can affect an app’s behavior
such as another app taking up too much memory or one device might be running a
different version of iOS than another device. Because runtime errors can’t always be
duplicated, they can be frustrating to find and even harder to fix since you can’t always
examine every possible condition your app might face when running on different iOS
devices. Some apps can work perfectly – except if the user accidentally presses two keys
at the same time. Other apps work just fine – until the user happens to save a file at the
exact moment that another app tries to receive data over a WiFi connection.
Usually, you can eliminate most syntax errors and find and fix most logic errors.
However, it may not be possible to find and completely eliminate all runtime errors in a
program. The best way to avoid spending time hunting for bugs is to strive to write code
and test it carefully to make sure it’s as error-free as possible.
29
Chapter 2 Debugging Code
• Add the // symbols at the beginning of each line that you want to
convert into a comment. This method lets you convert a single line
into a comment.
• Add the /* symbols at the beginning of code and add the */ at the end
of code you want to convert into a comment. This method lets you
convert one or more lines into a comment.
30
Chapter 2 Debugging Code
• Select the lines of code you want to turn into a comment and choose
Editor ➤ Structure ➤ Comment Selection (or press Command +
/). This method lets you convert one or more lines into a comment
by placing the // symbols at the beginning of each line of code you
selected.
Note Xcode color-codes comments in green (or whatever color you may have
defined to identify comments). After creating a comment, make sure Xcode color-
codes it properly to ensure you have created a comment. If Xcode fails to recognize
your comments, it will treat your text as a valid Swift command, which will likely
keep your code from running properly.
By turning code into comments, you essentially hide that code from Xcode. Now if
you want to turn that comment back into code again, you just remove the // or /* and */
symbols that define your commented out code.
If you commented out code by choosing Editor ➤ Structure ➤ Comment Selection
(or pressing Command + /), just repeat the command again to convert that commented
code back to working code once more.
Besides turning your code into comments to temporarily hide it, a second simple
debugging technique is to use the print command. The idea is to put the print command
in your code to print out the values of a variable wherever you think your code may be
making a mistake.
By doing this, you can see what values one or more variables may contain. Putting
multiple print commands throughout your program gives you a chance to make sure
your program is running correctly.
To see how using the print command along with commenting out code can work to
help you debug a program, follow these steps:
1. Create a new iOS App project, make sure it uses SwiftUI, and give
it a descriptive name (such as DebugApp).
2. Click the ContentView file in the Navigator pane. The Editor pane
displays the contents of the ContentView file.
31
Chapter 2 Debugging Code
import SwiftUI
}
}
If you know anything about temperatures in Fahrenheit and Celsius, you know that
the boiling point in Celsius is 100 degrees and the boiling point in Fahrenheit is 212
degrees. Yet our temperature conversion program calculates that 100 degrees Celsius is
equal to 157.6 degrees in Fahrenheit, which means the Fahrenheit temperature should
be 212 rather than 157.6.
32
Chapter 2 Debugging Code
Note When testing code, start out with answers that you already know, such as
knowing that 100 degrees Celsius must equal 212 degrees Fahrenheit. If the code
fails to give you the answer you already know, the code must be incorrect.
Obviously, 100 degrees Celsius cannot be equal to 157.6 degrees Fahrenheit, so let’s
use the print command and comments to help debug the problem.
This comment will let us check if the tempC parameter is properly coming into
the C2F function and getting stored in the tempF variable.
Note The print command only works in the Simulator and will not work when
running on the app using Live Preview in the Canvas pane. The print command
displays text in the Debug Area of the Xcode window.
33
Chapter 2 Debugging Code
132.0
Although the formula might look correct, the error occurs because
of the way Swift (and most programming languages) calculate
formulas. First, they start from left to right. Second, they calculate
certain operations such as multiplication before addition.
Notice that the program now correctly converts 100 degrees Celsius to 212 degrees
Fahrenheit. For simple debugging, turning code temporarily into comments and
using the print command can work, but it’s fairly clumsy to keep adding and removing
34
Chapter 2 Debugging Code
comment symbols and print commands. A much better solution is to use breakpoints
and variable watching, which essentially duplicates using comments and print
commands.
• Breakpoints
• Variable watching
Note Breakpoints and variable watching only work in the Simulator and will not
work when running on the app using the Live icon in the Canvas pane.
Using Breakpoints
Breakpoints let you identify a specific line in your code where you want your program to
stop. Once your program stops, you can step through your code, line by line. As you do
so, you can also peek at the contents of one or more variables to check if the variables are
holding the right values.
For example, if your program converts Celsius to Fahrenheit, but somehow converts
100 degrees Celsius into –41259 degrees Fahrenheit, you know your code isn’t working
right. By inserting breakpoints in your code and examining the values of your variables
at each breakpoint, you can identify where your code calculates its values. The moment
you spot the line where it miscalculates a value, you know the exact area of your program
that you need to fix.
35
Chapter 2 Debugging Code
• Clicking to the left of the code where you want to set the breakpoint
• Moving the cursor to a line where you want to set the breakpoint and
pressing Command + \
Xcode displays breakpoints as blue arrows in the left margin as shown in Figure 2-3.
• Step Over
• Step Into
• Step Out
The Step Over command examines the next line of code, treating function or method
calls as a single line of code.
36
Chapter 2 Debugging Code
The Step Into command works exactly like the Step Over command until it highlights
a function or method call. Then it jumps to the first line of code in that function
or method.
The Step Out command is used to prematurely exit out of a function or method that
you entered using the Step Into command. The Step Out command returns to the line of
code where a function or method was called.
All three Step commands are used after a program temporarily stops at a breakpoint.
By using a Step command, you can examine your code, line by line, and see how values
stored in different variables may change.
Such variable watching lets you examine the contents of one or more variables to
verify if it’s holding the correct data. The moment you spot a variable holding incorrect
data, you can zero in on the line of code that’s creating that error.
The best part about breakpoints is that you can easily add and remove them since
they don’t modify your code at all, unlike comments and multiple print commands.
Xcode can remove all breakpoints automatically, so you don’t have to hunt through your
code to remove them one by one.
To see how to use breakpoints, step commands, and variable watching, follow
these steps:
2. Click the ContentView file in the Navigator pane and modify the
C2F function as follows:
37
Chapter 2 Debugging Code
6. Choose Debug ➤ Step Into (or press F7). Xcode jumps from the
breakpoint into the function C2F. The information in the left-
hand side of the Debug Area displays the current values that your
program is using as shown in Figure 2-4.
38
Chapter 2 Debugging Code
Figure 2-4. By watching how variables change, you can see how each line of code
affects each variable
7. Choose Debug ➤ Step Over (or press F6) several more times until
the Simulator displays the two temperatures in both Celsius and
Fahrenheit.
39
Chapter 2 Debugging Code
10. Click the Run button or choose Product ➤ Run. The Simulator
window appears showing a blank screen. Notice that since you
deactivated breakpoints, Xcode runs the entire program without
stopping at any of the breakpoints.
13. Move the mouse pointer over the breakpoint and drag to the left
or right.
14. Release the left mouse button. Xcode deletes the breakpoint.
Managing Breakpoints
There’s no limit to the number of breakpoints you can put in a program, so feel free
to place as many as you need to help you track down errors. Of course, if you place
breakpoints in a program, you may lose track of how many breakpoints you’ve set
and where they might be set. To help you manage your breakpoints, Xcode offers a
Breakpoint Navigator.
You can open the Breakpoint Navigator in one of three ways:
• Press Command + 8.
The Breakpoint Navigator lists all the breakpoints set in your program and identifies
the files the breakpoints are in and the line number of each breakpoint as shown in
Figure 2-5.
40
Chapter 2 Debugging Code
41
Chapter 2 Debugging Code
• Ignore – The number of times from zero or more that you want
the function or method to run before temporarily halting program
execution.
Figure 2-6. The Symbolic Breakpoint popup window lets you define a breakpoint
42
Chapter 2 Debugging Code
3. Click in the Symbol text field and type C2F, which is the name of
the function or method you want to examine.
43
Chapter 2 Debugging Code
Figure 2-7. The symbolic breakpoint halts program execution in the C2F function
defined by the Symbol text field
44
Chapter 2 Debugging Code
45
Chapter 2 Debugging Code
46
Chapter 2 Debugging Code
Summary
Errors or bugs are unavoidable in any app. While syntax errors are easy to find and fix,
logic errors can be tougher to find because you thought your code would create one
type of result but it winds up creating a different result. Now you’re left trying to figure
out what you did wrong when you thought you were doing everything right. Even harder
errors to track down are runtime errors that occur seemingly at random because of
unknown conditions that affect an app.
To help you track down and eliminate most bugs, you can use the print command
along with comments, but for most robust debugging, you should use Xcode’s built-
in debugger. With the debugger, you can set breakpoints in your code and watch how
values get stored in one or more variables.
A conditional breakpoint only stops program execution when a certain condition
occurs. A symbolic breakpoint only stops program execution when a specific function
or method gets called. Once a breakpoint stops a program, you can continue examining
your code line by line using various step commands. The Step Into command lets
you view code stored inside a function or method, while the Step Out command lets
you prematurely exit out of a function or method and jump back to the function or
method call.
By using breakpoints and step commands, you can exhaustively examine how your
program works, line by line, to eliminate as many errors as possible. The fewer errors
your app contains, the happier your users will be.
47
CHAPTER 3
Understanding Closures
Reading a single sentence isn’t difficult for most people, but when you combine
thousands of sentences together, reading a long mass of text can be cumbersome. That’s
why people divide large amounts of text into parts such as paragraphs and chapters.
Programming is no different.
Rather than write code as one large mass of text, programmers typically divide a
large program into smaller functions where each function performs a single task. Not
only do functions help make a large program easier to understand, but functions also act
like building blocks that you can reuse in other programs.
You should already be familiar with the standard way to create a function by
using the func keyword followed by a descriptive name, parameter list, and a block of
code such as
func descriptiveName() {
// Code here
}
descriptiveName()
If a function returns a value, you can assign a function to represent a value such as
var x = descriptiveName()
1. Create a function.
49
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_3
Chapter 3 Understanding Closures
Another way to write a function is as a closure. Closures simply give you another way
to write functions that let you create and call them in a single step. Functions store code
in a separate location, which makes it cumbersome to see how that code works. Closures
store code right where it’s being used, so it’s easier to see how it works.
By using closures as a different way to write functions, you can write more concise
code (with the drawback of being harder to read and understand). Closures can be
written in several different ways. When you create a function, you need to use the func
keyword followed by a descriptive name, a parameter list, and code that calculates a
result such as
One way to rewrite this function as a closure involves dropping the func keyword and
the function name, then enclose the rest of the code in curly brackets like this:
A second way to write a closure is to eliminate the parameter list altogether like this:
let z = {x in return x * 2}
Still another shortcut is to eliminate the return keyword altogether like this:
let w = {x in x * 2}
An even more condensed version of a closure simply displays the return calculation
by eliminating any variables and replacing them with placeholders that identify different
parameters such as
let v = {$0 * 2}
50
Chapter 3 Understanding Closures
print(multiplyBy2(x: 4))
print(multiplyBy2(x: 17))
print("{x in x * 2}")
let w = {x in x * 2}
print (w(4))
print (w(17))
print("{$0 * 2}")
let v = {$0 * 2}
print(v(4))
print(v(17))
4. Click the Run button. The debug area displays the following:
34
{x in x * 2}
8
34
{$0 * 2}
8
34
Notice how all versions of the closure work exactly the same as the function
declaration. The only difference is how concise each written closure appears. By
understanding the different ways closures can be written, you can recognize them in
code written by other people.
Create a closure in whatever style you wish that makes most sense to you. For
simplicity, many programmers use the concise version that uses $0 as a placeholder for
the first passed parameter, $1 for the second passed parameter, $2 for the third passed
parameter, and so on.
When using closures, you need to enclose all parameters inside parentheses. In
many cases, you do not need to define the data type of each parameter since Swift can
infer that value based on the data type of the return value. For example, if the return
value data type is an integer, Swift infers that the passed parameters must be integers as
well such as
However, if there is any ambiguity, you must explicitly define the data types of your
parameters such as
52
Chapter 3 Understanding Closures
Notice that the top two examples define the integer data type with a colon and the
Int keyword, while the last example defines the integer data type with the “as” and Int
keywords.
Modify your ClosurePlayground file as follows and click the Run button:
print(addNumbers(x: 4, y: 5))
print(addNumbers(x: 17, y: 9))
53
Chapter 3 Understanding Closures
Figure 3-1. A function can access variables inside and above a function
In Figure 3-1, the “randomValue” constant is declared outside of the function, but
the function can still access its value. However, the “wildcard” constant is declared inside
the function, so it can only be accessed inside that function and nowhere else.
Since “wildcard” is declared inside the function, we cannot access that value outside
that function as shown in Figure 3-2.
Figure 3-2. Values declared inside a function cannot be accessed outside that
function
Because closures are just another way of writing a function, closures can also capture
and modify values declared outside of their scope.
54
Chapter 3 Understanding Closures
To call this function, you would use the function name and pass it
parameters such as
addNumbers(x: 17, y: 9)
Then you can run this closure by using its name and pass it parameters such as
addNumbers1(17, 9)
addNumbers2(17, 9)
addNumbers3(17, 9)
addNumbers4(17, 9)
addNumbers2(17, addNumbers1(17,9))
addNumbers2(17, 26)
55
Chapter 3 Understanding Closures
The first four lines define four different closures that work exactly alike, which is to
accept two integers as parameters, add them together, and return the sum. The fifth line
creates an array that holds each closure where each closure gets different parameters.
The sixth line prints the total number of items in the closureArray (4), and then the
for-in loop prints each item in the closureArray so the output looks like this:
10
5
13
12
() -> Void
2. Name it TrailingClosurePlayground.
56
Chapter 3 Understanding Closures
simpleExample() {
print("---2. Go to bathroom")
print("---3. Brush teeth")
}
The preceding code actually calls the function (simpleExample) and also defines the
closure’s code inside its curly brackets, which define two print statements.
Note When calling the function, the parameter list (parentheses) can be omitted
like this:
simpleExample {
print("---2. Go to bathroom")
print("---3. Brush teeth")
}
5. Click the Run button. The debug area displays the following:
1. Wake up
---2. Go to bathroom
---3. Brush teeth
4. Eat breakfast
57
Chapter 3 Understanding Closures
Notice how the print statements work. The first print statement defined by the
simpleExample function prints “1. Wake up”. Then it calls the closure.
The closure then prints “---2. Go to the bathroom” and “---3. Brush teeth”. Then the
second print statement in the simpleExample function prints “4. Eat breakfast”.
The preceding code defines two parameters that must be integer (Int) data types. To
see how to pass parameters to a trailing closure, follow these steps:
1. Make sure your TrailingClosurePlayground is loaded into Xcode.
2. Type the following to create a function:
The preceding code defines a parameter list that can accept two
integers. In between the two print statements, it calls the closure
and passes it two integer values (4, 8).
3. Type the following underneath the passParameters function:
passParameters { x, y in
print ("-- Closure code beginning")
print ("\(x * y)")
print ("-- ending")
}
58
Chapter 3 Understanding Closures
First line
-- Closure code beginning
32
-- ending
Second line
The preceding code defines two parameters that must be integer (Int) data types.
Then it returns an integer (Int) value. To see how to pass parameters to a trailing closure
that returns a value, follow these steps:
This code defines a trailing closure that accepts two integers and
returns a single integer value. In between its two print statements,
it calls the closure, passes it the integers 5 and 2, and then prints
that return value.
59
Chapter 3 Understanding Closures
returnValue { x, y in
x + y
}
4. Click the Run button. The debug area displays the following:
First line
7
Second line
Summary
Closures are nothing more than another way to write a function. Instead of creating a
function and then calling that function in a two-step process, you can create and use a
closure in one step.
There are different ways to write a closure where each succeeding version gets
sparser and more cryptic. Suppose you had a function like this:
60
Chapter 3 Understanding Closures
When passing parameters into a closure, enclose them in parentheses. In case the
data type of a closure’s parameters might not be clear, explicitly define the data type
like this:
Be aware that closures can access and modify variables declared outside of the
closure. You can assign closures to a name or simply use closures in place of data. Any
place where you can use data, you can use a closure. If you want to pass a function as an
argument, you can use a trailing closure.
Just be careful using closures since closures aren’t always obvious how they work.
Closures offer efficiency in exchange for possible confusion, so use closures sparingly or
add comments to explain how a closure works.
61
CHAPTER 4
Multithreaded
Programming Using
Grand Central Dispatch
The next time you pay for groceries in a supermarket, look at the lines at the checkout
stands. If there’s only one open checkout stand, there’s likely a long line of customers
waiting to pay. That means everyone has to wait their turn before they can leave.
However, if there are multiple checkout stands open, more customers can pay at the
same time, and the wait time for everyone is much less. That’s the basic idea behind
multithreaded programming.
In the old days of computers, tasks were fairly simple, so processors were fast enough
to handle them one at a time no matter how many there might be. Gradually, as software
got more sophisticated and tasks got more complex, processors couldn’t handle so many
complicated tasks simultaneously. Speeding up the processor by itself could only solve
the problem to a limited extent, so processors started offering multiple cores, which were
essentially separate processors that could work on different tasks simultaneously.
While multicore processors offered a solution, the bigger problem was none of
these multicore processors could work to their full potential unless the software took
advantage of these multiple cores. This forced programmers to write code that could run
at the same time known as concurrent programming. Writing code was hard enough,
and writing additional code to make different parts of a program run at the same time
was often confusing and difficult. As a result, most programmers didn’t bother, which
meant their software wouldn’t take full advantage of multicore processors.
To solve the problem of managing code to run in parallel, Apple created a solution
called Grand Central Dispatch (GCD), which provides support for concurrent code
execution on multicore hardware in iOS and macOS. Instead of forcing developers to
63
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_4
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
worry about the details of managing code to run in parallel, known as threads, Grand
Central Dispatch lets developers simply identify which chunks of code to run at the same
time and Grand Central Dispatch takes care of the actual details to do so.
In the old days, software was mostly self-contained in that it didn’t need to rely
on anything else. Today, software often depends on external factors that are largely
unpredictable such as waiting for a file to load or a network connection to complete.
While waiting, the entire program is effectively paused. If this pause is too long, it makes
the program look like it’s frozen and unresponsive.
That’s why you want to use Grand Central Dispatch to allow multiple threads of
execution within a program. That way, even if a single thread is stuck waiting for a
specific event, the other threads can keep going. By using Grand Central Dispatch, your
apps should never feel slow and unresponsive to the user.
Note Grand Central Dispatch works identically in both iOS and macOS.
Understanding Threads
To fully understand the advantage of Grand Central Dispatch, it’s important to see how
delays can ruin the responsiveness of an app in the eyes of a user. To do this, we’ll see
what happens when a process runs for too long, essentially forcing the entire app to wait
until the process finishes. During this time, the app appears frozen and unresponsive.
We’ll deliberately create an app that will lock up the user interface. To see how to
create an app that appears unresponsive, follow these steps:
1. Create a new iOS App project, make sure it uses SwiftUI, and give
it a descriptive name (such as ThreadApp).
2. Click the ContentView file in the Navigator pane. The Editor pane
displays the contents of the ContentView file.
4. Create a VStack inside the var body: some View and type the
following to add a Button, TextEditor, Slider, and Text like this:
}
TextEditor(text: $results)
Slider(value: $sliderValue)
Text("Message = \(message)")
}
}
This creates a Button at the top of the screen, a Text Editor in the
middle of the screen, and a Slider at the bottom of the screen.
5. Add the following code above the last curly bracket of the struct
ContentView:
Button("Click Me") {
let startTime = NSDate()
let fetchedData = fetchSomethingFromServer()
let processedData = processData(fetchedData)
let firstResult = calculateFirstResult(processedData)
let secondResult = calculateSecondResult(processedData)
let resultsSummary =
"First: [\(firstResult)]\nSecond: [\(secondResult)]"
results = resultsSummary
let endTime = NSDate()
message = "Completed in \(endTime.timeIntervalSince(startTime
as Date)) seconds"
}
import SwiftUI
}
}
67
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
8. Drag the slider left and right. Notice that you can easily drag the
slider back and forth.
9. Click the Click Me Button at the top of the screen. Notice that
the button dims. Try dragging the slider back and forth. Because
the app is running a process, the user interface now appears
frozen and unresponsive for about ten seconds. After the process
completes, it displays the results on the screen as shown in
Figure 4-1.
10. Drag the slider left and right. Notice that the slider now easily
moves once again.
68
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
This example lets you see how a process can freeze an app and make it appear
unresponsive even though the app is still running. If you submit an app that freezes its
user interface periodically, Apple will reject it from the App Store.
Most modern operating systems (including iOS) support multiple threads of
execution. If there’s just one processor core, the operating system will switch between all
executing threads, much like it switches between all executing processes. If more than
one core is available, the threads will be distributed among them.
All threads in a process share the same executable program code and the same
global data. Each thread can also have some data that is exclusive to the thread through
a special structure called a mutex (short for mutual exclusion) or a lock. Such a lock
ensures that a particular chunk of code can’t be run by multiple threads at once, which
can keep multiple threads from accessing the same data simultaneously.
When writing code, you need to make sure your code is thread-safe. As a
general rule, any code that controls the user interface is not thread-safe. Because
threads increase the chance of multiple processes interfering with each other, most
programmers don’t use threads directly. That’s why Apple created Grand Central
Dispatch (GCD) to help make concurrent programming easier and safer.
69
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
to a GCD queue will always be started in the order they were placed in the queue. That
said, they may not always finish in the same order, since a GCD queue will automatically
distribute its work among multiple threads, if possible.
To use GCD, we first need to create a queue using the DispatchQueue
keyword such as
Once we’ve created a queue, we need to define the code to run in that queue. This
code runs in a closure and can run synchronously or asynchronously. An asynchronous
queue runs whenever the processor has time to complete it. A synchronous queue runs
and must complete before any other code can run. In general, asynchronous queues are
most useful when you want to run multiple tasks at the same time but the order and time
that they complete isn’t important.
To make a queue run, we have to define whether it’s asynchronous or synchronous
and specify the code to run in a closure like this:
To see how asynchronous queues can work, but may complete at different,
unpredictable times, follow these steps:
import UIKit
70
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
print("Program stopped")
This code creates three queues and then runs tasks in each queue
that simply prints the name of the queue. Finally, the code ends by
printing “Program stopped”.
3. Click the Run button. Notice that the debug area displays the
output of the code such as
queue1
Program stopped
queue2
queue3
4. Click the Run button to run the program again. Notice that the
output may change such as
Program stopped
queue1
queue3
queue2
Even though the code is identical, asynchronous queues may complete at different
times. Each time you click the Run button, you’ll likely see a different result. While you
can have multiple tasks running on different asynchronous queues, you cannot predict
when any given queue will complete its task.
71
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
To see how synchronous queues work in the exact same order every time, modify the
playground code to change all async calls to sync as follows:
import UIKit
print("Program stopped")
No matter how many times you run this code, the output will always be predictable
and in order like this:
queue1
queue2
queue3
Program stopped
The only way you can change the order of the output is to change the position of the
queues such as putting queue3 ahead of queue1. Since synchronous queues are little
different than not using concurrency at all, asynchronous queues are used most often as
long as the order of task completion isn’t important.
Now that we know how GCD can run multiple tasks at the same time, we need to use
GCD to fix the unresponsive user interface of our ThreadApp. First, we need to identify
which code is causing the delay. In our example, it’s this code inside the Button:
72
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
Logically, it would seem like we could simply wrap this code inside a closure and run
it in a queue. However, look out for this line:
results = resultsSummary
This line updates the results State variable that appears inside the Text Editor. As
a general rule, updating the user interface in a queue is not thread-safe, which means
trying to update the user interface in a queue will cause an error. To see what happens
when you try to update the user interface within a queue, follow these steps:
Button("Click Me") {
let startTime = NSDate()
let queue = DispatchQueue.global(qos: .default)
queue.async {
let fetchedData = fetchSomethingFromServer()
let processedData = processData(fetchedData)
let firstResult = calculateFirstResult
(processedData)
let secondResult = calculateSecondResult
(processedData)
let resultsSummary =
"First: [\(firstResult)]\nSecond:
[\(secondResult)]"
73
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
results = resultsSummary
let endTime = NSDate()
message = "Completed in \(endTime.
timeIntervalSince(startTime as Date)) seconds"
}
}
import SwiftUI
75
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
}
}
6. Drag the slider left and right. Notice that even though the app is
processing, the user interface is still responsive. Eventually, the
app finishes its processing and displays its results in the text view,
but during that entire time, the user could still interact with the
interface.
76
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
To create a dispatch group, we just need to create a DispatchGroup object like this:
Then we run each queue inside this dispatch group like this:
queue.async(group: group) {
firstResult = calculateFirstResult(processedData)
}
To run a final closure after all other closures have finished, we create a group.notify
queue like this:
group.notify(queue: queue) {
let resultsSummary = "First: [\(firstResult!)]\nSecond:
[\(secondResult!)]"
let endTime = Date()
results = resultsSummary
message = "Completed in \(endTime.timeIntervalSince(startTime))
seconds"
}
One final difference is that the group.notify and the queue.async queues need to
access the firstResult and secondResult variables, so we need to declare them outside of
both queues like this:
VStack {
HStack {
Button("Click Me") {
4. Add a Spacer() after the last curly bracket of the “Click Me” Button.
77
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
Button("Dispatch Groups") {
let startTime = Date()
let queue = DispatchQueue.global(qos: .default)
queue.async {
let fetchedData = self.fetchSomethingFromServer()
let processedData = self.processData(fetchedData)
var firstResult: String!
var secondResult: String!
let group = DispatchGroup()
queue.async(group: group) {
firstResult = self.calculateFirstResult(processedData)
}
queue.async(group: group) {
secondResult = self.calculateSecondResult
(processedData)
}
group.notify(queue: queue) {
let resultsSummary = "First:
[\(firstResult!)]\nSecond:
[\(secondResult!)]"
let endTime = Date()
results = resultsSummary
message = "Completed in \(endTime.
timeIntervalSince(startTime)) seconds"
}
}
}
import SwiftUI
78
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
queue.async(group: group) {
firstResult = self.calculateFirstResult
(processedData)
}
queue.async(group: group) {
secondResult = self.calculateSecond
Result(processedData)
}
group.notify(queue: queue) {
let resultsSummary = "First:
[\(firstResult!)]\nSecond:
[\(secondResult!)]"
let endTime = Date()
results = resultsSummary
message = "Completed in \(endTime.
timeIntervalSince(startTime)) seconds"
}
}
}
}
TextEditor(text: $results)
Slider(value: $sliderValue)
Text("Message = \(message)")
}
}
80
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
}
}
7. Click the “Click Me” Button. Notice that when the process
completes, the Text view at the bottom of the screen displays a
message such as
81
Chapter 4 Multithreaded Programming Using Grand Central Dispatch
What was once a ten-second operation now takes just seven seconds, thanks to the
fact that we’re running both of the calculations simultaneously. Obviously, our contrived
example gets the maximum effect because these two “calculations” don’t actually do
anything but cause the thread they’re running on to sleep. In a real app, the speedup
would depend on what sort of work is being done and what CPU is available.
Summary
Grand Central Dispatch (GCD) is a way to run multiple parts of your code separately.
You can do this using threads, but manipulating individual threads can be troublesome
and error-prone. Instead of working with threads, you can use GCD instead, which takes
care of the details needed to start, run, and stop different threads safely.
As you can see, GCD can help speed up bottlenecks in your code where a single
process might take a long time to complete, which can make your app seem to freeze and
be unresponsive. By using GCD at points in your app where speed is essential or where
your app lags in responses to the user, you can easily provide a better user experience,
even in situations where you can’t improve the actual performance.
82
CHAPTER 5
Understanding
Concurrency
Most computer programs run instructions sequentially. In many cases, that’s fine
since computers can process instructions so rapidly that even calculating complex
mathematical equations takes so little time that a computer program barely seems to
slow down at all.
While the processors in today’s iPhone and iPad models rival the processors used
in desktop and laptop PCs, there’s still a problem running instructions sequentially no
matter how fast they can be processed. Suppose a program needs to retrieve data from a
server over a network. While waiting for the requested data, a program essentially halts
completely because the code waiting for data holds up the entire program. Only until it
finishes receiving the data it requested can the program continue executing additional
instructions as shown in Figure 5-1.
83
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_5
Chapter 5 Understanding Concurrency
Figure 5-2. Concurrent processing lets two or more tasks run simultaneously
import UIKit
enum Response {
case success
case failure
}
84
Chapter 5 Understanding Concurrency
if randomNumber == 0 {
completion(.failure(.powerOutage))
return
}
completion(.success(.success))
}
3. Move the mouse pointer to the left of the last line and click the
Run button.
85
Chapter 5 Understanding Concurrency
print("Starting task")
Thread.sleep(forTimeInterval: 2)
if randomNumber == 0 {
completion(.failure(.powerOutage))
return
}
completion(.success(.success))
}
If you fail to include all possible ways to exit the function, the function could run
endlessly, freezing your program. To make concurrency easier to read and understand,
Swift uses the async/await keywords. The async keyword defines a function to run
asynchronously. Then to call an asynchronous function, you use the await keyword.
To see how to use async/await, follow these steps:
import UIKit
enum Response {
case success
case failure
}
86
Chapter 5 Understanding Concurrency
if randomNumber == 0 {
throw BigError.powerOutage
}
return Response.success
}
callFunction()
let endTime = NSDate()
print("Completed in \(endTime.timeIntervalSince(startTime as
Date)) seconds")
3. Move the mouse pointer to the left of the last line and click the
Run button. The result will be similar to the following:
Starting task
Ending task
Completed in 0.010614991188049316 seconds
Result = success
87
Chapter 5 Understanding Concurrency
enum Response {
case success
}
5. Add a VStack underneath the var body: some View line that
includes a Button, Spacer(), Slider, and Text view like this:
VStack {
Button("Click Me") {
88
Chapter 5 Understanding Concurrency
}
Spacer()
Slider(value: $sliderValue)
Text("Message = \(message)")
}
The most important part of this code is that it halts the program
from running for 20 seconds using Thread.sleep(forTimeInterval:
20). This demonstrates the problem of a time-consuming task that
will freeze the user interface.
7. Add the following two functions above the last curly bracket in the
struct ContentView: View like this:
func callFunction() {
Task(priority: .high) {
do {
_ = try await doSomething()
} catch {
//
}
}
}
89
Chapter 5 Understanding Concurrency
import SwiftUI
func callFunction() {
Task(priority: .high) {
90
Chapter 5 Understanding Concurrency
do {
_ = try await doSomething()
} catch {
//
}
}
}
9. Click the Click Me Button. Try to drag the Slider left and right.
Notice that the user interface appears frozen and unresponsive
until the Thread.sleep(forTimeInterval: 20) ends after 20 seconds.
After 20 seconds has passed, you’ll finally be able to drag the
Slider back and forth.
The Thread.sleep function mimics a task that takes 20 seconds. During this time
period, the user interface remains frozen. To see how concurrency can avoid this
problem, follow these steps:
91
Chapter 5 Understanding Concurrency
enum Response {
case success
}
5. Add a VStack underneath the var body: some View line that
includes a Button, Spacer(), Slider, and Text view like this:
VStack {
Button("Click Me") {
}
Spacer()
Text("Task message = \(taskMessage)")
Slider(value: $sliderValue)
Text("Message = \(message)")
}
7. Add the following two functions above the last curly bracket in the
struct ContentView: View like this:
92
Chapter 5 Understanding Concurrency
func callFunction() {
Task(priority: .high) {
do {
_ = try await doSomething()
} catch {
//
}
}
}
Note To make it easier to read and write large numbers, Swift can
optionally use the underscore character to separate three digits of a large
number. So 20_000_000_000 is equal to 20,000,000,000.
import SwiftUI
enum Response {
case success
}
93
Chapter 5 Understanding Concurrency
// Thread.sleep(forTimeInterval: 20)
callFunction()
let endTime = NSDate()
message = "Completed in \(endTime.
timeIntervalSince(startTime as Date)) seconds."
}
Spacer()
Text("Task message = \(taskMessage)")
Slider(value: $sliderValue)
Text("Message = \(message)")
}
}
func callFunction() {
Task(priority: .high) {
do {
_ = try await doSomething()
} catch {
//
}
}
}
94
Chapter 5 Understanding Concurrency
Summary
The main idea behind concurrency is that you never want the app to be running code
that freezes the user interface and makes it appear unresponsive as if your app had
crashed. One way to use concurrency is to rely on completion handlers to run after a
function is finished. However, this can be hard to read and understand.
In the latest version of Swift, the new way to define concurrency is to use the async/
await keywords. The await keyword is used to call an asynchronous function that has
been defined by the async keyword. Thus, the async/await keywords work together to
allow code to run asynchronously, so the user interface is never affected.
95
CHAPTER 6
Understanding Data
Persistence
All but the simplest apps need to store data. The Stocks app lets users track their favorite
stocks, so it needs to store the list of stocks to follow that the user chose. Each time
the user launches the Stocks app again, it displays the list of stocks the user inputted
previously. If the user adds or deletes stocks from this list, the Stocks app needs to store
this updated list and retrieve it again the next time the user loads the Stocks app.
Other types of apps may have various settings that allow users to customize an app
such as defining its background color or sounds to play when certain events occur such
as one sound to represent a text message received and another sound to represent a
voicemail someone left you.
Storing and retrieving data when the user starts and stops an app is known as data
persistence. Three common ways to store and retrieve data in an iOS app includes
• UserDefaults
97
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_6
Chapter 6 Understanding Data Persistence
Reading and writing data to a file can be useful to store longer amounts of data such
as several lines of text. However, reading and writing to a file can be slow if you have lots
of data, which requires code to search through the entire file to find specific data.
Core Data lets you store different types of data in groups called entities, which are
similar to tables or records in a database. If you need to store large amounts of diverse
data, use Core Data over the other two options for storing data.
To store data using UserDefaults, you need to define a key and the data you want
to store in this format where “dataToSave” represents an actual value and “keyString”
represents a unique string:
The set command saves the key and its associated data. To retrieve previously saved
data, you need to know the key value and the type of data stored such as an integer,
Boolean, or double data type. Knowing the data type you want to retrieve, you can use
one of the following:
98
Chapter 6 Understanding Data Persistence
1. Create a new iOS App project, make sure it uses SwiftUI, and give
it a descriptive name such as UserDefaultsApp.
4. Add a VStack inside the var body: some View to define a spacing of
25. Make sure the VStack has nothing in it:
}
.padding()
Slider(value: $mySlider)
}
.padding()
Slider(value: $mySlider)
Button("Save data") {
UserDefaults.standard.set(myText, forKey: "Text")
UserDefaults.standard.set(myToggle, forKey: "Toggle")
UserDefaults.standard.set(mySlider, forKey: "Slider")
}
Button("Clear data") {
myText = ""
myToggle = true
mySlider = 0.0
}
Button("Retrieve data") {
myText = UserDefaults.standard.string
(forKey: "Text") ?? ""
myToggle = UserDefaults.standard.bool
(forKey: "Toggle")
mySlider = UserDefaults.standard.double
(forKey: "Slider")
}
100
Chapter 6 Understanding Data Persistence
}
.padding()
}
import SwiftUI
Slider(value: $mySlider)
Button("Save data") {
UserDefaults.standard.set(myText, forKey: "Text")
UserDefaults.standard.set(myToggle, forKey:
"Toggle")
UserDefaults.standard.set(mySlider, forKey:
"Slider")
}
Button("Clear data") {
myText = ""
myToggle = true
mySlider = 0.0
}
101
Chapter 6 Understanding Data Persistence
Button("Retrieve data") {
myText = UserDefaults.standard.string(forKey:
"Text") ?? ""
myToggle = UserDefaults.standard.bool(forKey:
"Toggle")
mySlider = UserDefaults.standard.double(forKey:
"Slider")
}
}
.padding()
}
}
11. Click the Save data Button to save the current state of the
TextField, Toggle, and Slider in UserDefaults.
12. Click the Clear data Button to reset the user interface.
13. Click the Retrieve data Button to retrieve the state of the TextField,
Toggle, and Slider.
102
Chapter 6 Understanding Data Persistence
let fm = FileManager.default
Next, we need to define a location for the file, which is the document directory in the
home folder:
Finally, we need to create a file name (such as “file.txt”) to store data like this:
Once we’ve stored text in a file, we can retrieve it by using the FileManager again and
look for the file in the document directory in the home folder. To see how to write data to
a file and then read it back again, follow these steps:
103
Chapter 6 Understanding Data Persistence
} .padding()
TextEditor(text: $displayText)
.foregroundColor(Color.purple)
.font(.custom("Courier", size: 40))
.border(Color.green, width: 5)
}
}
let fm = FileManager.default
let urls = fm.urls(for: .documentDirectory, in:
.userDomainMask)
let url = urls.last?.appendingPathComponent
("file.txt")
do {
let fileContent = try String(contentsOf: url!,
encoding: String.Encoding.utf8)
displayText = fileContent
} catch {
print("File reading error")
}
}
} .padding()
TextEditor(text: $displayText)
.foregroundColor(Color.purple)
.font(.custom("Courier", size: 40))
.border(Color.green, width: 5)
}
}
import SwiftUI
105
Chapter 6 Understanding Data Persistence
let fm = FileManager.default
let urls = fm.urls(for: .documentDirectory,
in: .userDomainMask)
let url = urls.last?.appendingPathComponent
("file.txt")
do {
try createText.write(to: url!, atomically:
true, encoding: String.Encoding.utf8)
} catch {
print("File writing error")
}
}
Spacer()
Button("Read File") {
let fm = FileManager.default
let urls = fm.urls(for: .documentDirectory,
in: .userDomainMask)
let url = urls.last?.appendingPathComponent
("file.txt")
do {
let fileContent = try String(contentsOf:
url!, encoding: String.Encoding.utf8)
displayText = fileContent
} catch {
print("File reading error")
}
}
} .padding()
TextEditor(text: $displayText)
.foregroundColor(Color.purple)
.font(.custom("Courier", size: 40))
.border(Color.green, width: 5)
}
}
}
106
Chapter 6 Understanding Data Persistence
8. Click the Write File button. This saves the text in a file.
9. Click the Read File button. Whatever text you saved in the file now
appears in the bottom TextEditor.
107
Chapter 6 Understanding Data Persistence
Figure 6-1. Core Data stores data in attributes, grouped together to represent a
single entity
There are two ways to add Core Data to a project. First, you can add Core Data after
you’ve created a project. Second, you can include Core Data at the time you create a
project. Let’s look at both methods to see how they work.
1. Choose File ➤ New ➤ Project and create a new iOS App project.
Give this project a name such as CoreDataApp. Make sure that the
Core Data check box is not selected.
4. Scroll down and click the Data Model under the Core Data
category as shown in Figure 6-2.
5. Click the Next button. A dialog appears, letting you choose a name
for the Core Data file.
6. Choose a name for your Core Data file, such as DataModel, and
click the Create button. Xcode displays your Core Data file in the
Navigator pane.
Once you’ve created a Core Data file, the next step is to define one or more entities.
An entity consists of multiple attributes such as holding a name, a price, an age, or a
phone number. Think of an entity like a record in a database and attributes like fields as
shown in Figure 6-3.
109
Chapter 6 Understanding Data Persistence
If you know ahead of time that you want to use Core Data, it’s easier to create a Core
Data file when you create a new project. This lets Xcode add the necessary Swift code to
access Core Data.
To create a new project that includes a Core Data file, follow these steps:
2. Click the iOS category and click the App. Then click the Next
button. Another dialog appears.
3. Click in the Product Name text field and give it a name. (When
creating your own projects, choose any name you wish.)
4. Make sure the Use Core Data check box is selected as shown in
Figure 6-4.
110
Chapter 6 Understanding Data Persistence
Figure 6-4. Selecting the Use Core Data check box when creating a new project
5. Click the Next button and then click the Create button.
6. Click the Persistence file in the Navigator pane. When you create a
new project using Core Data, Xcode adds the following code in the
Persistence file:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
111
Chapter 6 Understanding Data Persistence
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the
error appropriately.
// fatalError() causes the application to generate a
crash log and terminate. You should not use this
function in a shipping application, although it may
be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.
userInfo)")
}
return result
}()
/*
112
Chapter 6 Understanding Data Persistence
113
Chapter 6 Understanding Data Persistence
To see how to add Core Data to an existing project, follow these steps:
2. Click the Core Data file in the Navigator pane. Xcode displays a
data editor as shown in Figure 6-5.
Figure 6-5. The data editor lets you view and edit entities and attributes
114
Chapter 6 Understanding Data Persistence
3. Click the Add Entity icon, or choose Editor ➤ Add Entity. Xcode
displays an Entity under the ENTITIES category.
4. Click this Entity to select it and press ENTER to highlight the
entire name.
5. Type Animal and press ENTER. Entity names must always begin
with an uppercase letter such as Item, Person, or Vehicle.
After you’ve created at least one entity, you’ll need to add one or more attributes to
hold data. An attribute consists of a descriptive name (typed in lowercase) and the type
of data the attribute will hold such as a string, integer, or date.
To define an attribute in an entity, follow these steps:
1. Click the Core Data file in the Navigator pane. Xcode displays the
data model editor (see Figure 6-5).
2. Click the entity that you want to modify. Xcode displays an
Attributes category as shown in Figure 6-6.
3. Click the Add Attribute button in the bottom of the Xcode window
or underneath the Attribute column, or choose Editor ➤ Add
Attribute. Xcode displays an attribute and a type as shown in
Figure 6-7.
4. Type name and press ENTER. All attribute names must use
lowercase letters.
5. Click in the Type popup menu to display a list of data types the
attribute can store as shown in Figure 6-8.
6. Choose String.
7. Click the Add Attribute button in the bottom of the Xcode window
or underneath the Attribute column, or choose Editor ➤ Add
Attribute. Xcode displays an attribute and a type (see Figure 6-7).
9. Click in the Type popup menu and choose String. The two
attributes and one entity should look like Figure 6-9.
Figure 6-9. Defining a name and price attribute for an Item entity
117
Chapter 6 Understanding Data Persistence
Once you’ve added a Core Data model to a project and customized this data model
to define the entities and attributes of each entity, it’s time to add a separate Swift file to
add, retrieve, and delete data from Core Data by following these steps:
1. Make sure your Xcode project has a Core Data data model file.
118
Chapter 6 Understanding Data Persistence
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name:
"DataModel")
persistentContainer.loadPersistentStores { (description,
error) in
if let error = error {
fatalError("Core Data failed to initialize \
(error.localizedDescription)")
}
}
}
This class simply loads the Core Data file. In this case, the Core
Data file is named DataModel, so if you gave your Core Data
model file a different name, replace “DataModel” with the name
of your model file instead.
The preceding code simply loads Core Data, but we still need to
write functions to add, delete, and retrieve data from Core Data.
7. Add the following function to save data to the Core Data model:
do {
try persistentContainer.viewContext.save()
119
Chapter 6 Understanding Data Persistence
print("Pet saved!")
} catch {
print("Failed to save movie \(error)")
}
do {
return try persistentContainer.viewContext.
fetch(fetchRequest)
} catch {
return []
}
}
This function retrieves all data from the Animal entity defined by
the Core Data model file.
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error.
localizedDescription)")
}
}
120
Chapter 6 Understanding Data Persistence
This function accepts a single entity and attempts to delete it. The entire
CoreDataManager file should look like this:
import Foundation
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Core Data failed to initialize \(error.
localizedDescription)")
}
}
}
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error.localizedDescription)")
}
}
do {
return try persistentContainer.viewContext.fetch(fetchRequest)
} catch {
return []
}
}
121
Chapter 6 Understanding Data Persistence
do {
try persistentContainer.viewContext.save()
print("Pet saved!")
} catch {
print("Failed to save movie \(error)")
}
}
}
The final step is to design the user interface that will let users type in two strings (for
name and breed), save the data, display the data in a list, and then delete the selected
data. To design a user interface, follow these steps:
3. Add the following underneath the var body: some View line:
VStack {
TextField("Enter pet name", text: $petName)
.textFieldStyle(RoundedBorderTextFieldStyle())
122
Chapter 6 Understanding Data Persistence
Button("Save") {
coreDM.savePet(name: petName, breed: petBreed)
displayPets()
petName = ""
petBreed = ""
}
}.padding()
.onAppear(perform: {
displayPets()
})
4. Add the displayPets() function just above the last curly bracket of
the var body: some View:
func displayPets() {
petArray = coreDM.getAllPets()
}
This function retrieves all data from Core Data and stores it in the
petArray variable.
List {
ForEach(petArray, id: \.self) { pet in
VStack {
Text(pet.name ?? "")
Text(pet.breed ?? "")
123
Chapter 6 Understanding Data Persistence
}
}.onDelete(perform: { indexSet in
indexSet.forEach { index in
let pet = petArray[index]
coreDM.deletePet(animal: pet)
displayPets()
}
})
}
Spacer()
This creates a List that uses a ForEach loop to retrieve each chunk
of data from Core Data that gets displayed in a separate VStack.
The .onDelete modifier lets users swipe to the left on a chunk of
data to display a Delete button. If the user taps this Delete button,
the deletePet function gets called to delete the selected data. Then
it calls the displayPets() function again. Finally, there’s a Spacer at
the bottom of the List to push everything up.
This gives the preview of the ContentView file access to the CoreDataManager file.
The complete ContentView file should look like this:
import SwiftUI
List {
ForEach(petArray, id: \.self) { pet in
VStack {
Text(pet.name ?? "")
Text(pet.breed ?? "")
}
}.onDelete(perform: { indexSet in
indexSet.forEach { index in
let pet = petArray[index]
coreDM.deletePet(animal: pet)
displayPets()
}
})
}
Spacer()
}.padding()
.onAppear(perform: {
displayPets()
})
}
125
Chapter 6 Understanding Data Persistence
func displayPets() {
petArray = coreDM.getAllPets()
}
The preceding code displays two TextFields and a Button on the screen where users
can type a name and a breed type in the two TextFields. Then the user can click the
Button to save the data, which appears in a List underneath the Button. Finally, users
can swipe left on the displayed data to delete it. To see how this project works, follow
these steps:
2. Click in the top TextField and type a name for a pet such as Fido.
3. Click in the second TextField and type a breed for the pet such
as Collie.
4. Click the Save Button. Notice that the data you typed in each
TextField now appears underneath the Save Button.
5. Repeat steps 2–4 except type a different name and breed. Make
sure you have at least two different chunks of data (name and
breed) as shown in Figure 6-11.
126
Chapter 6 Understanding Data Persistence
7. Click the Delete button. Notice that the selected data now
disappears.
8. Repeat steps 6 and 7 on each chunk of data to delete it until all
data is gone.
127
Chapter 6 Understanding Data Persistence
Summary
Storing data and retrieving it again can be important for many apps. For storing simple
app settings, store data in UserDefaults that your app can load to retrieve any settings the
user chose.
To store longer amounts of data, especially text, save and retrieve data in files. Files
can hold multiple lines of text, so they can be handy for large amounts of data.
If you need to store larger amounts of related data, use Core Data to save this
information. Whether you store data in UserDefaults, files, or Core Data, you can always
retrieve that data again, so an app can display that data automatically without requiring
the user to manually load data each time.
128
CHAPTER 7
For simple apps, you might only need a single structure to define your user interface.
However, the more sophisticated your app gets, the more Swift code you’ll need to write.
Eventually, this can create a cluttered structure that can be hard to read and understand.
The solution is to divide a user interface into multiple structures that you can either
store within the same file or in separate files. However, once you separate your user
interface into separate structures, those separate structures no longer have access to any
State variables declared by the main structure. To get around this problem, we need to
use Bindings.
129
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_7
Chapter 7 Sharing Data Between Structures
Bindings essentially declare a variable that holds the same data type as a State
variable that’s defined in a different structure. Now the structure containing the State
variable can pass it to a Binding variable defined in the other structure.
To see how to use Bindings, follow these steps:
4. Hold down the Command key and click the HStack. A popup
menu appears as shown in Figure 7-1.
130
Chapter 7 Sharing Data Between Structures
131
Chapter 7 Sharing Data Between Structures
132
Chapter 7 Sharing Data Between Structures
DisplayTextField(newVariable: $message)
import SwiftUI
Text("Send a greeting:")
TextField("Type a message here", text: $newVariable)
}
}
}
10. Click in the TextField of your user interface and type a name.
Notice that as you type, the name appears in the Text view
displaying “Hello, ”.
Figure 7-3. A State variable gets passed to another structure’s Binding variable
134
Chapter 7 Sharing Data Between Structures
Creating separate structures and storing them in the same file can work, but the
more structures you create, the more cluttered that single file will get. A better solution is
to store a structure in a separate file altogether. To see how to use Binding variables with
a structure stored in a separate file, follow these steps:
135
Chapter 7 Sharing Data Between Structures
import SwiftUI
import SwiftUI
SwiftUIView(newVariable: $message)
}
}
}
136
Chapter 7 Sharing Data Between Structures
8. Click in the TextField and type a name. The Text view displays the
name as you type.
As you can see, you can store a structure as either part of the same file or saved in a
separate file. Either method works, but if you store structures in separate files, make sure
you also pass in data to the Binding variable within the preview structure like this:
SwiftUIView(newVariable: .constant(""))
Because the Binding variable inside the SwiftUIView structure is a String data type,
the preview structure must give it a String value as well such as an empty string.
137
Chapter 7 Sharing Data Between Structures
Within this ObservableObject, you must define one or more variables using the @
Published keyword like this:
Then you can create a StateObject and store data in that StateObject’s properties
like this:
Finally, you can pass the StateObject to another structure that uses an
ObservedObject like this:
3. Add the following class just below the last curly bracket:
138
Chapter 7 Sharing Data Between Structures
HStack {
Text("Breed:")
TextField("Type a breed here", text:
$creature.breed)
}
139
Chapter 7 Sharing Data Between Structures
HStack {
Text("Age:")
Slider(value: $creature.age, in: 0...20)
}
HStack {
Text("Weight:")
Slider(value: $creature.weight, in: 0...200)
}
}
}
}
import SwiftUI
HStack {
Text("Breed:")
TextField("Type a breed here", text:
$creature.breed)
}
HStack {
Text("Age:")
Slider(value: $creature.age, in: 0...20)
}
HStack {
Text("Weight:")
Slider(value: $creature.weight, in: 0...200)
}
}
}
}
141
Chapter 7 Sharing Data Between Structures
10. Drag the bottom Slider to define a weight. All your chosen data
appears at the top of the user interface as shown in Figure 7-5.
142
Chapter 7 Sharing Data Between Structures
Any time you want to share an object with another structure, you must pass it to that
other structure’s ObservedObject. A much simpler method is to use EnvironmentObject
instead.
When you use EnvironmentObject, you no longer need to pass a StateObject to each
structure. First, you must create a class as an ObservableObject like this:
Next, you must create an object based on this class using StateObject like this:
143
Chapter 7 Sharing Data Between Structures
Within the same structure where you declare a StateObject, you must also define an
.environmentObject to share using the StateObject variable like this:
.environmentObject(cat)
Finally, each structure can access this StateObject by declaring its own
EnvironmentObject variable like this:
You must also use the .environmentObject modifier to identify which class to
use such as
.environmentObject(AnimalModel())
3. Add the following class just below the last curly bracket:
144
Chapter 7 Sharing Data Between Structures
Text("Weight: \(cat.weight)")
DisplayTextField()
}.environmentObject(cat)
}
}
145
Chapter 7 Sharing Data Between Structures
HStack {
Text("Breed:")
TextField("Type a breed here", text:
$creature.breed)
}
HStack {
Text("Age:")
Slider(value: $creature.age, in: 0...20)
}
HStack {
Text("Weight:")
Slider(value: $creature.weight, in: 0...200)
}
}.environmentObject(AnimalModel())
}
}
import SwiftUI
146
Chapter 7 Sharing Data Between Structures
}.environmentObject(cat)
}
}
HStack {
Text("Breed:")
TextField("Type a breed here", text:
$creature.breed)
}
HStack {
Text("Age:")
Slider(value: $creature.age, in: 0...20)
}
HStack {
Text("Weight:")
Slider(value: $creature.weight, in: 0...200)
}
}.environmentObject(AnimalModel())
}
}
147
Chapter 7 Sharing Data Between Structures
11. Drag the bottom Slider to define a weight. All your chosen data
appears at the top of the user interface (see Figure 7-5).
Summary
Normally, data stays within a structure when you use State variables. To share data
between structures, create a State variable in one structure and then use Binding
variables in other structures. The State variable typically contains an initial value, while a
Binding variable simply defines a variable of the same data type as the State variable.
State and Binding variables can hold individual data types such as integers (Int),
decimal numbers (Double or Float), or text (String). If you want to share entire objects
that can consist of multiple properties, you need to use StateObject (instead of a State
variable) and ObservedObject (instead of a Binding variable). In addition, you need to
create a class that’s an ObservableObject.
When you use ObservedObjects, you must explicitly pass an object to another
structure. To avoid this, you can use EnvironmentObject instead. First, you must create
a StateObject within a structure and then use the .environmentObject modifier to share
that StateObject.
Within each separate structure, you must declare an EnvironmentObject and also
use the .environmentObject modifier to identify the class of the StateObject.
By using State, Binding, StateObject, ObservedObject, and EnvironmentObject, you
can easily share data between structures whether those structures are stored in the same
file or in a separate file.
148
CHAPTER 8
Translating with
Localization
Most people create apps in their native language, but if you translate your app into
other languages, you could sell and distribute your app to other parts of the world.
Translating text from one language to another requires an experienced translator, but
from a technical point of view, how do you create a single app and let it display different
languages?
The hard way is to create separate apps for each language. The easy way is to create
a single app and use something called localization. The idea behind localization is that
you create your app once, then instead of typing text to appear in the app, you use a
special localized string that represents the text to display.
Now you store different text in separate files stored in a localization folder.
Depending on the language the user’s iOS device uses, your app then yanks out the
correct file that matches the user’s language. So if you wanted your app to display
text in English, Arabic, and Russian, you would create one file containing English
words to appear in your app, a second file containing Arabic words that represent
equivalent English text, and a third file containing Russian words that represent
equivalent text.
If the user switches the settings on their iOS device to display text in Russian,
then your app will automatically replace all text with Russian text. If the user
switches to Arabic, then your app will automatically replace all text with Arabic text.
By creating text in different languages, you can create an app that adapts to different
languages.
149
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_8
Chapter 8 Translating with Localization
An app that supports localization will likely need to replace the following to adjust to
different languages:
• Images
Besides changing text and images that appear on the user interface, localization
also needs to adjust the app’s user interface as well. For example, buttons that may look
perfect when displayed in English may look too small when displaying equivalent text in
German or may look too big when displaying equivalent text in Chinese. When designing
a user interface, you need to consider the size of displayed text and make sure your user
interface adapts to different size text.
Besides the size and text itself, you must also consider how different cultures and
regions display dates and numbers. In some areas, people separate decimal numbers
with a period such as 3.1415, while in others, people separate decimal numbers with a
comma such as 3,1415.
Likewise, some areas display dates with the month first followed by the day and the
year like June 4, 2019, while other places display dates differently such as 4 June 2019. So
not only must your app display the proper text adjusted on the user interface to appear
correctly, but your app must also recognize different number and date formats.
150
Chapter 8 Translating with Localization
2. Click the project name at the top of the Navigator pane as shown
in Figure 8-1. Xcode displays information about the project in the
middle pane.
151
Chapter 8 Translating with Localization
4. Click Info at the top of the middle Xcode pane. Xcode displays an
Info pane as shown in Figure 8-3.
152
Chapter 8 Translating with Localization
153
Chapter 8 Translating with Localization
9. Scroll down and select Strings File under the Resource category as
shown in Figure 8-5. Then click Next.
10. Type Localizable to name the Strings File. The file must be
named Localizable for Xcode to recognize it as a file that contains
translations.
11. Click the Localizable file in the Navigator pane. The contents of
the Localizable file appear in the middle Xcode pane.
154
Chapter 8 Translating with Localization
155
Chapter 8 Translating with Localization
13. Click French. Xcode defines the Localizable file to hold French.
14. Click the Localizable file in the Navigator pane. The Inspector
pane displays check boxes for each language you previously
selected as shown in Figure 8-8.
Figure 8-8. The Inspector pane displaying multiple language check boxes
156
Chapter 8 Translating with Localization
15. Select the English, French, and Spanish check boxes. When all
check boxes are selected, Xcode creates a Localizable file for
French, Spanish, and English as shown in Figure 8-9.
16. Click the Localizable (English) file and type the following:
17. Click the Localizable (Spanish) file and type the following:
18. Click the Localizable (French) file and type the following:
What these localizable files do is define placeholder text with arbitrary names
such as “greeting-label” or “button-label”. Then depending on which language the app
is using, the appropriate translation appears in place of the placeholder text. In this
example, we want an app to display text in English, Spanish, and French, so we need to
create three separate Localizable files and define translations for each placeholder text.
157
Chapter 8 Translating with Localization
In this example, the canvas pane will display French (“fr”) by looking for a
French Localizable file. You’ll need to use a separate .environment modifier for each
language you want to display in the canvas pane. In the LocalApp example, we want
to use English, French, and Spanish. That means we’ll need to define three separate
.environment modifiers.
To see how to define multiple language previews in the canvas pane, follow
these steps:
158
Chapter 8 Translating with Localization
Figure 8-10. The Canvas pane can display multiple iOS simulated screens in
different languages
159
Chapter 8 Translating with Localization
Notice that the Text view uses the placeholder text “greeting-label”
and the Button uses the placeholder text “button-label”. Xcode will
place both of two placeholders with the translations stored in the
Localizable file for each language (English, Spanish, and French).
5. Click the different Content View tabs at the top of the Canvas
pane. Notice that the canvas pane displays an English, French,
and Spanish iOS screen as shown in Figure 8-11.
6. Click the Live icon in the Canvas pane for any of the iOS simulated
screens and then click the Button to toggle between a red or
yellow background in the VStack.
In this simple example, you can see how Xcode lets you create separate translations
for different placeholders. By using these placeholders in your user interface, you
can let the app replace the placeholder text with the text defined in the appropriate
Localizable file.
160
Chapter 8 Translating with Localization
That’s why you need a separate Localizable file for each language, and you need
a separate translation for each placeholder text. The more languages you need to
support, the more Localizable files you’ll need with a separate translation for each
placeholder text.
Using a LocalizableStringKey
The Text and Button views let you define placeholder text that will be replaced with
different language translations defined in the separate Localizable files. However, if you
want to define different language translations for text that appears in something other
than a Text or Button view, you need to use something called a LocalizableStringKey.
Normally, when you define a String, you might use code like this:
However, this stores English text in the variable. If you wanted to change this text to
French or Spanish, you’d have to rewrite this text in another language such as French or
Spanish. Since this isn’t practical, SwiftUI lets you declare a String variable or constant as
a LocalizedStringKey such as
In the French Localizable file, we can define what text to use in place of “name-label”
like this:
161
Chapter 8 Translating with Localization
Now the app will replace “secret-label” with the appropriate text (“Type your name
here” or “Tapez votre nom ici”) depending on whether the app is displaying English or
French. To see how LocalizableStringKey works, follow these steps:
1. Make sure the LocalApp project is loaded into Xcode and that
you have created a localization file for French and Spanish (see
Figure 8-9).
2. Click the Localizable (English) file so the entire file looks like the
following:
3. Click the Localizable (French) file so the entire file looks like the
following:
4. Click the Localizable (Spanish) file so the entire file looks like the
following:
162
Chapter 8 Translating with Localization
import SwiftUI
163
Chapter 8 Translating with Localization
7. Click each Content View tab at the top of the Canvas pane to see
the different translations as shown in Figure 8-12.
Figure 8-12. Different languages appear in the Text, Button, and TextField views
Wherever Xcode finds the string “greeting-label,” it will replace that string with
the fixed string “Hello, world!” for an English version of the app. However, many
times strings combine fixed text with a value inserted inside the string using string
interpolation such as
If the value of the “age” variable is 26, the preceding Text view would display “You
are 26 years old”. When you need to use string interpolation within text that may need to
appear in different foreign languages, you need to identify where you want to use string
interpolation within that text no matter which language may be displayed.
To specify where string interpolation occurs, you need to use a format specifier for
that particular data type such as
%@ – For String
164
Chapter 8 Translating with Localization
These format specifiers appear in both the placeholder text and the translated
text. For example, suppose you wanted to display this text in different languages where
“petCount” is an integer:
First, you would need to create placeholder text for the Text view like this:
Text("pet-label \(petCount)")
Then to define “pet-label” in the Localizable file, you would need to use the format
specifier in both the placeholder text to the left of the equal sign and in the actual text on
the right of the equal sign like this:
Notice that the %lld format specifier appears where string interpolation appears in
the string such as at the end. To see how to use string interpolation with localization,
follow these steps:
1. Make sure the LocalApp project is loaded into Xcode and that
you have created a localization file for French and Spanish (see
Figure 8-9).
2. Click the Localizable (English) file so the entire file looks like the
following:
3. Click the Localizable (French) file so the entire file looks like the
following:
165
Chapter 8 Translating with Localization
4. Click the Localizable (Spanish) file so the entire file looks like the
following:
5. Click the ContentView file and add the following variables and
State variables:
166
Chapter 8 Translating with Localization
import SwiftUI
167
Chapter 8 Translating with Localization
8. Click the Content View tabs at the top of the Canvas pane.
Notice that each Text view displays “Value of pi = 3.1415” in the
appropriate language.
9. Click in the TextField and type a name. Notice that the name
appears in the top Text view.
10. Click the Button. Notice that each time you click the Button, the
number to the left increments by one.
This project shows how to include string interpolation with text that will be localized
in different languages. By using the right format specifier for String, Int, or Double data
types and placing the format specifier in the front or end of a string, you can make sure
your Localizable file translates text with data displayed by string interpolation.
Then define one or more settings for how to format the information such as
formatter.dateStyle = .full
168
Chapter 8 Translating with Localization
To see how to use formatters to display data in different languages and regions,
follow these steps:
1. Make sure the LocalApp project is loaded into Xcode with the
English, French, and Spanish Localizable files.
init() {
date = Date()
dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
}
169
Chapter 8 Translating with Localization
import SwiftUI
init() {
date = Date()
dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .medium
}
170
Chapter 8 Translating with Localization
5. Click the Live icon on the Canvas pane. The English, French, and
Spanish versions of your app appear as shown in Figure 8-13.
Notice that the English version displays decimal points with a
period (3.1415), while the French and Spanish versions display
decimal points with a comma (3,1415).
Figure 8-13. Each region displays numbers, dates, and times differently
Using Pseudolanguages
When designing apps that will display other languages, keep in mind that translated text
is rarely the same height and length as the text you used to design an app. For example,
text displayed in French might have accent characters above or below certain letters,
increasing the text height. In other cases, text translated into German might appear
longer, while the same text translated into Chinese will appear shorter since each
character in Chinese represents a complete word.
171
Chapter 8 Translating with Localization
While you could test every possible language to see how it appears within your app’s
user interface, a much simpler solution is to use pseudolanguage. This lets you see how
text will appear in other languages without actually translating text into other languages.
The available options for displaying pseudolanguage include
To see how to use pseudolanguage to help design your app’s user interface, follow
these steps:
172
Chapter 8 Translating with Localization
// .lineLimit(2)
}
}
}
import SwiftUI
5. Click the Run icon on the left panel and then click the Options tab.
6. Click the App Language list box and choose Double-Length
Pseudolanguage as shown in Figure 8-14.
173
Chapter 8 Translating with Localization
7. Click the Close button. Xcode displays your app’s user interface
with text twice the length as shown in Figure 8-15.
174
Chapter 8 Translating with Localization
9. Click the Run icon on the left panel and then click the Options tab.
10. Click the App Language list box and choose Right-to-Left
Pseudolanguage.
11. Click the Close button and then click the Resume button in the
upper-right corner of the canvas pane. Xcode displays your app’s
user interface with text aligned along the right margin as shown in
Figure 8-16.
13. Click the Run icon on the left panel and then click the Options tab.
14. Click the App Language list box and choose Accented
Pseudolanguage.
15. Click the Close button and then click the Resume button in the
upper-right corner of the canvas pane. Xcode displays your app’s
user interface with text displaying accent characters above and
below text as shown in Figure 8-17.
175
Chapter 8 Translating with Localization
18. Click the Run icon on the left panel and then click the Options tab.
19. Click the App Language list box and choose Bounded String
Pseudolanguage.
20. Click the Close button. Xcode displays your app’s user interface.
Notice that the square brackets let you see where text begins and
ends, but with the .lineLimit(2) modifier, the last square bracket is
not visible because text is being cut off as shown in Figure 8-18.
// .lineLimit(2)
23. Click the Run icon on the left panel and then click the Options tab.
176
Chapter 8 Translating with Localization
24. Click the App Language list box and choose Right-to-Left
Pseudolanguage With Right-to-Left Strings Pseudolanguage.
25. Click the Close button. Xcode displays your app’s user interface
with text reversed and aligned on the right margin as shown in
Figure 8-19.
Pseudolanguage lets you see how your app’s user interface adjusts to different types
of languages such as those that appear with accent characters and those that appear
right to left. By testing how pseudolanguage might affect the appearance of your app, you
can make sure that the user interface adapts to all types of languages.
Summary
Creating an app in your native language may be fine, but if you want to reach other
markets, you need to translate the text of your app into other languages. By using
Xcode’s pseudolanguage feature, you can mimic other languages to make sure your user
interface adapts to longer or shorter text. You can also display text in other languages
to see how specific foreign words and phrases will look on your app’s user interface.
Don’t forget that some languages and regions display data differently such as dates and
numbers.
Creating an app can be hard work, so it only makes sense to distribute your app as
broadly as possible so it can reach as many people as possible. The more languages your
app can support, the bigger your potential market, so always keep foreign markets in
mind to increase your app’s popularity.
177
CHAPTER 9
Displaying Gauges
and Progress Views
When a program is busy doing something such as retrieving data or performing a
task, it’s good practice to let the user know the program is still working. Otherwise, the
program may appear frozen as if it crashed or froze. To give visual feedback to the user
that a program is busy but likely to finish soon, you can use gauges and progress views.
The main idea behind gauges is to show the user approximately how far along a task
might be. This often appears as a horizontal bar that gradually fills up the closer the task
gets to completion as shown in Figure 9-1.
Figure 9-1. Many programs visually display how close a task is to completion
While a gauge shows approximately how much a task has been completed, a
progress view has the option of displaying a horizontal bar or displaying a rotating image
to let users know that something is happening but there’s no definite idea when that task
will finish. Progress views simply give visual feedback that the program hasn’t crashed
but is still working as shown in Figure 9-2.
Figure 9-2. A spinning progress view shows activity without identifying when that
activity will finish
179
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_9
Chapter 9 Displaying Gauges and Progress Views
Creating a Gauge
A Gauge visually represents a numeric value. By default, this numeric value ranges
between zero and one, but you can define any range of values you want. The most basic
Gauge requires a value like this, which creates a simple horizontal Gauge as shown in
Figure 9-3:
Gauge(value: aNumericValue) {
}
Inside the curly brackets of a Gauge is where you can display descriptive text or an
image to help users better understand the purpose of the Gauge and what it’s measuring
as shown in Figure 9-4.
Instead of a Text view, you can also use an Image or Label view such as
Gauge(value: speed) {
Text("Text view")
}
Gauge(value: speed) {
Image(systemName: "hare")
}
180
Chapter 9 Displaying Gauges and Progress Views
Gauge(value: speed) {
Label {
Text("Text view")
} icon: {
Image(systemName: "tortoise")
}
}
Although a Gauge represents a value of zero to one, you can define your own
minimum and maximum values by defining a range like this:
To make the current value of the Gauge appear underneath the Gauge, you can
define the currentValueLabel like this:
The preceding code defines the Gauge’s range from –25 to 100, displays “Descriptive
text” above the Gauge, and displays the current value of the Gauge underneath as shown
in Figure 9-5.
Figure 9-5. Displaying descriptive text on top and its current numeric value
underneath a Gauge
181
Chapter 9 Displaying Gauges and Progress Views
To make it clear what the minimum and maximum values of a Gauge represent, you
can use the minimumValueLabel and maximumValueLabel properties to define these
values at the left and right ends of the Gauge like this:
The preceding code assumes the minimumValue and maximumValue variables have
been defined such as
Since the Gauge represents a decimal number, the Text views defined under both
the minimumValueLabel and maximumValueLabel properties convert these values to
integers that appear on the far left and right of the Gauge as shown in Figure 9-6.
Figure 9-6. Displaying the minimum and maximum values of a Gauge at the
far ends
By default, a Gauge displays a fill color using blue. To customize this color, you can
use the .tint modifier like this:
182
Chapter 9 Displaying Gauges and Progress Views
When you add descriptive text and the currentValueLabel property, the descriptive
text and current value appear centered above and below the Gauge (see Figure 9-6). If
you want to push this descriptive text and current value to the far left, you can modify the
Gauge using the .gaugeStyle modifier like this:
.gaugeStyle(.accessoryLinearCapacity)
This pushes the descriptive text and current value to the left as shown in Figure 9-7.
Figure 9-7. Changing the location of descriptive text and current values
of a Gauge
}
.padding()
5. Add a Gauge and a Slider view inside the VStack like this:
183
Chapter 9 Displaying Gauges and Progress Views
} minimumValueLabel: {
Text("\(Int(minimumValue))")
} maximumValueLabel: {
Text("\(Int(maximumValue))")
}.tint(.green)
.gaugeStyle(.accessoryLinearCapacity)
import SwiftUI
7. Drag the Slider left and right to change the value of the Gauge.
Notice how the currentValueLabel property displays the Gauge’s
current value underneath.
• .accessoryCircular
• .accessoryCircularCapacity
To define a circular Gauge, you must create a Gauge view and then
add the .gaugeStyle modifier that specifies either .accessoryCircular or
.accessoryCircularCapacity. Notice how the descriptive text appears in both
circular Gauges.
With .accessoryCircular, the text appears at the bottom and keeps the Gauge from
completing forming a closed circle.
With .accessoryCircularCapacity, the text appears inside the circular Gauge, which
allows the Gauge to form a complete circle.
185
Chapter 9 Displaying Gauges and Progress Views
VStack {
Gauge(value: speed, in: minimumValue...maximumValue) {
Text("RPMs")
} currentValueLabel: {
Text("\(speed)")
} minimumValueLabel: {
Text("\(Int(minimumValue))")
} maximumValueLabel: {
Text("\(Int(maximumValue))")
}.tint(.purple)
.gaugeStyle(.accessoryCircular)
import SwiftUI
186
Chapter 9 Displaying Gauges and Progress Views
6. Drag the Slider left and right to see how the circular Gauge works
as shown in Figure 9-9.
187
Chapter 9 Displaying Gauges and Progress Views
.gaugeStyle(.accessoryCircularCapacity)
9. Drag the Slider left and right. Notice that the circular Gauge looks
different as shown in Figure 9-10.
ProgressView()
Besides displaying a spinning animation, the Progress view can also display text
underneath as shown in Figure 9-11.
If you specify a value and a total, the Progress view can look like a Gauge as a
horizontal line that gradually fills up. The value parameter defines the current value that
the Progress view represents, and the total parameter defines the value representing
100% completion such as
188
Chapter 9 Displaying Gauges and Progress Views
ProgressView("Text added")
import SwiftUI
ProgressView("Text added")
189
Chapter 9 Displaying Gauges and Progress Views
5. Click the Live icon on the Canvas pane. Notice that the top
Progress view displays a spinning animation, while the second
Progress view displays a spinning animation with “Text added”
appearing underneath. When a Progress view represents a value,
it appears as a horizontal line.
Summary
When an app needs to perform a lengthy task, it can appear frozen or unresponsive.
That’s why apps use visual clues like Gauges and Progress views to show that the app is
still working and even how close it might be to completion.
Gauges can appear as a horizontal line or as a circular image. To customize
the appearance of a Gauge, you can change the .tint color, .gaugeStyle, and
minimumValueLabel/maximumValueLabel.
Progress views can appear as spinning animation by themselves or with explanatory
text underneath. If a Progress view represents a numeric value, then it appears as a
horizontal line much like a linear Gauge.
Both Gauges and Progress views are ways to give feedback to the user so they know
that the app is continuing to work and is getting closer to completion.
190
CHAPTER 10
Figure 10-1. The search bar appears in the Contacts app on an iPhone
Swift now lets you create your own search bar to search through data stored in a
list. Essentially, all you need to do is store data in a List view and then add a .searchable
modifier to that List. That will allow users to search for specific data stored in that
List view.
NavigationStack{
}.searchable(text: $stateVariable)
191
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_10
Chapter 10 Adding Search to an App
You’ll also need to declare a State variable to use within the .searchable modifier
like this:
Whatever you type inside the search bar will get stored in the State variable. Then
you’ll need to filter out the List to match whatever the user typed using code like this:
.searchable(text: $stateVariable) {
ForEach(listArray.filter {$0.hasPrefix(stateVariable)},
id: \.self) { name in
Text(name)
}
The preceding code assumes an array called listArray. Then it uses a ForEach loop
with .filter to search for the text the user typed into the search bar (stateVariable).
This filters out the items in the array and displays the results in the Text view using an
arbitrarily named variable called “name”.
To see how to search for items in a List view, follow these steps:
This code creates a State variable to work with the search bar.
In addition, it creates an array of strings. Notice that each string
begins with a capital letter. That’s because the search bar always
capitalizes the first letter of any text typed in.
192
Chapter 10 Adding Search to an App
5. Add the following underneath var body: some View like this:
NavigationStack{
PetListView(animals: petArray)
}.searchable(text: $searchText) {
ForEach(petArray.filter {$0.hasPrefix(searchText)},
id: \.self) { name in
Text(name)
}
}
import SwiftUI
193
Chapter 10 Adding Search to an App
ForEach(petArray.filter {$0.hasPrefix(searchText)},
id: \.self) { name in
Text(name)
}
}
}
}
6. Click the Live icon on the Canvas pane. The canvas pane displays
the List view containing all the strings defined in the petArray
variable.
7. Click in the search bar and type C. Notice that the List view
now filters out only those items that contain “C” as shown in
Figure 10-2.
194
Chapter 10 Adding Search to an App
Figure 10-2. Filtering out items in the List view through the search bar
8. Continue typing Can. The List view will display the only text that
matches “Can” which is Canary.
Any text that appears after the prompt parameter appears as placeholder text in the
search bar as shown in Figure 10-3.
195
Chapter 10 Adding Search to an App
To see how to change this placeholder text in the search bar, follow these steps:
4. Click the Live icon in the Canvas pane. Notice that the placeholder
text now displays the text you defined after the prompt parameter
in the .searchable modifier (see Figure 10-3).
Text("Croaking").searchCompletion("Frog")
196
Chapter 10 Adding Search to an App
The .searchCompletion modifier contains text that will appear in the search bar if the
user selects the suggested text (displayed by the Text view). In the preceding example,
“Croaking” will appear underneath the search bar, and if the user clicks “Croaking,” then
the .searchCompletion modifier will display “Frog” in the search bar.
To see how to add suggestions to a search bar, follow these steps:
This code creates a State variable to work with the search bar.
In addition, it creates an array of strings. Notice that each string
begins with a capital letter. That’s because the search bar always
capitalizes the first letter of any text typed in.
5. Add the following underneath var body: some View like this:
NavigationStack{
PetListView(animals: petArray)
}
197
Chapter 10 Adding Search to an App
import SwiftUI
198
Chapter 10 Adding Search to an App
Notice the Divider() that appears right above the ForEach loop.
This Divider() view simply adds a horizontal line separating the
suggestions from the actual items displayed in the List view.
8. Click in the search bar. Notice that the search bar now displays
suggestions in a different color as shown in Figure 10-4.
199
Chapter 10 Adding Search to an App
9. Click one of the suggestions under the search bar. Notice that
when you select a suggestion, it automatically displays the text
defined by that Text view’s .searchCompletion modifier.
Summary
A search bar can make it easy to find items in a List view, especially if there are multiple
items that may not be visible in the screen. Creating a list of suggestions can be helpful to
simplify searching. Text views combined with the .searchComplete modifier can create
these suggestions. By using variables in these Text views instead of strings, you could
make this list of suggestions dynamic so they constantly adapt to the user’s behavior.
Search bars can make finding items in a List view fast and easy, so consider using them
whenever you can.
200
CHAPTER 11
Detecting Motion
and Orientation
Mobile computer devices like the iPhone and iPad essentially put a PC in your pocket,
letting you use a computer wherever you happen to be. However, unlike a desktop
or even a laptop PC, mobile computers can track movement and orientation. This
can come in handy for tracking the user’s arm movements in a game or helping you
measure angles.
To track motion and orientation, every iOS device comes with a built-in
accelerometer that can detect movement. In addition to the accelerometer, the iOS
devices also include a gyroscope to detect positions of the iOS device. By adding motion
and orientation detection, you can create apps that respond to physical gestures as well
as touch gestures.
201
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_11
Chapter 11 Detecting Motion and Orientation
Note To test motion and orientation, you need a real iOS device connected to
your Macintosh through a USB cable. The Simulator and Canvas pane cannot
detect changes in physical movements and different orientations.
To use Core Motion in an app, you need to import the CoreMotion framework and
then create a CMMotionManager object like this:
import CoreMotion
Finally, you need to check for data updates on a special queue called
OperationQueue. Without this OperationQueue, motion-detecting data could arrive
faster than the app could process it, making the app feel frozen or unresponsive. You can
define this OperationQueue() as a constant like this:
Detecting Acceleration
The accelerometer can measure both acceleration and gravity in three dimensions. The
accelerometer can determine not only how an iOS device is being held but also whether
it’s lying face down or face up on a flat surface such as a table. Accelerometers measure
g-forces (g for gravity), so a value of 1.0 returned by the accelerometer means that 1 g is
sensed in a particular direction, as in these examples:
202
Chapter 11 Detecting Motion and Orientation
Figure 11-1. The iPhone accelerometer’s axes in three dimensions. The front view
of an iPhone on the left shows the x and y axes. The side view on the right shows
the z axis
import CoreMotion
203
Chapter 11 Detecting Motion and Orientation
5. Underneath the var body: some View line, add the following
VStack with three Text views inside:
VStack{
Text("x: \(x)")
Text("y: \(y)")
Text("z: \(z)")
}
These Text views will display the changing values as the user
moves the iPhone in the different x, y, and z directions.
6. Add the .onAppear modifier at the end of the VStack like this:
.onAppear {
motionManager.startAccelerometerUpdates(to: queue) { (data:
CMAccelerometerData?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let trackMotion: CMAcceleration = data.acceleration
motionManager.accelerometerUpdateInterval = 2.5
DispatchQueue.main.async {
x = trackMotion.x
y = trackMotion.y
z = trackMotion.z
}
}
}
204
Chapter 11 Detecting Motion and Orientation
import SwiftUI
import CoreMotion
205
Chapter 11 Detecting Motion and Orientation
}
}
}
}
206
Chapter 11 Detecting Motion and Orientation
Figure 11-2. Choosing an actual iOS device through the Product menu
207
Chapter 11 Detecting Motion and Orientation
208
Chapter 11 Detecting Motion and Orientation
209
Chapter 11 Detecting Motion and Orientation
11. Tap the Developer Mode toggle. An action sheet appears at the
bottom of the screen, asking you to select Restart or Cancel.
12. Tap Restart. Your iOS device will shut off and turn on again.
210
Chapter 11 Detecting Motion and Orientation
13. Make sure your iOS device is connected to Xcode (see Figure 11-3)
and connected to your Macintosh through a USB cable.
15. Lay your iOS device flat on a table. The z value should be
near 1 or –1.
16. Hold the iPhone in portrait mode. The y value should be 1 or –1.
17. Hold the iPhone in landscape mode. The x value should be 1 or –1.
18. Click the Stop button in Xcode (or choose Product ➤ Stop) to stop
running the app.
211
Chapter 11 Detecting Motion and Orientation
To see how to use the gyroscope to detect and measure rotation, follow these steps:
import CoreMotion
5. Underneath the var body: some View line, add the following
VStack with three Text views inside:
VStack{
Text("x: \(x)")
Text("y: \(y)")
Text("z: \(z)")
}
These Text views will display the changing values as the user
moves the iPhone in the different x, y, and z directions.
6. Add the .onAppear modifier at the end of the VStack like this:
.onAppear {
motionManager.startGyroUpdates(to: queue) { (data:
CMGyroData?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
212
Chapter 11 Detecting Motion and Orientation
import SwiftUI
import CoreMotion
213
Chapter 11 Detecting Motion and Orientation
}
.onAppear {
motionManager.startGyroUpdates(to: queue) { (data:
CMGyroData?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let trackMotion: CMRotationRate = data.
rotationRate
motionManager.gyroUpdateInterval = 2.5
DispatchQueue.main.async {
x = trackMotion.x
y = trackMotion.y
z = trackMotion.z
}
}
}
}
}
214
Chapter 11 Detecting Motion and Orientation
9. Rapidly dip your iOS device forward and backward across its
horizontal center (x axis). Notice that the x value displayed on the
screen changes drastically away from 0 such as reaching a value
of –8 or 10.
10. Twist your iOS device around its vertical center (y axis). Notice
that the y value displayed on the screen changes drastically away
from 0 such as reaching a value of 7 or –6.
12. Click the Stop button in Xcode (or choose Product ➤ Stop) to stop
running the app.
import CoreMotion
215
Chapter 11 Detecting Motion and Orientation
5. Underneath the var body: some View line, add the following
VStack with three Text views inside:
VStack{
Text("x: \(x)")
Text("y: \(y)")
Text("z: \(z)")
}
These Text views will display the changing values as the user
moves the iPhone in the different x, y, and z directions.
6. Add the .onAppear modifier at the end of the VStack like this:
.onAppear {
motionManager.startMagnetometerUpdates(to: queue) { (data:
CMMagnetometerData?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let trackMotion: CMMagneticField = data.magneticField
motionManager.magnetometerUpdateInterval = 2.5
DispatchQueue.main.async {
x = trackMotion.x
y = trackMotion.y
z = trackMotion.z
}
}
}
216
Chapter 11 Detecting Motion and Orientation
import SwiftUI
import CoreMotion
217
Chapter 11 Detecting Motion and Orientation
y = trackMotion.y
z = trackMotion.z
}
}
}
}
}
9. Move your iPhone around to see how the different values change.
10. Click the Stop button in Xcode (or choose Product ➤ Stop) to stop
running the app.
218
Chapter 11 Detecting Motion and Orientation
To see how to detect roll, pitch, and yaw, follow these steps:
import CoreMotion
219
Chapter 11 Detecting Motion and Orientation
5. Underneath the var body: some View line, add the following
VStack with three Text views inside:
VStack{
Text("Pitch: \(pitch)")
Text("Yaw: \(yaw)")
Text("Roll: \(roll)")
}
These Text views will display the changing values as the user
rotates and tilts the iPhone in different directions.
6. Add the .onAppear modifier at the end of the VStack like this:
.onAppear {
motionManager.startDeviceMotionUpdates(to: queue) { (data:
CMDeviceMotion?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let trackMotion: CMAttitude = data.attitude
motionManager.deviceMotionUpdateInterval = 2.5
DispatchQueue.main.async {
pitch = trackMotion.pitch
yaw = trackMotion.yaw
roll = trackMotion.roll
}
}
}
220
Chapter 11 Detecting Motion and Orientation
import SwiftUI
import CoreMotion
221
Chapter 11 Detecting Motion and Orientation
}
}
}
9. Twist your iOS device around its vertical axis. The Roll value
should deviate from 0 such as –2 or 3.
10. Flip the front of your iOS device back and forward. The Pitch value
should deviate from 0 such as 1 or –2.
11. Rotate your iOS device on the flat surface clockwise and counter-
clockwise. The Yaw value should deviate from 0 such as –2 to 1.
12. Click the Stop button in Xcode (or choose Product ➤ Stop) to stop
running the app.
Summary
Every iOS device comes with built-in sensors to measure movement. To detect
movements of an iOS device, you need to use the CoreMotion framework.
Some of the different types of motion data an app can detect include acceleration,
rotation, and even nearby magnetic fields. Detecting the movement of an iOS device lets
an app respond appropriately, giving movement another way to control an app.
222
CHAPTER 12
Note The more accurate and more often you need to track the movement of
an iOS device’s location, the more battery power the app will require, so you
need to trade off between greater accuracy and constant updates against longer
battery life.
Using MapKit
The first step to defining a location is to import the MapKit framework into an app
like this:
import MapKit
223
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_12
Chapter 12 Using Location and Maps
To display a map on the user interface, we can just use the following code that
displays a map using specific latitude and longitude coordinates:
Map(coordinateRegion: $region)
Defining Accuracy
When using Core Location, you need to define the amount of accuracy you want.
Remember, the greater the accuracy, the more power the iOS device will require, so
it’s best to choose the level of accuracy your app absolutely needs. If you just need to
identify the user’s geographical location such as a city, then you don’t need specific
accuracy. However, if your app needs to know the iOS device’s precise location to
locate the user such as for a ride-sharing service that needs to know where to pick up a
passenger, then you’ll need greater precision.
You can define a specific level of accuracy in meters such as 150 meters. However,
Core Location provides several constants you can use that define varying degrees of
accuracy:
224
Chapter 12 Using Location and Maps
To define accuracy, you need to set the desiredAccuracy property to a value or to one
of the preceding constants like this:
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.distanceFilter = 100
Requesting a Location
Core Location gives you two ways to request the location of an iOS device. The first
method requests the location once. This can be useful for apps that don’t need constant
updating to track movement. To request a location once, use the requestLocation
method like this:
225
Chapter 12 Using Location and Maps
locationManager.requestLocation()
Because the requestLocation method only checks for a location once, it requires
far less power than the second method, which requests locations continuously.
To track locations continuously, you need to use the startUpdatingLocation and
stopUpdatingLocation methods like this:
locationManager.startUpdatingLocation()
locationManager.stopUpdatingLocation()
Core Location also offers two Boolean values you can modify as follows:
• floor – Returns the floor of a building where the iOS device is located
226
Chapter 12 Using Location and Maps
Requesting Authorization
Apps often need to request permission to access many hardware features of an iOS
device. By forcing an app to request permission, Apple wants to make sure users
authorize an app’s access to features such as the camera, the microphone, and the
device’s location. Requesting authorization provides privacy for users and allows them
to know exactly when an app might need to request access to specific hardware features.
Any app that uses Core Location must request authorization to track an iOS device’s
location. Core Location provides two ways to request authorization:
In most cases, you’ll only want to use location services while your app is running.
Besides using one of the preceding methods, an app also needs to modify its Info.plist
file and add the Privacy – Location Always and When In Use Usage Description key as
shown in Figure 12-1. In addition, you’ll need to add descriptive text explaining why your
app needs to access location services.
227
Chapter 12 Using Location and Maps
import MapKit
The preceding latitude and longitude will display Paris, but you
can enter any latitude and longitude coordinates you wish.
5. Inside var body: some View, add a VStack, a Map, and a TextEditor
like this:
VStack{
Map(coordinateRegion: $region)
TextEditor(text: $message)
.frame(width: .infinity, height: 100)
}
This displays the map at the top of the screen and a TextEditor
underneath.
.onAppear {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
228
Chapter 12 Using Location and Maps
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
import SwiftUI
import MapKit
7. Click the Live icon in the Canvas pane. The map of Paris (or
whatever latitude and longitude coordinates you entered) now
appears on the screen as shown in Figure 12-2.
The preceding steps display a map in the canvas pane. However, if you want to
run this project in the Simulator or on an actual iOS device, we must go through the
additional step of changing a privacy setting in the Info file. To do this, follow these steps:
2. Click the project name at the top of the Navigator pane. Then click
the Info tab to display a list of properties as shown in Figure 12-3.
230
Chapter 12 Using Location and Maps
3. Move the mouse over the up/down arrow in any line in the Key
column. The + and – icons appear.
4. Click the + icon to insert a new property into the property list.
231
Chapter 12 Using Location and Maps
import MapKit
232
Chapter 12 Using Location and Maps
self.location = CLLocationCoordinate2D(
latitude: lat,
longitude: long)
}
}
Map(coordinateRegion: $region,
annotationItems: [place])
{ place in
MapMarker(coordinate: place.location,
tint: Color.purple)
}
TextEditor(text: $message)
.frame(width: .infinity, height: 100)
233
Chapter 12 Using Location and Maps
.onAppear {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
import SwiftUI
import MapKit
234
Chapter 12 Using Location and Maps
{ place in
MapMarker(coordinate: place.location,
tint: Color.purple)
}
TextEditor(text: $message)
.frame(width: .infinity, height: 100)
}
.onAppear {
locationManager.desiredAccuracy =
kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
}
10. Click the Live icon in the Canvas pane to see the map marker
placed on the map (see Figure 12-4).
235
Chapter 12 Using Location and Maps
import MapKit
236
Chapter 12 Using Location and Maps
self.id = id
self.location = CLLocationCoordinate2D(
latitude: lat,
longitude: long)
}
}
Map(coordinateRegion: $region,
annotationItems: [place])
{ place in
MapAnnotation(coordinate: place.location) {
Rectangle().stroke(Color.purple, lineWidth: 5)
.frame(width: 70, height: 40)
}
}
237
Chapter 12 Using Location and Maps
import SwiftUI
import MapKit
238
Chapter 12 Using Location and Maps
8. Click the Live icon in the Canvas pane to see the map annotation
placed on the map (see Figure 12-5).
Summary
Mobile devices such as the iPhone and iPad can be especially useful when tracking
the user’s current location. When combined with a map display, an app can show the
location of the user and the locations of other places or people as well.
When identifying a user’s location, you can define the accuracy you want and
whether to use markers, pins, or annotations to identify and highlight certain areas on
the map.
Remember that the greater the accuracy you need, the more power the app will
require, which can drain the iOS device’s battery, so only use greater accuracy when you
need it. Also, make sure that any app that uses location services requests permission to
do so as well.
239
CHAPTER 13
Note If you have an audio or video file stored in a different format, you’ll need to
convert it to a format that iOS can recognize.
241
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_13
Chapter 13 Playing Audio and Video
import AVFoundation
After you’ve imported AVFoundation into your project, you can create a variable to
represent the AVAudioPlayer such as
To play an audio file, you need to drag and drop an audio file into the Navigator pane.
Then you need to write code that loads the audio file into the AVAudioPlayer variable.
Finally, you can use the play(), pause(), and stop() methods to control the playing of the
audio file.
To complete this example, you’ll need an audio file stored in a supported file format
such as .mp3 or .mov. If you don’t have any audio files stored on your Macintosh, you
can download free audio files from the following sites:
• soundbible.com
• archive.org
• freesound.org
• gamesounds.xyz
You can also record audio files on a Macintosh by loading the QuickTime Player
program and choosing File ➤ New Audio Recording. Once you have an audio file, either
one you downloaded or created through QuickTime Player program, you can test how to
play an audio file in an iOS app.
When working with files of any type, you need to specify the file name and the
file path. The file name is the complete name of the file and its file extension such as
HappyBirthday.mp3 or JingleBells.mov. The file path defines the location of the file
within your app.
To retrieve the file path, you need to identify the file name and type you want to
find such as
242
Chapter 13 Playing Audio and Video
Once you know the path of the file you want to play, then you can load that file and
path into the AVAudioPlayer to play it.
To see how to play an audio file, follow these steps:
1. Create a new iOS App project and name this new project
AudioApp.
243
Chapter 13 Playing Audio and Video
4. Inside this newly created Swift file, add the following line
underneath import Foundation:
import AVFoundation
func stopSound() {
audioPlayer?.stop()
}
import Foundation
import AVFoundation
244
Chapter 13 Playing Audio and Video
func stopSound() {
audioPlayer?.stop()
}
7. Drag and drop an audio file into the Navigator pane as shown in
Figure 13-2. A dialog appears.
245
Chapter 13 Playing Audio and Video
8. Click Finish. Your audio file now appears in the Navigator pane.
9. Click the ContentView file in the Navigator pane and add the
following State variable underneath the struct ContentView:
View line:
10. Add the following Button inside the var body: some View line
like this:
Button(action: {
if playAudio {
playSound(sound: "Small World", type: "mp3")
} else {
stopSound()
}
playAudio.toggle()
}, label: {
Text(playAudio ? "Play audio" : "Stop audio")
})
Notice that the text “Small World” in the preceding code refers to
the name of the audio file, and “mp3” refers to the file type, which
is called Small World.mp3. You may need to change this audio file
and file type to match the audio file you added to your project.
import SwiftUI
246
Chapter 13 Playing Audio and Video
playAudio.toggle()
}, label: {
Text(playAudio ? "Play audio" : "Stop audio")
})
}
}
12. Click the Play audio button. Notice that the audio file starts
playing, and the Button changes from “Play audio” to “Stop audio”.
13. Click the Stop audio button. Notice that the audio stops playing,
and the Button changes from “Stop audio” to “Play audio”.
Experiment with different audio files and file formats such as a .wav or .mov audio
file. Remember to modify your code to use the exact name and file format of each new
audio file you test in this project.
Playing Video
Videos can display tutorials or tips for how to use an app. Just keep in mind that video
files tend to be much larger than audio files, so you’ll generally want to use short videos
to avoid taking up too much space.
To play video, your app needs to import the AVKit framework like this:
import AVKit
Then you need to create a State variable that represents an AVPlayer like this:
247
Chapter 13 Playing Audio and Video
This player is an optional variable because we have no guarantee that it will be able
to load a video file or not.
To actually display video in an app, we just need the VideoPlayer view that displays
the AVPlayer like this:
VideoPlayer(player: player)
At this point, we still need to load a video file into the player (AVPlayer). To do that,
we can use an .onAppear modifier to load the video file as soon as the VideoPlayer
appears. The first step is to use a guard statement to make sure we can load a specific
video file stored in the Xcode project like this:
This guard statement looks for a video file named SaturnV.mov. If this file exists, then
it loads that video file into the videoURL constant. Otherwise, the guard statement exits
out to prevent the player (AVPlayer) from trying to run a nonexistent video file.
After the guard statement confirms that the video file exists, we can safely load it into
the player like this:
To complete the following exercise, you’ll need a video file. You can record your own
videos using the QuickTime Player on a Macintosh or record a video on an iOS device
such as an iPhone or iPad. You can also find free video files at the following sites:
• nasa.gov
• publicdomainfiles.com
• archive.org
248
Chapter 13 Playing Audio and Video
3. Drag and drop a video file into the Navigator pane (see
Figure 13-2).
4. Add the import AVKit line underneath the import SwiftUI line
like this:
import AVKit
6. Add a VideoPlayer view underneath the var body: some View line
like this:
VideoPlayer(player: player)
VideoPlayer(player: player)
.onAppear {
}
VideoPlayer(player: player)
.onAppear {
guard let videoURL = Bundle.main.url(forResource:
"SaturnV", withExtension: "mov") else {
print ("Video file not found")
return
}
}
249
Chapter 13 Playing Audio and Video
9. Add the following code at the end of the .onAppear block to finally
load the video file into the AVPlayer:
VideoPlayer(player: player)
.onAppear {
guard let videoURL = Bundle.main.url(forResource:
"SaturnV", withExtension: "mov") else {
print ("Video file not found")
return
}
player = AVPlayer(url: videoURL as URL)
}
import SwiftUI
import AVKit
Note The preceding code tries to load a video file named SaturnV.mov, so make
sure you modify the file name and file extension in your code to match the video
file you added to your project.
10. Click the Live icon in the Canvas pane. Your video file appears.
11. Click the Play button to play the video file. Notice that you can
pause the video or drag the slider to view a different part of the
video at any time as shown in Figure 13-3.
251
Chapter 13 Playing Audio and Video
})
Within the videoOverlay’s curly brackets, you can define a Text view to display
text such as
By default, the videoOverlay places the Text view in the center of the video and
displays text in black. To change the color of the text, just modify the Text view with the
.foregroundColor modifier like this:
To adjust the position of the Text view over the video, move the Text view using a
Spacer(), which means we’ll need to enclose the entire Text view inside a VStack like this:
252
Chapter 13 Playing Audio and Video
To further adjust the placement of the Text view, we can use a .padding() and .frame
modifier as well like this:
You can vary the exact frame height based on what you think looks best. Rather than
or in addition to using the Spacer(), you could also use the .offset modifier or any other
technique to adjust the spacing and position of the Text view.
To see how to overlay text on a video, follow these steps:
3. Drag and drop a video file into the Navigator pane (see
Figure 13-2).
4. Add the import AVKit line underneath the import SwiftUI line
like this:
import AVKit
6. Add a VideoPlayer view underneath the var body: some View line
like this:
253
Chapter 13 Playing Audio and Video
VideoPlayer(player: player)
.onAppear {
}
VideoPlayer(player: player)
.onAppear {
guard let videoURL = Bundle.main.url(forResource:
"SaturnV", withExtension: "mov") else {
print ("Video file not found")
return
}
}
10. Add the following code at the end of the .onAppear block to finally
load the video file into the AVPlayer:
VideoPlayer(player: player)
.onAppear {
guard let videoURL = Bundle.main.url(forResource:
"SaturnV", withExtension: "mov") else {
254
Chapter 13 Playing Audio and Video
import SwiftUI
import AVKit
Note The preceding code tries to load a video file named SaturnV.mov, so make
sure you modify the file name and file extension in your code to match the video
file you added to your project.
11. Click the Live icon in the Canvas pane. Your video file appears.
12. Click the Play button to play the video file. Notice that the text
appears underneath the video as shown in Figure 13-4.
Summary
Any app can enhance the user’s experience by playing audio or video. Audio lets your
app play music, sound effects, or spoken speech. Video files let you display movies
that users can watch. Since video files can take up large amounts of space, use video
files sparingly, or else the size of your app can dramatically increase each time you add
another video file to your app.
By overlaying text on a video, you can provide additional information about that
video. When defining overlay text, simply use standard SwiftUI views such as Text,
Spacer(), and modifiers to position the overlay text where you want it to appear over
the video.
256
CHAPTER 14
Using Speech
The Speech framework lets apps recognize audio commands as a supplement to taps
and gestures. In addition, the Speech framework can also transcribe speech into text. By
adding speech recognition features, your app can offer more ways for the user to interact
in a natural manner that’s easy for everyone to do.
Before an app can use speech recognition, the user must give permission for the app
to access the microphone and use speech recognition. You may also want to make your
users aware that speech recognition may send audio data to Apple’s servers over the
Internet to improve accuracy. That’s why it’s important to get the user’s permission to
use the microphone and use speech recognition due to privacy concerns.
By adding speech recognition to your app, your user interface is no longer limited to
the touch screen. Speech recognition may never replace the touch screen, but it can give
users another way to interact with your app by just speaking to it out loud.
Note You can only test speech recognition on an actual iOS device. You cannot
test speech recognition with the canvas pane or the Simulator program.
257
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_14
Chapter 14 Using Speech
To see how this speech to text recognition feature works, follow these steps:
2. Click the project name at the top of the Navigator pane and then
click the Info tab to display the properties.
3. Move the mouse pointer over any row in the Key column until the
+ and – icons appear.
import Speech
258
Chapter 14 Using Speech
10. Add an optional variable to store the recognition task. Since the
task may or may not succeed, it needs to be an optional variable:
func stopSpeech() {
audioEngine.stop()
request.endAudio()
recognitionTask?.cancel()
audioEngine.inputNode.removeTap(onBus: 0)
}
func recognizeSpeech() {
let node = audioEngine.inputNode
request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
audioEngine.prepare()
do {
259
Chapter 14 Using Speech
try audioEngine.start()
} catch {
return print (error)
}
if !recognizeMe.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with:
request, resultHandler: {result, error in
if let result = result {
let transcribedString = result.bestTranscription.
formattedString
self.textLabel.text = transcribedString
} else if let error = error {
print(error)
}
})
}
request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
260
Chapter 14 Using Speech
self.request.append(buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print (error)
}
guard let recognizeMe = SFSpeechRecognizer() else {
return
}
if !recognizeMe.isAvailable {
return
}
261
Chapter 14 Using Speech
14. Define the user interface underneath the var body: some View line
as follows:
Button {
message = ""
stopSpeech()
} label: {
Text("Stop recording")
}
}
import SwiftUI
import Speech
262
Chapter 14 Using Speech
Text("Start recording")
}
Button {
message = ""
stopSpeech()
} label: {
Text("Stop recording")
}
}
}
func stopSpeech() {
audioEngine.stop()
request.endAudio()
recognitionTask?.cancel()
audioEngine.inputNode.removeTap(onBus: 0)
}
func recognizeSpeech() {
let node = audioEngine.inputNode
request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print (error)
}
263
Chapter 14 Using Speech
if !recognizeMe.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with:
request, resultHandler: {result, error in
if let result = result {
let transcribedString = result.bestTranscription.
formattedString
message = transcribedString
} else if let error = error {
print(error)
}
})
}
15. Connect an iOS device to your Macintosh through its USB cable.
16. Choose Product ➤ Destination and then select the iOS device
connected to your Macintosh under the iOS Device category. You
can also click the current target displayed at the top of the Xcode
window to display a menu where you can also select an iOS device
under the iOS Device category.
17. Click the Run button or choose Product ➤ Run. The first time you
run the app, it will ask permission to access the microphone.
264
Chapter 14 Using Speech
19. Speak a sentence and the transcribed text should appear in the
TextField. Then tap the Stop recording Button when you’re done.
The transcribed text may make mistakes, but in general, you’ll
find it’s fairly accurate in transcribing common words into text as
shown in Figure 14-2.
265
Chapter 14 Using Speech
266
Chapter 14 Using Speech
If the user says “purple,” “green,” or “yellow,” the app will change
the UILabel background to a different color. If the user says
anything else, the UILabel background will turn to white. Now
we need to call this checkSpokenCommand function inside the
recognizeSpeech function like this:
checkSpokenCommand(commandString: transcribedString)
func recognizeSpeech() {
let node = audioEngine.inputNode
request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print (error)
}
if !recognizeMe.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with:
request, resultHandler: {result, error in
if let result = result {
267
Chapter 14 Using Speech
}
Button {
message = ""
newColor = .white
stopSpeech()
} label: {
Text("Stop recording")
}
.background(newColor)
import SwiftUI
import Speech
268
Chapter 14 Using Speech
Button {
message = ""
newColor = .white
stopSpeech()
} label: {
Text("Stop recording")
}
}.background(newColor)
}
func stopSpeech() {
audioEngine.stop()
request.endAudio()
recognitionTask?.cancel()
269
Chapter 14 Using Speech
audioEngine.inputNode.removeTap(onBus: 0)
}
func recognizeSpeech() {
let node = audioEngine.inputNode
request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print (error)
}
if !recognizeMe.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with:
request, resultHandler: {result, error in
if let result = result {
let transcribedString = result.bestTranscription.
formattedString
message = transcribedString
checkSpokenCommand(commandString:
transcribedString)
} else if let error = error {
270
Chapter 14 Using Speech
print(error)
}
})
}
12. Say one of the three words (“purple,” “green,” or “yellow”) that
will change the background color of the VStack. When the app
recognizes one of these three command words, it changes the
VStack background color as shown in Figure 14-3.
271
Chapter 14 Using Speech
13. Tap the Stop recording Button. This clears the TextField and
changes the VStack color back to white.
272
Chapter 14 Using Speech
import AVFoundation
}
}
273
Chapter 14 Using Speech
TextEditor(text: $textToRead)
.frame(width: 250, height: 200)
}
import SwiftUI
import AVFoundation
274
Chapter 14 Using Speech
10. Click the Read Text Out Loud Button. The app reads the text in
the TextEditor, which is “This is a test of the emergency broadcast
system.”
11. Edit the text in the TextEditor and click the Read Text Out
Loud Button again. Notice that this time the app reads the text
you typed.
12. Drag the Slider left or right. Then click the Read Text Out Loud
Button again. Notice that this time the app reads the text faster
or slower.
275
Chapter 14 Using Speech
Summary
Adding speech recognition requires the Speech framework, while adding a speech
synthesizer to read text out loud requires the AVFoundation framework. Speech
recognition gives users another way to interact with your app, while the speech
synthesizer lets your app read short strings or even long amounts of text out loud. This
can be handy for people with visibility problems or to provide information to users if
they can’t look at the iPhone screen, such as when they’re driving.
By adding speech recognition and speech synthesizer, your app can use audio as
another part of its user interface to allow users to give and receive data from your app.
276
CHAPTER 15
Integrating SwiftUI
with UIKit
The previous way to create user interfaces was to rely on storyboards and the UIKit
framework. While storyboards and UIKit have been around for years, Apple is
encouraging developers to migrate to SwiftUI. Not only is SwiftUI adaptable to different
screen sizes without requiring a lot of work, but SwiftUI can create user interfaces with
far less code than storyboards or UIKit.
Although SwiftUI is clearly the future for developing apps in all of Apple’s operating
systems, there’s still a problem. SwiftUI isn’t as mature as UIKit, which means many
features are still not available in SwiftUI that you can find in UIKit.
There are two solutions. One is to wait until Apple adds more features to
SwiftUI. However, if you need certain features now, the second solution is to combine
SwiftUI with UIKit. Not only does this give you the best of both worlds, but it also allows
you to add SwiftUI to existing apps created with UIKit.
The first step is to import UIKit in any SwiftUI project like this:
import UIKit
Next, you need to create a structure that encloses the UIKit view you want to use
using the UIViewRepresentable protocol like this:
277
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_15
Chapter 15 Integrating SwiftUI with UIKit
The name of the structure can be any descriptive name you want, so feel free to
replace “Wrapper” with a name of your own choosing. Inside this structure, we need two
functions called makeUIView and updateUIView like this:
}
}
}
The makeUIView function runs once to create the specific UIView, so you must
replace UIView with the specific UIKit view you want to integrate into SwiftUI. The
updateUIView function runs to keep updating the UIView within the SwiftUI project.
Whatever UIView you defined in the makeUIView function is the same UIView you need
to use to replace “UIViewType” in the updateUIView function.
Finally, to display the UIKit view in a SwiftUI project, just reference the structure
name. In the preceding example, the structure has an arbitrary name of “Wrapper,” so to
make it appear in SwiftUI, just use its name like this:
278
Chapter 15 Integrating SwiftUI with UIKit
Although SwiftUI does not support PDFKit yet, we can still use PDFKit by wrapping
it inside a UIViewRepresentable structure. To see how to display a PDF file using PDFKit,
follow these steps:
2. Drag and drop a PDF file in the Navigator pane. You may also want
to rename the PDF file to something simple and easy to type for
your convenience.
import PDFKit
To open a PDF file, we’ll need to pass in the file name and
location, which is a URL data type. The preceding “url” constant
will let us call the structure (called ViewMe) by sending it a url that
specifies the PDF file name and location.
279
Chapter 15 Integrating SwiftUI with UIKit
UIViewRepresentableContext<ViewMe>)
Notice that this function specifies the object that the makeUIView
function returns:
_ uiView: PDFView
UIViewRepresentableContext<ViewMe>
10. Under the var body: some View line, add the following:
ViewMe(url: fileURL!)
import SwiftUI
import PDFKit
}
}
281
Chapter 15 Integrating SwiftUI with UIKit
11. Click the Live icon in the Canvas pane. The canvas pane displays
the PDF file you added to the Navigator pane as shown in
Figure 15-1. The page may appear cut off, so you may need to
scroll side to side to view the entire width of each PDF page. If the
PDF file contains multiple pages, you can view all the pages by
scrolling up or down.
282
Chapter 15 Integrating SwiftUI with UIKit
Displaying a Website
Another Apple framework that isn’t available to SwiftUI is WebKit, which can load
a website within an app. To use WebKit, just wrap it inside a UIViewRepresentable
structure. To see how to display a website using WebKit, follow these steps:
import WebKit
UIViewRepresentableContext<WebView>)
283
Chapter 15 Integrating SwiftUI with UIKit
Notice that this function specifies the object that the makeUIView
function returns:
_ uiView: WKWebView
UIViewRepresentableContext<WebView>
8. Under the var body: some View line, add the following:
import SwiftUI
import WebKit
284
Chapter 15 Integrating SwiftUI with UIKit
9. Click the Live icon in the Canvas pane. The canvas pane displays
the website specified by the string as shown in Figure 15-2. Click
the different hyperlinks on the website, then change this string to
another website address to display a different website.
285
Chapter 15 Integrating SwiftUI with UIKit
286
Chapter 15 Integrating SwiftUI with UIKit
What will likely become more common is that you’ll need to modify existing projects
created using storyboards and gradually add SwiftUI features. That means combining
an existing storyboard project with SwiftUI views. That lets you create user interfaces
quickly and easily using SwiftUI while using the time-tested code created and stored in a
project created with storyboards.
To see how to integrate SwiftUI files into a storyboard project, follow these steps:
1. Create a new iOS App project, make sure it’s a storyboard (not
SwiftUI) as shown in Figure 15-3, and name it StoryboardApp.
2. Click Next, choose a folder to store your project, and click Create.
3. Click the Main file in the Navigator pane to see the storyboard that
defines the user interface. Initially, the user interface consists of a
single view controller that mimics an iPhone screen.
287
Chapter 15 Integrating SwiftUI with UIKit
288
Chapter 15 Integrating SwiftUI with UIKit
289
Chapter 15 Integrating SwiftUI with UIKit
Figure 15-6. Opening the Library window by clicking the Library icon (+)
9. Click the Library icon (+) and look for a Hosting View Controller
as shown in Figure 15-7. A Hosting View Controller is a special
controller that encloses a SwiftUI view so it can appear within a
storyboard project.
290
Chapter 15 Integrating SwiftUI with UIKit
10. Drag and drop a Hosting View Controller to the right of the
existing view controller in the storyboard.
11. Click the Button on the view controller, hold down the Control
key, hold down the left mouse button, and drag the mouse over
the Hosting View Controller as shown Figure 15-8.
291
Chapter 15 Integrating SwiftUI with UIKit
12. Release the mouse and Control key. A popup menu appears as
shown in Figure 15-9.
Figure 15-9. A popup menu to define the type of connection between controllers
292
Chapter 15 Integrating SwiftUI with UIKit
13. Choose Show under the Action Segue category. Xcode draws
a line, called a segue, that connects the view controller to the
hosting controller as shown in Figure 15-10.
Figure 15-10. A segue connects the view controller to the hosting controller
14. Choose File ➤ New ➤ File and choose SwiftUI View as shown in
Figure 15-11.
293
Chapter 15 Integrating SwiftUI with UIKit
15. Click Next. The default file name for the file will be SwiftUIView.
16. Click Create. Xcode adds a SwiftUIView file in the Navigator pane.
17. Click the SwiftUIView file and edit it so the entire file looks
like this:
import SwiftUI
294
Chapter 15 Integrating SwiftUI with UIKit
This SwiftUI view does nothing more than display a name in a Text
view such as “Hello, Hanna”. The “name” variable will let us pass a
string into the SwiftUI view.
18. Click the Main file in the Navigator pane to view the entire
storyboard.
19. Click the Add Editor on Right icon in the upper-right corner as
shown in Figure 15-12. This splits the editor pane in half.
20. Click the right half of the editor pane and then click
ViewController in the Navigator pane. Xcode displays the contents
of the ViewController file in one of the editor panes and the
contents of the Main file in the other editor pane.
21. Click in the editor pane that contains the Main storyboard file (it
should look entirely gray). The Main storyboard should appear
after you click in its editor pane.
22. Move the mouse pointer over the segue that connects to the
Hosting Controller.
23. Hold down the Control key and the left mouse button, then
drag the mouse above the last curly bracket at the bottom of the
ViewController file as shown in Figure 15-13.
295
Chapter 15 Integrating SwiftUI with UIKit
24. Release the Control key and the left mouse button. A window
appears as shown in Figure 15-14.
25. Click in the Name text field and type a name such as
openSwiftUIView and click the Connect button. Xcode creates an
@IBSegueAction function.
The preceding code simply loads the SwiftUIView file and passes
the “Nancy” string into the “name” parameter. Feel free to
substitute “Nancy” with any name you wish. Notice that Xcode
displays an error message.
27. Add the following underneath the import UIKit line to eliminate
the error message:
import SwiftUI
28. Click the Run button or choose Product ➤ Run. The user interface
appears in the Simulator displaying the Button.
Summary
SwiftUI is the future for creating user interfaces, but it still lacks many features that
storyboards can provide. Fortunately, you can combine features from other frameworks
such as UIKit, PDFKit, WebKit, and many others into a SwiftUI project. Eventually, Apple
will provide SwiftUI support for all of its frameworks, but until that happens, you can still
use Apple’s various frameworks anyways.
While you can include storyboard features in SwiftUI, you can also add SwiftUI to
existing storyboard-based projects. By giving you the ability to combine SwiftUI and
storyboards in a single project, Xcode lets you get the best of both worlds.
297
CHAPTER 16
import PhotosUI
After adding the PhotosUI framework, we can then use the PhotosPicker to access
images and videos stored in the Photos Library. The data type that represents pictures in
the Photos Library is called PhotosPickerItem.
299
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_16
Chapter 16 Accessing the Photos Library
Figure 16-1. The PhotosPicker can act like a button to access the Photos Library
3. Add import PhotosUI under the import SwiftUI line like this:
import PhotosUI
5. Add the following inside the var body: some View like this:
PhotosPicker(selection: $selectedItems,
maxSelectionCount: 2,
matching: .images) {
Text("Select Some Pictures")
}
300
Chapter 16 Accessing the Photos Library
The Text view defines text that users can select to access the
Photos Library. The selection parameter stores everything
selected in the selectedItems array, which can hold
PhotosPickerItem data. The maxSelectionCount is an option
parameter that lets you define how many items the user can
select. In this case, the maxSelectionCount limits the user to
selecting a maximum of two items. The matching parameter filters
the Photos Library to display only images.
import SwiftUI
import PhotosUI
7. Click the “Select Some Pictures” Text view displayed on the user
interface. The default images stored in the Photos Library appear
(see Figure 16-1).
301
Chapter 16 Accessing the Photos Library
302
Chapter 16 Accessing the Photos Library
Figure 16-3. The PhotosPicker showing a selected image and thumbnails of other
selected images
10. Click Done. The Photos Library shows all images again.
11. Click the Albums tab at the top of the screen to view a list of
photo albums.
12. Click Cancel. The app returns to showing the PhotosPicker’s text
displaying “Select Some Pictures”.
303
Chapter 16 Accessing the Photos Library
• .cinematicVideos – Videos
• .depthEffectPhotos – Photos with depth information
To choose any of these types of images or videos, define them in the matching
parameter like this:
PhotosPicker(selection: $selectedItems,
matching: .videos) {
Text("Open the Photo Library")
}
If you want to choose two or more types of images or videos to display, you can
define an array to list multiple options like this:
PhotosPicker(selection: $selectedItems,
matching: .any(of: [.bursts, .livePhotos])) {
Text("Open the Photo Library")
}
304
Chapter 16 Accessing the Photos Library
The preceding code would display only .bursts and .livePhotos stored in the Photos
Library but nothing else. Rather than specifically defining which images to display, you
can also specifically define which images not to display by using .not to specify what type
of image or video not to display like this:
PhotosPicker(selection: $selectedItems,
matching: .any(of: [.videos, .not(.cinematicVideos)])) {
Text("Open the Photo Library")
}
To see how to select which items to view and which items to hide in the Photos
Library, follow these steps:
3. Add import PhotosUI under the import SwiftUI line like this:
import PhotosUI
5. Add the following inside the var body: some View like this:
VStack {
PhotosPicker(selection: $selectedItems,
matching: .any(of: [wantedAssets,
.not(notWantedAssets)])) {
Text("Open the Photo Library")
}
HStack {
Text("Items to view")
Picker(selection: $wantedAssets) {
Text("Bursts").tag(PHPickerFilter.bursts)
Text("Cinematic videos").tag(PHPickerFilter.
cinematicVideos)
305
Chapter 16 Accessing the Photos Library
306
Chapter 16 Accessing the Photos Library
} label: {
Text("Non-viewable items")
}
}
}
}
import SwiftUI
import PhotosUI
HStack {
Text("Items to view")
Picker(selection: $wantedAssets) {
307
Chapter 16 Accessing the Photos Library
Text("Bursts").tag(PHPickerFilter.bursts)
Text("Cinematic videos").tag(PHPickerFilter.
cinematicVideos)
Text("Depth effects photos").
tag(PHPickerFilter.depthEffectPhotos)
Text("Images").tag(PHPickerFilter.images)
Text("Live photos").tag(PHPickerFilter.
livePhotos)
Text("Screen recordings").tag(PHPickerFilter.
screenRecordings)
Text("Screenshots").tag(PHPickerFilter.
screenshots)
Text("Slow motion videos").tag(PHPickerFilter.
slomoVideos)
Text("Time lapse videos").tag(PHPickerFilter.
timelapseVideos)
Text("Videos").tag(PHPickerFilter.videos)
} label: {
Text("Viewable items")
}
}
HStack {
Text("Items to filter out")
Picker(selection: $notWantedAssets) {
Text("Bursts").tag(PHPickerFilter.bursts)
Text("Cinematic videos").tag(PHPickerFilter.
cinematicVideos)
Text("Depth effects photos").
tag(PHPickerFilter.depthEffectPhotos)
Text("Images").tag(PHPickerFilter.images)
Text("Live photos").tag(PHPickerFilter.
livePhotos)
Text("Screen recordings").tag(PHPickerFilter.
screenRecordings)
308
Chapter 16 Accessing the Photos Library
Text("Screenshots").tag(PHPickerFilter.
screenshots)
Text("Slow motion videos").tag(PHPickerFilter.
slomoVideos)
Text("Time lapse videos").tag(PHPickerFilter.
timelapseVideos)
Text("Videos").tag(PHPickerFilter.videos)
} label: {
Text("Non-viewable items")
}
}
}
}
}
309
Chapter 16 Accessing the Photos Library
10. Tap the “Open the Photos Library” PhotosPicker text to display
the Photos Library. Notice that the Photos Library only shows the
type of images/videos you defined and does not show the images/
videos you specified you did not want to see.
11. Repeat steps 8–10 to experiment with viewing and hiding different
types of images/videos.
12. Click the Stop icon (or choose Product ➤ Stop) in Xcode.
310
Chapter 16 Accessing the Photos Library
Summary
Many people capture still images and videos using the cameras on their iOS devices.
By accessing the Photos Library, you can select one or more images/videos using the
PhotosPicker. The PhotosPicker lets you define filters to choose only to view the type of
images/videos you want and specify the type of images/videos you don’t want. Accessing
the Photos Library gives apps a way to retrieve previously saved images/videos.
311
CHAPTER 17
313
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_17
Chapter 17 Using Machine Learning
314
Chapter 17 Using Machine Learning
The advantage of simply using a trained machine learning model is that you can add
artificial intelligence to your iOS apps quickly and easily. The drawback is that you need
to find trained machine learning models that do what you need. In addition, you cannot
increase the trained machine learning model’s intelligence. You’re essentially taking a
fixed machine learning model that won’t improve over time.
Since most people aren’t able to write machine learning algorithms and train it with
large amounts of data, they must rely on machine learning models that others have
created. There are two sources of machine learning models:
• Core ML models
• Non-Core ML models
When you add a machine learning model to an iOS project, it must be stored in a file
format known as Core ML (which stands for Core Machine Learning). Since Core ML
is a new file format, most machine learning models are stored in different file formats.
Fortunately, Apple has converted some popular machine learning models into the Core
ML format. That means you can use these machine learning models in your iOS apps
right away.
The main purpose for adding machine learning to your iOS apps is so your app
can anticipate the user’s needs. Essentially, machine learning lets your app become
smarter. The smarter your app is able to respond to the user, the happier the user will be.
Machine learning gives your app new capabilities without requiring you to exhaustively
write instructions yourself.
315
Chapter 17 Using Machine Learning
Note One huge problem with machine learning is that it’s only as smart as the
data it’s trained on and the algorithms created by programmers who may have
unknowingly added bias to their algorithms. Since facial recognition algorithms are
typically written by men, many facial recognition systems have trouble recognizing
minority women.
Likewise, if a machine learning model is trained with faulty data, it will never be
accurate. Microsoft once created a chatbot nicknamed Tay, which learned from
comments people typed in over the Internet. So people flooded Tay with sexist
and racist comments. As a result, Tay gradually learned to use sexist and racist
comments in all of its replies until Microsoft pulled the plug on Tay.
What this chapter will focus on is finding Core ML machine learning models, adding
them to iOS projects, and using them in your iOS app.
316
Chapter 17 Using Machine Learning
Figure 17-1. Core ML models briefly describe what the model does
By clicking the button to view models, you can see more details about the
different variations available. Generally, the larger the Core ML model, the more
accurate it will be. Figure 17-2 shows the different variations of the SqueezeNet image
recognition model.
317
Chapter 17 Using Machine Learning
Figure 17-2. Viewing details about Core ML models can define different file sizes
Since the size of Core ML models can vary dramatically, you need to weigh the
benefits of each model with its size. Adding 5 MB to the size of your iOS app may be
reasonable, but adding 553.5 MB may not. There’s often a trade-off between large file
size and greater accuracy, but sometimes smaller models can outperform larger ones, so
you may need to experiment with different models until you find the right one for your
app that balances accuracy and file size.
318
Chapter 17 Using Machine Learning
Image Recognition
At the time of this writing, most of the Core ML models available on Apple’s machine
learning site focus on image recognition. This can work in two ways:
First, we’ll start simple and add an image to an Xcode project. This will only allow us
to recognize that single image hard-coded into the app, but it will let us focus on getting
the Core ML model working within an app. Once we know the Core ML model works, we
can focus on the nonmachine learning functions to retrieve an image from the Photos
app or from the iPhone/iPad camera.
The first step is to download a Core ML model to your computer. Visit https://
developer.apple.com/machine-learning and download the MobileNet and
SqueezeNet models. Both models focus on image recognition, and both are fairly small
in size. By experimenting with two different Core ML models, you can see how accurate
both of them might be and how using any Core ML model works in similar ways.
The second step is to visit any search engine and look for images of any object such
as a car, dog, computer, or bird. The exact image doesn’t matter, but choose an image
that has a blank background such as all white. By choosing an image that’s isolated
and not cluttered with other items, you’ll improve the Core ML model’s chance of
recognizing it correctly.
Obviously, in real apps, you can’t always choose pictures that are easiest for the
Core ML model to identify, but for our purposes, we just want to get the Core ML model
working to identify items in a picture. Once you have downloaded the Core ML models
(SqueezeNet and MobileNet) and a single image of any object, you’re ready to create the
Xcode project to use machine learning.
To see how to use a Core ML file, follow these steps:
319
Chapter 17 Using Machine Learning
Figure 17-3. Adding the MobileNet and SqueezeNet models to the Navigator pane
import CoreML
import Vision
320
Chapter 17 Using Machine Learning
7. Drag and drop the same images into the Navigator pane, and
when a dialog appears, click the Finish button. Make sure these
image files have a descriptive name such as “cat.jpg” and that
they have the same file extension such as .png or .jpg. Figure 17-4
shows several images stored in the Navigator pane.
9. Click the Preview tab. Xcode displays a box labeled Drag or Add
Images to test the model’s accuracy.
10. Drag and drop an image into the Drag or Add Images box.
The Preview box displays the model’s accuracy as shown in
Figure 17-5.
321
Chapter 17 Using Machine Learning
322
Chapter 17 Using Machine Learning
The last State variable simply loads an image stored in the Assets
folder, so make sure you replace “cat” with the name of an image
you dragged and dropped into the Assets folder.
This array contains the names of all the images stored in the
Assets folder. Make sure you change these names to the exact
names of each image in the Assets folder.
Note Your images must be referenced in three separate areas: in the Assets
folder, in the Navigator pane, and by name in the photoArray.
VStack {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250, height: 250)
TextEditor(text: $message)
.padding()
Button {
useAI(sentImage: photoArray[arrayIndex])
} label: {
Text("Analyze Image")
}.padding()
}
Notice that the Button calls a function called useAI, which we’ll
add later.
323
Chapter 17 Using Machine Learning
15. Underneath the Button, add an HStack with two Buttons like this:
HStack {
Button {
if arrayIndex == 0 {
arrayIndex = photoArray.count - 1
} else {
arrayIndex -= 1
}
message = ""
image = UIImage(named: photoArray[arrayIndex])!
} label: {
Image(systemName: "chevron.left.square.fill")
}
Button {
if arrayIndex == photoArray.count - 1 {
arrayIndex = 0
} else {
arrayIndex += 1
}
message = ""
image = UIImage(named: photoArray[arrayIndex])!
} label: {
Image(systemName: "chevron.right.square.fill")
}
}
These two Buttons display a left/right arrow that displays the previous or next item
organized in photoArray. At this point, we’ve created the user interface. Now it’s time to
write Swift code. The machine learning model needs an image as input, so we need to
identify the image file you added to your Xcode project. This involves two steps. First,
you need to identify the file name, file extension of the image, and path of that image.
Second, you need to store this information as a URL to give to the Core ML model.
324
Chapter 17 Using Machine Learning
If the guard statement determines that the image file exists, then it
loads the location of that image file into the imageURL constant.
4. Add the following line under the previously added line in the
function:
325
Chapter 17 Using Machine Learning
5. Add the following line under the previously added line in the
function:
Now the next step is to let the Core ML model examine the image.
We already defined the image name, extension, and path in the
“imageURL” constant, so we can use this to define an image
request.
6. Add the following line under the previously added line in the
viewDidLoad method:
7. Add the following two lines under the previously added line in the
viewDidLoad method:
The last step is to write the function for the completion handler,
which is called “findResults”.
326
Chapter 17 Using Machine Learning
327
Chapter 17 Using Machine Learning
import SwiftUI
import CoreML
import Vision
struct ContentView: View {
let photoArray = ["cat", "plane", "banana", "car"]
@State var message = ""
@State var arrayIndex = 0
@State var image: UIImage = UIImage(named: "cat")!
328
Chapter 17 Using Machine Learning
.padding()
Button {
useAI(sentImage: photoArray[arrayIndex])
} label: {
Text("Analyze Image")
}.padding()
HStack {
Button {
if arrayIndex == 0 {
arrayIndex = photoArray.count - 1
} else {
arrayIndex -= 1
}
message = ""
image = UIImage(named: photoArray[arrayIndex])!
} label: {
Image(systemName: "chevron.left.square.fill")
}
Button {
if arrayIndex == photoArray.count - 1 {
arrayIndex = 0
} else {
arrayIndex += 1
}
message = ""
image = UIImage(named: photoArray[arrayIndex])!
} label: {
Image(systemName: "chevron.right.square.fill")
}
}
}
}
func useAI(sentImage: String) {
guard let imagePath = Bundle.main.path(forResource:
sentImage, ofType: "jpg") else {
message = "Image not found"
329
Chapter 17 Using Machine Learning
return
}
let imageURL = NSURL.fileURL(withPath: imagePath)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
330
Chapter 17 Using Machine Learning
ContentView()
}
}
13. Click the Live icon on the Canvas pane. The simulated iOS screen
appears, displaying the image you added to the image view along
with the Core ML model’s guess and confidence level of the image
as shown in Figure 17-6.
331
Chapter 17 Using Machine Learning
If you modify your project by adding a different Core ML model, you’ll notice that
each Core ML model identifies the same object in slightly different ways with different
confidence levels. You can also try adding different images to the Assets folder and
Navigator pane of your project such as images containing apples, trees, horses, or trains
to see how accurately it identifies the displayed item. Machine learning models aren’t
perfect, but you can see that we’ve created an app that can identify items with very little
coding. Instead, we’ve let a trained machine learning model do all the hard work of
identifying items in an image.
Detecting Languages
While artificial intelligence often requires training a model and adding it to a project,
Apple has provided various frameworks that offer artificial intelligence as well. One
interesting framework is the NaturalLanguage framework (https://developer.
apple.com/documentation/naturallanguage), which can identify languages based on
examining one or more words. Rather than add a machine learning model to gain these
features, we just need to import the NaturalLanguage framework into any project.
To see how to use the NaturalLanguage framework to detect languages, follow
these steps:
import NaturalLanguage
332
Chapter 17 Using Machine Learning
5. Add a VStack underneath the var body: some View line and add a
TextField, Text, and Button like this:
VStack {
TextField("Type foreign text here", text: $language)
.disableAutocorrection(true)
.padding()
Text("Language = \(message)")
.padding()
Button {
if let prediction = predictLanguage(text: language) {
message = prediction
}
} label: {
Text("Identify Language")
}
}
6. Add the following function above the last curly bracket of the
struct ContentView: View:
recognizer.processString(text)
333
Chapter 17 Using Machine Learning
import SwiftUI
import NaturalLanguage
334
Chapter 17 Using Machine Learning
recognizer.processString(text)
9. Click the Identify Language Button. The app displays the language
it thinks you typed into the TextField as shown in Figure 17-7.
335
Chapter 17 Using Machine Learning
Summary
By adding artificial intelligence to your app, you can add additional features without
writing a lot of code yourself. Core ML machine learning models can give your app the
ability to recognize items in images, but they still have limitations in failing to recognize
everything with perfect accuracy. Not all Core ML models are equal in accuracy or size,
so you may need to experiment with different Core ML models until you find the one
that works best with your app.
The main idea behind Core ML is that you can add the ability to deal with unknown
data by simply using a trained machine learning model. Apple will continue adding new
Core ML models that will likely offer different features beyond just image recognition.
Over time, you’ll be able to add these trained machine learning models to your apps and
give your app artificial intelligence with little effort.
336
CHAPTER 18
337
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_18
Chapter 18 Using Bottom Sheets
Figure 18-1. Bottom sheets can appear partially on the screen or covering the
entire screen
In Figure 18-1, the bottom sheet partially covers the bottom of the screen. When
pulled up to fill the entire screen, the bottom sheet provides more information. All of this
information can then be pulled down completely to hide it from view.
338
Chapter 18 Using Bottom Sheets
Next, the bottom sheet must be attached to a view using the .sheet modifier like this:
.sheet(isPresented: $booleanStateVariable) {
}
.sheet(isPresented: $booleanStateVariable) {
Text("Just a Text view here")
.presentationDetents([.medium, .large])
}
The .presentationDetents defines two ways the bottom sheet can appear. When
first displayed, the first option (.medium in this example) defines how the bottom sheet
first appears when its Boolean State variable is true. The second option (.large in this
example) defines how the bottom sheet can appear next, where .large lets the bottom
sheet fill the entire screen.
To see how to create a simple bottom sheet, follow these steps:
VStack {
Toggle(isOn: $flag) {
Text("Toggle me")
}
}.sheet(isPresented: $flag) {
339
Chapter 18 Using Bottom Sheets
The Toggle simply switches the Boolean State variable flag from true
to false and vice versa. Then the .sheet modifier displays the bottom
sheet when this Boolean State variable flag is true. The contents of
the bottom sheet are defined by a single Text view (“Just a Text view”).
Finally, the initial appearance of the bottom sheet is defined by
.medium. Then its other appearance option is defined by .large,
which allows the bottom sheet to expand to fill the entire screen. (If
both options were identical such as [.medium, .medium], this would
only display the bottom sheet to fill half the screen but would not
allow the user to expand the bottom sheet to fill the entire screen.)
import SwiftUI
340
Chapter 18 Using Bottom Sheets
5. Click the Live icon in the Canvas pane. The bottom sheet remains
hidden because the Boolean State variable flag is false.
6. Click the Toggle. The bottom sheet appears near the bottom,
leaving part of the original screen visible as shown in Figure 18-2.
Figure 18-2. The initial appearance of the bottom sheet defined by .medium
7. Drag the handle in the top middle of the bottom sheet up.
The bottom sheet expands to fill the entire screen as shown in
Figure 18-3.
341
Chapter 18 Using Bottom Sheets
8. Drag the handle all the way down until the bottom sheet
disappears completely. Notice that when the bottom sheet is
hidden, the Toggle switches off because the Boolean State variable
flag is now false.
342
Chapter 18 Using Bottom Sheets
Since this choice between half height and full height might feel too restrictive,
SwiftUI gives you three options for customizing the size of a bottom sheet:
• Fixed height
• Custom height
.height(175)
To see how to create a fixed height bottom sheet, follow these steps:
VStack {
Toggle(isOn: $flag) {
Text("Toggle me")
}
}.sheet(isPresented: $flag) {
Text("Fixed height")
.presentationDetents([.height(125), .height(455)])
}
343
Chapter 18 Using Bottom Sheets
import SwiftUI
5. Click the Live icon in the Canvas pane. The bottom sheet remains
hidden because the Boolean State variable flag is false.
6. Click the Toggle. The bottom sheet appears near the bottom,
defined by the fixed height of 125 points.
7. Drag the handle to expand the bottom sheet as high as it can go.
Notice that this time the bottom sheet does not fill the entire
screen because its height is fixed at 455 points.
344
Chapter 18 Using Bottom Sheets
.fraction(0.25)
The preceding code defines a fractional height of 25% of the screen height. By
changing the numerical value, between 0 and 1, you can define different fractional
heights.
To see how to create a fractional height bottom sheet, follow these steps:
VStack {
Toggle(isOn: $flag) {
Text("Toggle me")
}
}.sheet(isPresented: $flag) {
Text("Fractional height")
.presentationDetents([.fraction(0.25), .fraction(0.85)])
}
345
Chapter 18 Using Bottom Sheets
import SwiftUI
5. Click the Live icon in the Canvas pane. The bottom sheet remains
hidden because the Boolean State variable flag is false.
6. Click the Toggle. The bottom sheet appears near the bottom,
defined by the fractional height of 25% of the screen height.
346
Chapter 18 Using Bottom Sheets
• A minimum height
By defining two values, SwiftUI will use the largest value. So if you define a minimum
height of 400 and a 75% height of the screen, SwiftUI will compare which value is larger
and then use that larger value. On some screens, the larger height might be 400 points,
and on other screens, the 75% height might be the larger height.
To define a custom height, there are two steps. First, you must define a structure that
conforms to CustomPresentationDetent. Within this structure, you define a minimum
fixed height value and a percentage of the screen height:
The preceding code defines a fixed minimum height of 20 points or a 10% height of
the screen. Whichever value is larger will be the value used.
To see how to create a custom height bottom sheet, follow these steps:
347
Chapter 18 Using Bottom Sheets
VStack {
Toggle(isOn: $flag) {
Text("Toggle me")
}
}.sheet(isPresented: $flag) {
Text("Custom height")
.presentationDetents([.custom(InitialDetent.self), .custom
(ExpandedDetent.self)])
}
import SwiftUI
348
Chapter 18 Using Bottom Sheets
Text("Custom height")
.presentationDetents([.custom(InitialDetent.self),
.custom(ExpandedDetent.self)])
}
}
}
6. Click the Live icon in the Canvas pane. The bottom sheet remains
hidden because the Boolean State variable flag is false.
7. Click the Toggle. The bottom sheet appears near the bottom,
defined by either the fixed height of 20 points or 10% of the
screen height.
349
Chapter 18 Using Bottom Sheets
Summary
Bottom sheets can be a handy way to provide additional information to the user.
Although a bottom sheet can contain just a single view such as a Text view, it’s far more
common to display an entire structure containing multiple views such as text, images,
and buttons (see Figure 18-1).
By default, the height of the bottom sheet appears about halfway up the screen and
expands to fill the entire screen. You can define a fixed height for both values, a fractional
height, or a custom height. You can even mix two different options such as a fixed height
for the initial appearance of the bottom sheet and a fractional height for the expanded
appearance of the bottom sheet.
Bottom sheets give apps another way to display information without forcing users
to leave the current screen. By using bottom sheets, your app can provide useful
information that’s easy to display and hide at any time.
350
CHAPTER 19
Using ViewThatFits
and AnyLayout
User interfaces need to adapt to different screen sizes and different orientations
(landscape and portrait). SwiftUI can adapt easily to different screen sizes, but
sometimes a user interface designed for portrait orientation looks odd when viewed in
landscape orientation and vice versa.
Rather than compromise and try to design a less than optimal user interface that
looks okay in both portrait and landscape orientations, it can be better to optimize two
different user interfaces: one optimized for portrait orientation and one optimized for
landscape orientation. While this can create extra work, it can also create a far better user
interface experience for the user.
To help create the best user interface experience possible, SwiftUI offers two similar
options called ViewThatFits and AnyLayout.
Using ViewThatFits
ViewThatFits is a new view that acts like a stack that can contain two different views.
Based on the current orientation of an iOS device, ViewThatFits determines which
view to display. That means you can create one view optimized for portrait (vertical)
orientation and a second view optimized for landscape (horizontal) orientation.
ViewThatFits looks like this:
ViewThatFits {
}
351
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_19
Chapter 19 Using ViewThatFits and AnyLayout
Within the ViewThatFits curly brackets, you can define two different views. The
simplest views can be a single view like a Text view such as
ViewThatFits {
Text ("Short text")
.frame(width: 300, height: 500)
In portrait orientation, the preceding code would display the “Short text” Text view
as shown in Figure 19-1.
When the iOS device switches to landscape orientation, the ViewThatFits chooses
the Text view displaying a large amount of text defined by a wider frame width (800) as
shown in Figure 19-2.
352
Chapter 19 Using ViewThatFits and AnyLayout
Figure 19-2. ViewThatFits displays the wider Text view in landscape orientation
To see how to use ViewThatFits to create custom portrait and landscape orientation
user interfaces, follow these steps:
353
Chapter 19 Using ViewThatFits and AnyLayout
9. Edit the VerticalView file so the entire file looks like this:
import SwiftUI
Circle()
.frame(width: 250, height: 200)
.foregroundColor(.green)
Capsule()
.frame(width: 250, height: 200)
.foregroundColor(.blue)
Text("Vertical View")
.font(.largeTitle)
}.padding()
}
}
355
Chapter 19 Using ViewThatFits and AnyLayout
import SwiftUI
356
Chapter 19 Using ViewThatFits and AnyLayout
Circle()
.frame(width: 250, height: 200)
.foregroundColor(.green)
Capsule()
.frame(width: 250, height: 200)
.foregroundColor(.blue)
}.padding()
Text("Horizontal View")
.font(.largeTitle)
}
}
}
357
Chapter 19 Using ViewThatFits and AnyLayout
13. Click the Live icon in the Canvas pane. Notice that in portrait
orientation, the user interface displays the VerticalView as shown
in Figure 19-7.
358
Chapter 19 Using ViewThatFits and AnyLayout
14. Click the Device Settings icon and click the Orientation toggle
to turn it on. Make sure one of the Landscape options’ button is
selected as shown in Figure 19-8.
359
Chapter 19 Using ViewThatFits and AnyLayout
Figure 19-8. The Device Settings icon lets you change the orientation of the
simulated iOS device
Notice that in landscape orientation, the ViewThatFits now chooses to display the
HorizontalView file as shown in Figure 19-9.
360
Chapter 19 Using ViewThatFits and AnyLayout
Using AnyLayout
AnyLayout lets you switch between a horizontal stack layout and a vertical stack layout.
To use AnyLayout, you must first define a Boolean State variable like this:
Next, you must define an arbitrarily named constant that uses the Boolean State
variable to switch between a horizontal stack and a vertical stack layout like this:
The preceding code says if the changeMe variable is true, use the HStackLayout.
Otherwise, use the VStackLayout. Once we’ve defined this constant, we can use it within
a VStack or an HStack like this:
VStack {
myLayout {
}
}.padding()
361
Chapter 19 Using ViewThatFits and AnyLayout
This code says that anything within the curly brackets of myLayout will change
between a horizontal stack layout and a vertical stack layout.
Changing the Boolean State variable will automatically change the layout. To
make this switch between a horizontal stack layout and a vertical stack layout appear
animated, we can use withAnimation and change the Boolean State variable within
like this:
withAnimation {
changeMe.toggle()
}
4. Under the var body: some View line, define a horizontal stack
layout and a vertical stack layout using AnyLayout like this:
VStack {
myLayout {
}
}.padding()
6. Add a Button above the last curly bracket of the VStack but
underneath the last curly bracket of myLayout like this:
VStack {
myLayout {
}
362
Chapter 19 Using ViewThatFits and AnyLayout
Button("Change layout") {
withAnimation {
changeMe.toggle()
}
}
}.padding()
myLayout {
Rectangle()
.frame(width: changeMe ? 100 : 250, height: changeMe ?
100 : 200)
.foregroundColor(.red)
Circle()
.frame(width: changeMe ? 125 : 250, height: changeMe ?
100 : 200)
.foregroundColor(.green)
Capsule()
.frame(width: changeMe ? 125 : 250, height: changeMe ?
100 : 200)
.foregroundColor(.blue)
}
import SwiftUI
363
Chapter 19 Using ViewThatFits and AnyLayout
VStack {
myLayout {
Rectangle()
.frame(width: changeMe ? 100 : 250, height:
changeMe ? 100 : 200)
.foregroundColor(.red)
Circle()
.frame(width: changeMe ? 125 : 250, height:
changeMe ? 100 : 200)
.foregroundColor(.green)
Capsule()
.frame(width: changeMe ? 125 : 250, height:
changeMe ? 100 : 200)
.foregroundColor(.blue)
}
Button("Change layout") {
withAnimation {
changeMe.toggle()
}
}
}.padding()
}
}
364
Chapter 19 Using ViewThatFits and AnyLayout
8. Click the Live icon on the Canvas pane. Because the Boolean
State variable changeMe is initially set to false, the initial layout is
VStackLayout. In addition, the width and height of the rectangle,
circle, and capsule also use the second value listed within its
.frame modifier. For example, the Capsule() uses a width of 250
and a height of 200 as shown in Figure 19-10.
365
Chapter 19 Using ViewThatFits and AnyLayout
Summary
SwiftUI can typically adapt user interfaces to any screen size and orientation. However,
sometimes you may want to create an optimized user interface for both portrait
and landscape orientations. In that case, you can define a user interface for portrait
orientation and a second user interface for landscape orientation and use ViewThatFits
to automatically choose the best user interface for portrait or landscape orientation.
If you prefer to give the user an option to switch between vertical and horizontal
layouts, use AnyLayout instead. Then define all the views that you want to alternate
between vertical and horizontal layouts.
By using ViewThatFits and AnyLayout, you can create user interfaces that look best
no matter what orientation of the iOS device the app runs on.
366
CHAPTER 20
Handling Errors
In a perfect world, you would write code, run it, and it would work exactly as you
expected. Unfortunately, we don’t live in a perfect world, and in our world, writing
software is never simple and easy. In most cases, you’ll write code that almost works, but
not quite. Then the majority of your time will be spent trying to tweak your code to make
it finally work the way you want.
When writing any type of software, always expect the unexpected. In other words,
you can never anticipate all possible problems your program might face, but you
have to plan for it anyway. Suppose your app needs to retrieve data from a server over
the Internet. While your code may work perfectly well, there are so many unknown
situations that are completely out of your control such as
• Is the Internet connection reliable or does it lose data being sent and
received?
With so many unknowns to handle, you might write code that works perfectly well
under ideal circumstances, but fails dramatically when one little thing goes wrong that’s
out of your control. Rather than let your app crash and frustrate the user, it’s much better
to write code that can handle all possible problems.
367
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_20
Chapter 20 Handling Errors
Error handling doesn’t necessarily mean that your code will be able to fix or avoid
problems, but that your app won’t crash or go haywire when an error does occur. Error
handling lets your app deal with possible problems even though you may not even
know what that problem might be. Essentially, error handling means your app will keep
running and deal with errors gracefully.
3. Drag and drop a picture into the Assets folder. In this example, the
picture file is called “car.jpg” but you’ll need to replace the code
with the name of the picture file you added to the Assets folder.
368
Chapter 20 Handling Errors
5. Add a VStack, Image, and Button underneath the var body: some
View line like this:
VStack {
Image(uiImage: showImage)
.resizable()
.scaledToFit()
.frame(width: 300, height: 400)
Button {
getImage()
} label: {
Text("Add Picture")
}
}
6. Add the following function above the last curly bracket of the
struct ContentView: View like this:
func getImage() {
showImage = UIImage(named: "car")!
}
Notice that this function tries to load an image file stored in the
Assets folder called “car”. The assumption is that the “car” image
file exists. The entire ContentView file should look like this:
import SwiftUI
369
Chapter 20 Handling Errors
Button {
getImage()
} label: {
Text("Add Picture")
}
}
}
func getImage() {
showImage = UIImage(named: "car")!
}
}
8. Click the Add Picture Button. Notice that the image you stored in
the Assets folder appears on the user interface.
The last State variable simply loads an image stored in the Assets folder, so make
sure you replace “car” with the name of an image you dragged and dropped into the
Assets folder.
At this point, it’s easy to assume your code works correctly, but the assumption is
that the proper image file exists. To see the problem with this assumption, change the
name of the image file to a nonexistent image file like this:
func getImage() {
showImage = UIImage(named: "xyz")!
}
Click the Live icon on the Canvas pane and then click the Add Picture Button again.
Because the “xyz” image file does not exist, your app will crash as shown in Figure 20-1.
370
Chapter 20 Handling Errors
Figure 20-1. An app will crash when it can’t handle unexpected errors
Ideally, your code should test if a particular image file exists before trying to load it. If
it does not, then load a default image. Change the getImage function as follows:
func getImage() {
if UIImage(named: "xyz") != nil {
showImage = UIImage(named: "xyz")!
} else {
showImage = UIImage(systemName: "xmark.octagon.fill")!
}
}
Although the preceding if-then statement works, it’s not immediately obvious that
this if-then statement is checking for possible problems. To make it clear that your code
is checking or guarding against possible problems, it’s better to use the guard statement
instead.
The guard statement makes it blatantly obvious that it exists solely to guard against a
possible error. Rewrite the getImage function as follows:
371
Chapter 20 Handling Errors
Note Think of the guard statement as defining what you want. In the preceding
example, we want the UIImage(named: imageName) to contain an actual file. In
other words, we want the UIImage to contain any non-nil value.
You’ll also need to change the function call to pass it a string like this:
getImage(imageName: "xyz")
import SwiftUI
372
Chapter 20 Handling Errors
Notice that the guard statement checks to see if it can load the image file or not. If
not, then it loads the “xmark.octagon.fill” image instead and immediately stops the rest
of the code in the function from running (return). However, if the image file does exist,
the guard statement lets the rest of the code to run, which loads the image file to appear
inside the Image view.
Guard statements are often used in the beginning of functions to check that the
function received valid data before trying to run. By using guard statements to protect or
guard against possible errors, you can reduce the chance that your app will crash when it
doesn’t receive expected data.
do {
// try something here
} catch {
// catch errors here
}
The try keyword calls a function defined with the throws keyword like this:
Once a function has been defined to throw an error, you can call that particular
function using the try keyword like this:
try someFunction()
373
Chapter 20 Handling Errors
To catch errors, we need to create an enumeration that lists different types of problems
that might occur. This enumeration must conform to the Error protocol so that it can
return an error value from inside a function. A typical enumeration might look like this:
To see how to use the do-try-catch statement to catch possible errors, follow
these steps:
The last two State variables are Float data types because they
represent the value of Sliders, which store values as Float
data types.
4. Add a VStack underneath the var body: some View line and add
two HStacks that contain a Text and a Slider view. Then add a
Button and a Text view like this:
VStack {
HStack {
Text("Numerator = \(numeratorSlider)")
Slider(value: $numeratorSlider, in: -20...20, step: 1.0)
}
HStack {
Text("Denominator = \(denominatorSlider)")
Slider(value: $denominatorSlider, in: -20...20, step: 1.0)
}
374
Chapter 20 Handling Errors
Button {
divideFunction(numerator: Double(numeratorSlider),
denominator: Double(denominatorSlider))
} label: {
Text("Divide")
}
Text(message)
}
The two HStacks display a Text view and a Slider side by side.
Then the Button and Text view appear underneath as shown in
Figure 20-2.
5. Add the following function above the last curly bracket of the
struct ContentView: View:
This function accepts two numbers (Double data types) and uses
two guard statements. Notice that the function is defined by the
throws keyword. That means to call this function, we’ll need to use
the try keyword. Inside this function are two guard statements.
375
Chapter 20 Handling Errors
6. Add the following function above the last curly bracket of the
struct ContentView: View:
Notice that there are multiple catch portions that catch all
the errors defined by the MajorProblems enumeration.
The catch MajorProblems.divideByZero looks for problems
trying to divide by zero, while the catch MajorProblems.
noNegativeNumbersPlease looks for negative numbers.
The last catch portion runs in case neither the division by zero
nor negative number error runs. This catch portion will catch any
problems that may not be foreseen ahead of time.
376
Chapter 20 Handling Errors
import SwiftUI
377
Chapter 20 Handling Errors
9. Click the Divide Button. Notice that the message “Can’t divide by
zero” appears. That’s because the MajorProblems.divideByZero
error has been thrown by the checkMe function.
378
Chapter 20 Handling Errors
11. Drag the two Sliders so they both represent a positive number and
then click the Divide Button. Notice that the division now works as
expected.
The preceding if let statement checks if the try? checkMe2 function returns a value. If
so, then store that answer in the message State variable. If the checkMe2 function returns
an optional, then display “Error occurred”. To use the preceding code, the checkMe2
function needs to look like this:
379
Chapter 20 Handling Errors
Notice that the .errorOccurred is stored in the MajorProblems enum, which is now
defined like this:
import SwiftUI
380
Chapter 20 Handling Errors
HStack {
Text("Denominator = \(denominatorSlider)")
Slider(value: $denominatorSlider, in: -20...20,
step: 1.0)
}
Button {
divideFunction(numerator: Double(numeratorSlider),
denominator: Double(denominatorSlider))
} label: {
Text("Divide")
}
Text(message)
}
}
381
Chapter 20 Handling Errors
6. Click the Divide Button. Notice that the message “Error occurred”
appears. That’s because the MajorProblems.divideByZero error
has been thrown by the checkMe function.
8. Drag the two Sliders so they both represent a positive number and
then click the Divide Button. Notice that the division now works as
expected.
The try keyword must call a function within a do-catch statement. The try? keyword
returns an optional variable. The third variation is the try! keyword.
Like try?, the try! keyword also returns an optional variable. However, the try!
keyword unwraps that optional value without checking if it’s nil or not. If it is a nil value,
then the app will crash (see Figure 20-1). To see how the try! keyword works, edit the
TryApp project with the following divideFunction:
The preceding function will work as long as the denominator is not zero or both the
numerator and denominator are positive. Otherwise, the try! keyword will crash if there’s
an error.
382
Chapter 20 Handling Errors
Since the try! keyword can be so dangerous, use it only if you’re positive that
there’s no possibility of an error. Otherwise, use the try keyword within a do-try-catch
statement, or use the try? keyword and check for an optional value.
Summary
Never assume that your program will receive the data it needs. Instead, always assume
that you need to check for valid data at all times. By constantly checking to verify that
your program receives the data it expects, you can minimize the chances that your app
won’t crash.
The simplest way to verify data is correct is to use an if-then statement. To make it
clear that you’re verifying data, it’s better to use a guard statement, which works much
like an if-then statement. With a guard statement, you define what type of data you want.
If the guard statement fails, then it exits out of a function without running the rest of the
function’s code. This prevents possible crashes by trying to manipulate invalid data.
Yet another error handling method is to use the try keyword to call a function
defined by the throws keyword. When calling a function that can throw an error, you
must call that function within a do-try-catch statement where the catch portion can trap
one or more possible errors.
To avoid using the do-try-catch statement, you can also use the try? keyword. If
the try? keyword calls a function that throws an error, the try? keyword will retrieve an
optional variable, which could be a nil value. Both the try and try? keywords help catch
possible errors that could crash your app.
The try! keyword unwraps any optional variables, which could be dangerous if the
optional value is nil. For that reason, use the try! keyword sparingly and rely on the try or
try? keywords instead.
383
CHAPTER 21
385
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1_21
Chapter 21 Odds and Ends
Note You can only preview your app’s launch screen by running it on the Simulator
or on an actual iOS device. You cannot preview a launch screen in the Canvas pane.
A Launch Screen file is a storyboard file that lets you place items like text, images,
and shapes on specific locations on an iOS screen. Since your app may appear on
different size iPhone and iPad screens, the Launch Screen file uses something called
constraints that align text (stored in a label) in the center of the screen. That way, no
matter what iOS screen size your app runs on, it will always appear centered.
To see how to create a launch screen, follow these steps:
386
Chapter 21 Odds and Ends
4. Click Next. Another dialog appears letting you choose a name for
your Launch Screen file and a location to save it.
5. Keep the default name “Launch Screen” and click Create. Xcode
adds your LaunchScreen storyboard file to the Navigator pane as
shown in Figure 21-2.
387
Chapter 21 Odds and Ends
Figure 21-3. Defining the launch screen file as the launch screen
388
Chapter 21 Odds and Ends
389
Chapter 21 Odds and Ends
390
Chapter 21 Odds and Ends
14. Choose a distinctive color. Notice that the entire Launch Screen
background changes to the color you selected.
15. Click the Run button or choose Product ➤ Run to run the app in
the Simulator, or connect an iPhone or iPad to your Macintosh
through its USB cable and make sure you choose your iPhone or
iPad at the top of the Xcode window. The launch screen appears
briefly before disappearing and displaying the ContentView.
VStack {
Image(systemName: "wifi", variableValue: value)
.font(.custom("", size: 125))
.foregroundColor(.red)
Slider(value: $value) {
}
}
.font(.largeTitle)
391
Chapter 21 Odds and Ends
import SwiftUI
Slider(value: $value) {
}
}
.font(.largeTitle)
}
}
6. Drag the Slider to the right. Notice that as you drag the Slider to
the right (increasing the value that the Slider represents), the
WiFi SF Symbol icon keeps highlighting in color as shown in
Figure 21-6.
392
Chapter 21 Odds and Ends
Summary
A launch screen can help define your app’s name, logo, and visual style as soon as the
user starts it. Some developers like using launch screens to customize the appearance of
their app, while others prefer not to use a launch screen, so decide what’s best for your
project.
SF Symbol icons can be handy to use to create standard icons without the need to
create and design them yourself. By coloring an SF Symbol icon gradually, you can show
progress. Such a minor change can make your app look distinctive and playful, which
can increase a user’s enjoyment.
Ultimately, the purpose of any app is to solve a problem, but subtle touches like
gradually colored SF Symbol icons can make any app’s user interface more enjoyable to
use on a consistent basis.
393
Index
A sheet modifier, 339
steps, 339
AnyLayout
VStack file, 339
ContentView file, 363, 364
horizontal/vertical stack layout, 361,
362, 366 C
steps, 362
Closures
vertical stack layout, 365
data, 54–56
VStack, 361
func keyword, 49, 50
Artificial intelligence (AI), 313, 315,
parameter, 52, 53
332, 336
process information, 49
Audio/video files
run option, 51
AVFoundation project, 242
source code, 51
ContentView file, 246
steps, 50
file formats, 241
trailing data
navigator pane, 245
advantages, 56–58
PlaySound file, 244
pass parameters, 58, 59
steps, 243
return values, 59, 60
Swift file, 243
value capturing, 54
videos (see Video files)
Concurrency
web sites, 242
asynchronous function, 84–88
await keyword, 86, 87
B completion handlers, 85
Bottom sheets processing information, 84
ContentView file, 340 sequential processing risks, 83
custom heights, 347–349 time-consuming tasks, 83
customizing process, 343 user interface, 88–95
fractional height, 345, 346 Core Data model
entire screen, 337, 338, 341, 342 data model file, 108–113
fixed height sheet, 343, 344 definition, 107
NavigationStacks/TabViews, 337 entities/attributes, 107
screen view, 341 existing project
395
© Wallace Wang 2023
W. Wang, Pro iPhone Development with SwiftUI, https://doi.org/10.1007/978-1-4842-9544-1
INDEX
396
INDEX
G H, I, J, K
Gauges/progress views Handling errors, see Error handling
circular style creation, 185
ContentView file, 184, 185
current value, 183 L
descriptive text, 180, 181, 183 Launch screen file
horizontal bar, 179 background color menu, 390
Image/Label view, 180 creation, 386
minimum/maximum properties, 182 editor pane, 387
numeric value, 180 Inspector pane, 389
progress (see Progress view) popup menu, 388
spinning progress view, 179 SF Symbol icons/color, 391, 392
ValueLabel, 181 steps, 385
VStack file, 183 storyboard file, 386
Global Positioning System (GPS), 223 Localization
Grand Central Dispatch (GCD) canvas pane, 159, 160
asynchronous queues, 70 content view tab, 164
Button code, 73 dateStyle/timeStyle options, 169
ContentView file, 74, 76 dialog box, 155
definition, 63 Info pane, 152, 153
dispatch groups, 76–82 Inspector pane, 155, 156
key concept, 69 language previews, 158–161
playground code, 70, 72 languages, 150
queue, 70 LocalizableStringKey, 161–164
Run button, 71 navigator pane, 151, 157
source code, 70 numbers/dates format, 168–171
threads PROJECT heading, 151
advantage, 64 pseudolanguages
Button code, 66 app language list, 173
ContentView file, 64–67 ContentView file, 172
mutex (mutual exclusion), 69 display option, 172
process screen, 68 string text, 176, 177
steps, 64 user interface, 172
thread-safe, 69 Xcode displays, 174, 176
VStack, 65 steps, 151
user interface, 73 string interpolation, 164–168
397
INDEX
398
INDEX
399
INDEX
400
INDEX
401