2.
Getting Started
Written by Audrey Tam
SwiftUI is some of the most exciting news since Apple first announced Swift in 2014. It’s an enormous step towards Apple’s goal of getting everyone coding; it simplifies the basics so that you can spend more time on custom features that delight your users.
If you’re reading this book, you’re just as excited as I am about developing apps with this new framework. This chapter will get you comfortable with the basics of creating a SwiftUI app and (live-) previewing it in Xcode. You’ll create a small color-matching game, inspired by our famous BullsEye app from our book iOS Apprentice. The goal of the app is to try and match a randomly generated color by selecting colors from the RGB color space:
In this chapter, you will:
- Learn how to use the Xcode canvas to create your UI side-by-side with its code, and see how they stay in sync — a change to one side always updates the other side.
- Create a reusable view for the sliders seen in the image.
- Learn about
@State
variables and use them to update your UI whenever a state value changes. - Present an alert to show the user’s score.
Time to get started!
Getting started
Open the RGBullsEye starter project from the chapter materials, and build and run:
This app displays a target color with randomly generated red, green and blue values. The user moves the sliders to make the left color block match the right side. You’re about to create a SwiftUI app that does the exact same thing, but more swiftly!
Creating a new SwiftUI project
To start, create a new Xcode project (Shift-Command-N), select iOS ▸ Single View App, name the project RGBullsEye, then select SwiftUI in the User Interface menu:
Save your project somewhere outside the RGBullsEye-Starter folder.
In the project navigator, open the RGBullsEye group to see what you got: the AppDelegate.swift, which you may be used to seeing, is now split into AppDelegate.swift and SceneDelegate.swift. The latter creates the app’s window
:
SceneDelegate
itself isn’t specific to SwiftUI, but this line is:
window.rootViewController = UIHostingController(
rootView: contentView)
UIHostingController
creates a view controller for the SwiftUI view contentView
, created a few lines above as an instance of ContentView
.
Note:
UIHostingController
enables you to integrate SwiftUI views into an existing app. You’ll learn how in Chapter 4: “Integrating SwiftUI”. When the app starts,window
displays this instance ofContentView
, which is defined in ContentView.swift. It’s astruct
that conforms to theView
protocol:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
This is SwiftUI declaring that the body
of ContentView
contains a Text
view that displays Hello World.
Previewing your ContentView
Below the ContentView
struct, ContentView_Previews
contains a view that contains an instance of ContentView
:
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is where you can specify sample data for the preview, and you can compare different screen and font sizes. But where is the preview?
There’s a big blank space next to the code, with this at the top:
By default, the preview uses the currently active scheme.
Click Resume and wait a while to see the preview:
Note: If you don’t see the Resume button, click the Editor Options button and select Canvas:
If you still don’t see the Resume button, make sure you’re running macOS Catalina (10.15).
Note: Instead of clicking the Resume button, you can use the very useful keyboard shortcut Option-Command-P. It works even when the Resume button isn’t displayed immediately after you change something in the view.
Previewing in landscape
RGBullsEye looks best in landscape orientation. However, at the time of writing, Xcode 11 doesn’t provide an easy way to preview in landscape orientation. For now, you have to specify fixed width and height values.
Inside the static previews
property, add a previewLayout
modifier to ContentView()
:
ContentView()
.previewLayout(.fixed(width: 568, height: 320))
These values display an iPhone SE-sized window in landscape orientation.
You can find dimensions for other iPhone models in “The Ultimate Guide To iPhone Resolutions” at bit.ly/29Ce3Ip.
Note: To save some display space here, I set the editor layout to Canvas on Bottom.
Creating your UI
Your SwiftUI app doesn’t have a storyboard or a view controller — ContentView.swift takes over their jobs. You can use any combination of code and drag-from-object-library to create your UI, and you can perform storyboard-like actions directly in your code! Best of all, everything stays in sync all the time!
SwiftUI is declarative: You declare how you want the UI to look, and SwiftUI converts your declarations into efficient code that gets the job done. Apple encourages you to create as many views as you need to keep your code easy to read. Reusable parameterized views are especially recommended — it’s just like extracting code into a function, and you’ll create one later in this chapter.
For this chapter, you’ll mostly use the canvas, similar to how you’d layout your UI in Interface Builder (IB).
Some SwiftUI vocabulary
Before you dive into creating your views, there’s a little vocabulary you must learn.
-
Canvas and Minimap: To get the full SwiftUI experience, you need Xcode 11 and macOS 10.15. Then you’ll be able to preview your app’s views in the canvas, alongside the code editor. Also available is a minimap of your code: It doesn’t appear in my screenshots because I hid it: Editor ▸ Hide Minimap.
-
Modifiers: Instead of setting attributes or properties of UIKit objects, you can call modifier methods for foreground color, font, padding and a lot more.
-
Container views: If you’ve previously used stack views, you’ll find it pretty easy to create this app’s UI in SwiftUI, using
HStack
andVStack
container views. There are other container views, includingZStack
andGroup
. You’ll learn about them in Chapter 8: “Introducing Stacks and Containers”.
In addition to container views, there are SwiftUI views for many of the UIKit objects you know and love, like Text
, Button
and Slider
. The + button in the toolbar displays the Library of SwiftUI views.
Creating the target color block
In RGBullsEye, the target color block, which is the color your user is trying to match, is a Color
view above a Text
view. But body
is a computed property that returns a single View
, so you’ll need to embed them in a container view — a VStack
(vertical stack) in this scenario.
The workflow is as follows:
-
Embed the
Text
view in aVStack
and edit the text. - Add a
Color
view to the stack.
Step 1: Command-click the Hello World Text
view in the canvas — notice Xcode highlights the code line — and select Embed in VStack:
The canvas looks the same, but there’s now a VStack
in your code.
Change "Hello World"
to "Match this color"
: You could do this directly in the code, but, just so you know you can do this, Command-click the Text
view in the canvas, and select Show SwiftUI Inspector…:
Then edit the text in the inspector:
Your code updates to match! Just for fun, change the text in your code and watch it change in the canvas. Then change it back. Efficient, right?
Step 2: Click the + button in the toolbar to open the Library. Make sure the selected library is Views then search for color. Drag this object onto the Text
view in the canvas. While dragging, move the cursor down until you see the hint Insert Color in Vertical Stack — not Add Color to a new Vertical Stac… — but keep the cursor near the top of the Text
view because you want to insert it above the text. Then release the Color
object.
And there’s your Color
view inside the VStack
, in both the canvas and your code!
Note: In IB, you could drag several objects onto the view, then select them all and embed them in a stack view. But the SwiftUI Embed command only works on a single object.
Creating the guess color block
The guess color block looks a lot like the target color block, but with different text. It needs to be on the right-side of the target color block; that means using an HStack
(horizontal stack) as the top-most view.
In SwiftUI, it’s easier to select nested objects in the code than in the canvas.
In your code, Command-click the VStack
and select Embed in HStack.
Note: If Command-click jumps to the definition of
VStack
, use Control-Command-click instead.
Now copy the VStack
closure, paste it inside the HStack
, and change the Text
in the second VStack
to "R: 127 G: 127 B: 127"
. Your HStack
now looks like this:
HStack {
VStack {
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("Match this color")
}
VStack {
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("R: 127 G: 127 B: 127")
}
}
Creating the button and slider
In the original app, the Hit me! button and color sliders went below the color blocks; again a container view is needed. To achieve the desired result, you need to put your HStack
with color blocks inside a VStack
.
Note: To keep the Library open, Option-click the + button.
First, in your code, embed the HStack
in a VStack
, then drag a Button
from the Library into your code: Hover slightly below the HStack
view’s closing brace until a new line opens for you to drop the object.
Press Option-Command-P or click Resume to see your button:
Now that the button makes it clear where the VStack
bottom edge is, you can drag a Slider from the Library onto your canvas, just below the Button
:
Change the Button Text
to "Hit Me!"
and set the Slider value
to .constant(0.5). You’ll learn why it’s not just 0.5 in the section on Bindings.
Here’s what it looks like:
Note: If your slider thumb isn’t centered, refresh the preview (Option-Command-P) until it is.
Well, yes, you do need three sliders, but the slider values will update the UI, so you’ll first set up the red slider, then replicate it for the other two sliders.
Updating the UI
You can use “normal” constants and variables in SwiftUI, but if the UI should update when its value changes, you designate a variable as a @State
variable. In SwiftUI, when a @State
variable changes, the view invalidates its appearance and recomputes the body
. To see this in action, you’ll ensure the variables that affect the guess color are @State
variables.
Using @State
variables
Add these properties at the top of struct ContentView
, above the body
property:
let rTarget = Double.random(in: 0..<1)
let gTarget = Double.random(in: 0..<1)
let bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double
In the RGB color space, R, G and B values are between 0 and 1. The target color doesn’t change during the game, so its values are constants, initialized to random values. You could also initialize the guess values to 0.5, but I’ve left them uninitialized to show you what you must do if you don’t initialize some variables.
Scroll down to the ContentView_Previews
struct, which instantiates a ContentView
to display in the preview. The initializer now needs parameter values for the guess values. Change ContentView()
to this:
ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)
This makes sure the sliders’ thumbs are centered when previewing the view.
You must also modify the initializer in SceneDelegate
, in scene(_:willConnectTo:options:)
— add parameters to ContentView()
in this line:
let contentView = ContentView(rGuess: 0.5, gGuess: 0.5,
bGuess: 0.5)
When the app loads its root scene, the slider thumbs will be centered.
Updating the Color
views
Back in ContentView.swift, in the VStack
containing Text("Match this color")
, edit the Color
view to use the target values:
Color(red: rTarget, green: gTarget, blue: bTarget)
Press Option-Command-P to see a random target color.
Note: The preview refreshes itself periodically, as well as when you click Resume or the live preview button (more about this soon), so don’t be surprised to see the target color change, all by itself, every so often.
Similarly, modify the guess Color
to use the guess values:
Color(red: rGuess, green: gGuess, blue: bGuess)
When the R, G and B values are all 0.5, you get gray. To check these guess values are working, change them in the preview. For example:
static var previews: some View {
ContentView(rGuess: 0.7, gGuess: 0.3, bGuess: 0.6)
.previewLayout(.fixed(width: 568, height: 320))
}
And see the preview update to something like this:
The R, G and B values in the Text
view are still 127, but you’ll fix that soon.
Change the preview values back to 0.5.
Making reusable views
Because the sliders are basically identical, you’ll define one slider view, then reuse it for the other two sliders — exactly as Apple recommends.
Making the red slider
First, pretend you’re not thinking about reuse, and just create the red slider. You should tell your users its minimum and maximum values with a Text
view on either side of the Slider
. To achieve this layout, you’ll need an HStack
.
Embed the Slider
in an HStack
, then insert Text
views above and below (in code) or to the left and right (in canvas). Change the Placeholder
text to 0
and 255
, then update the preview to see how it looks:
Note: You and I know the slider goes from 0 to 1, but the 255 end label and 0-to-255 RGB values are for your users, who might feel more comfortable thinking of RGB values between 0 and 255, as in the hexadecimal representation of colors.
The numbers look cramped, so you’ll fix that and also make this look and behave like a red slider.
Edit the slider HStack
code to look like this:
HStack {
Text("0")
.foregroundColor(.red)
Slider(value: $rGuess)
Text("255")
.foregroundColor(.red)
}
.padding(.horizontal)
You’ve modified the Text
views to be red, set the Slider
value to $rGuess
— the position of its thumb — and modified the HStack
with some horizontal padding. But what’s with the $
? You’ll find out real soon, but first, check that it’s working.
Down in the preview code, change rGuess
to something different from 0.5, like 0.8, then press Option-Command-P:
Awesome — rGuess
is 0.8, and the slider thumb is right where you’d expect it to be! And the numbers are red, and not squashed up against the edges.
Bindings
So back to that $
. It’s actually pretty 8cool and ultra-powerful for such a little symbol. rGuess
by itself is just the value — read-only. $rGuess
is a read-write binding. You need it here to update the guess color while the user is changing the slider’s value.
To see the difference, set the values in the Text
view below the guess Color
view: Change Text("R: 127 G: 127 B: 127")
to the following:
Text("R: \(Int(rGuess * 255.0))"
+ " G: \(Int(gGuess * 255.0))"
+ " B: \(Int(bGuess * 255.0))")
Here, you’re only using (read-only) the guess values, not changing them, so you don’t need the $
prefix.
Press Option-Command-P:
And now the R value is 204. That’s 255 * 0.8
, as it should be!
Extracting subviews
Next, the purpose of this section is to create a reusable view from the red slider HStack
. To be reusable, the view needs some parameters. If you were to Copy-Paste-Edit this HStack
to create the green slider, you’d change .red
to .green
, and $rGuess
to $gGuess
. So those are your parameters.
Command-click the red slider HStack
, and select Extract Subview:
This works the same as Refactor ▸ Extract to Function, but for SwiftUI views.
Name the extracted view ColorSlider.
Note: Right after you select Extract Subview from the menu,
ExtractedSubview
is highlighted. If you rename it while it’s highlighted, the new name appears in two places: where you extracted it from and also in the extracted subview, down at the bottom of the file. If you don’t rename it in time, then you have to manually change the name of the extracted subview in these two places.
Don’t worry about all the error messages that appear. They’ll go away when you’ve finished editing your new subview.
Now add these properties at the top of ColorSlider
, before the body
property:
@Binding var value: Double
var textColor: Color
For the value
variable, you use @Binding
instead of @State
, because the ColorSlider
view doesn’t own this data — it receives an initial value from its parent view and mutates it.
Now, replace $rGuess
with $value
and .red
with textColor
:
Text("0")
.foregroundColor(textColor)
Slider(value: $value)
Text("255")
.foregroundColor(textColor)
Then go back up to the call to ColorSlider()
in the VStack
, and add your parameters:
ColorSlider(value: $rGuess, textColor: .red)
Check that the preview still shows the red slider correctly, then Copy-Paste-Edit this line to create the other two sliders:
ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)
Change the guess values in the preview code, then update the preview:
Everything’s working! You can’t wait to play the game? Coming right up!
But first, set the guess values back to 0.5 in the preview code.
Live Preview
You don’t have to fire up Simulator to play the game: Down by the lower-right corner of the preview device, click the live preview button:
Wait for the Preview spinner to stop; if necessary, click Try Again.
Now move those sliders to match the color!
Note: At the time of writing, Xcode’s live preview doesn’t use the fixed width and height settings. Instead, it uses the Simulator device that’s selected in the project’s scheme — in this case, iPhone 11 Pro Max.
Stop and think about what’s happening here, compared with how the UIKit app works. The SwiftUI views update themselves whenever the slider values change! The UIKit app puts all that code into the slider action. Every @State
variable is a source of truth, and views depend on state, not on a sequence of events.
How amazing is that! Go ahead and do a victory lap to the kitchen, get your favorite drink and snacks, then come back for the final step! You want to know your score, don’t you?
Presenting an alert
After using the sliders to get a good color match, your user taps the Hit Me! button, just like in the original UIKit game. And just like in the original, an Alert
should appear, displaying the score.
First, add a method to ContentView
to compute the score. Between the @State
variables and the body
, add this method:
func computeScore() -> Int {
let rDiff = rGuess - rTarget
let gDiff = gGuess - gTarget
let bDiff = bGuess - bTarget
let diff = sqrt((rDiff * rDiff + gDiff * gDiff
+ bDiff * bDiff) / 3.0)
return lround((1.0 - diff) * 100.0)
}
The diff
value is just the normalized distance between two points in three-dimensional space. You subtract it from 1, then scale it to a value out of 100. Smaller diff
yields a higher score.
Next, you’ll work on your Button
view:
Button(action: {}) {
Text("Hit Me!")
}
A Button
has an action and a label, just like a UIButton
. The action you want to happen is the presentation of an Alert
view. But if you just create an Alert
in the Button
action, it won’t do anything.
Instead, you create the Alert
as one of the subviews of ContentView
, and add a @State
variable of type Bool
. Then you set the value of this variable to true
when you want the Alert
to appear — in the Button
action, in this case. The value changes to false
when the user dismisses the Alert
, so the Alert
disappears.
So add this @State
variable, initialized to false
:
@State var showAlert = false
Then add this line as the Button
action:
self.showAlert = true
You need the self
because showAlert
is inside a closure.
Finally, add an alert
modifier to the Button
, so your Button
view looks like this:
Button(action: { self.showAlert = true }) {
Text("Hit Me!")
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Your Score"),
message: Text(String(computeScore())))
}
.padding()
You pass the $showAlert
binding because its value will change when the user dismisses the alert, and this changed value will change the UI.
SwiftUI has simple initializers for Alert
views, just like the ones that many developers have created for themselves, in a UIAlertViewController
extension. This one has a default OK
button, so you don’t even need to include it as a parameter.
Finally, you add some padding, to make the button stand out better.
Turn off live preview, click Resume to refresh the preview, then turn on live preview, and try your hand at matching the target color:
Hey, when you’ve got a live preview, who needs Simulator?
Note: As you develop your own apps, you might find the preview doesn’t always work as well as this. If it looks odd, or crashes, try running in a simulator. If that doesn’t work, run it on a device.
Challenge
Challenge: Create a SwiftUI app
The challenge/starter folder contains a UIKit version of our “famous” BullsEye app from our book iOS Apprentice. Your challenge is to create a SwiftUI app with the same UI and behavior.
The UIKit app doesn’t use a stack view for the slider, but you’ll find it really easy to create your SwiftUI UI using stacks.
The solution is in the challenge/final folder for this chapter.
Key points
- The Xcode canvas lets you create your UI side-by-side with its code, and they stay in sync: A change to one side always updates the other side.
- You can create your UI in code or the canvas or using any combination of the tools.
- You organize your view objects with horizontal and vertical stacks, just like using stack views in storyboards.
- Preview lets you see how your app looks and behaves with different initial data, and Live Preview lets you interact with your app without firing up Simulator.
- You should aim to create reusable views. Xcode’s Extract Subview tool makes this easy.
- SwiftUI updates your UI whenever a
@State
variable’s value changes. You pass a reference to a subview as a@Binding
, allowing read-write access to the@State
variable. - Presenting alerts is easy again.