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 UIKit 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
properties 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 UIKit/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 other view’s color match the target color. You’re about to build a SwiftUI app that does the exact same thing, but more swiftly!
Exploring the SwiftUI starter project
Open the SwiftUI/RGBullsEye starter project from the chapter materials.
In the project navigator, open the RGBullsEye group to see what’s here: the AppDelegate.swift, which you may be used to seeing, is now RGBullsEyeApp. This creates the app’s WindowGroup
from ContentView()
:
@main
struct RGBullsEyeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
The @main
attribute means this struct contains the entry point for the app. The App
protocol takes care of generating the static main
function that actually runs. When the app starts, it displays this instance of ContentView
, which is defined in the ContentView file. It’s a struct
that conforms to the View
protocol:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
This is SwiftUI declaring that the body
of ContentView
contains a Text
view that displays Hello World. The padding()
modifier adds 10 points padding around the text.
There’s a Model group containing files that define a Game
struct with properties and methods and an RGB
struct to wrap the red, green and blue color values. The Color
extension provides a custom initializer to create a Color
view from an RGB
struct.
Previewing your ContentView
In the ContentView file, 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 — the canvas — next to or below the code, with this at the top:
Note: If your Xcode window’s width is less than its height, the canvas appears below the code editor.
By default, the preview uses the currently active scheme’s run destination.
Click Resume and wait a while to see the preview:
I clicked the text to show you the padding box.
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) or later.
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.
Creating your UI
Your SwiftUI app doesn’t have a storyboard or a view controller. The ContentView file 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, you need to know some vocabulary.
-
Canvas and Minimap: To get the full SwiftUI experience, you need at least 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 unchecked it in Editor Options.
-
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 7: “Introducing Stacks & 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 and modifiers, as well as media and code snippets.
Creating the target color view
In RGBullsEye, the target color view, 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. In this scenario, you’ll use a VStack
(vertical stack).
This is your workflow:
-
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:
Note: If Command-click jumps to the definition of
VStack
, use Control-Command-click instead. You just have a different setting in Xcode navigation preferences.
The canvas looks the same, but there’s now a VStack
in your code.
Change "Hello World"
to "R: ??? G: ??? B: ???"
: You could do this directly in the code, but, just so you know you can do this, Control-Option-click the Text
view in the canvas to show its 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 Stack… — 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!
The 0.5 values are highlighted because they’re just placeholders. For now, just accept them by selecting each, then pressing Enter.
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 view
The guess color view looks a lot like the target color view, but with different text. It goes below the target color view, so you’ll just add it to the VStack
.
In the code editor, copy the Color
and Text
code, including the padding()
, and paste them below the padding()
line.
Change the string in the second Text
view to "R: 204 G: 76 B: 178"
. These sample values create a bright fuchsia color :].
Your VStack
now looks like this:
VStack {
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("R: ??? G: ??? B: ???")
.padding()
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("R: 204 G: 76 B: 178")
.padding()
}
Creating the button and slider
The color sliders and Hit me! button go below the color blocks so again, you’ll just add them to your VStack
.
Earlier, you dragged a Color
view onto the canvas. This time, you’ll drag Slider
and Button
views into your code.
Note: To keep the Library open, Option-click the + button.
Open the library and drag a Button
into the code editor. Hover slightly below the second padding
line until a new line opens for you to drop the object.
If the button doesn’t appear right away, press Option-Command-P or click Resume:
Now that the button makes it clear where the VStack
bottom edge is, drag a Slider from the Library onto your canvas, just above the Button
:
In the code editor, set the Slider value
to .constant(0.5). You’ll learn why it’s not just 0.5 in the section on Bindings.
Accept the empty action: {}
and change the button content to Text("Hit Me!")
.
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, and this is the topic of the next section. So you’ll get the red slider working, then extract it to a reusable subview with parameters to create all three sliders.
Updating the UI
If the UI should update when a SwiftUI view property’s value changes, you designate it as a @State
property. In SwiftUI, when a @State
property’s value changes, the view invalidates its appearance and recomputes the body
. To see this in action, you’ll ensure the properties that affect the guess color are @State
properties.
Using @State
properties
Add these properties at the top of struct ContentView
, above the body
property:
@State var game = Game()
@State var guess: RGB
You create a Game
object to access the properties and methods required to display and run the RGBullsEye game. One of these properties is the target
RGB
object:
var target = RGB.random()
Creating game
initializes the red, green and blue values of target
to random values between 0 and 1.
You also need a local RGB object guess
to store the slider values.
You could initialize guess
to RGB()
, which initializes red
, green
and blue
to 0.5 (the color gray). I’ve left it uninitialized to show you what you must do if you don’t initialize it.
Scroll down to the ContentView_Previews
struct, which instantiates a ContentView
to display in the preview. The initializer now needs a parameter value for guess
. Change ContentView()
to this:
ContentView(guess: RGB(red: 0.8, green: 0.3, blue: 0.7))
These values will display the fuchsia color in the preview.
You must also replace the ContentView()
initializer in the RGBullsEyeApp file. This time, use the default initializer:
ContentView(guess: RGB())
When the app loads its initial scene, the slider thumbs will be centered. The guess color starts out gray.
Updating the Color
views
Back in ContentView, edit the Color
view above Text("R: ??? G: ??? B: ???")
to use the target
property of the game
object:
Color(rgbStruct: game.target)
You’re using the RGB
struct initializer defined in Model/ColorExtension to create a Color
view with the target
color values.
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
color values:
Color(rgbStruct: guess)
Refresh the preview to see the fuchsia color you set up in the preview ContentView
:
The R, G and B values in the guess Text
view match the color, but you’ll soon make them respond to slider values set by the user.
Making reusable views
Because the sliders are basically identical, you’ll define one slider view, then reuse it for the other two sliders. This is 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 at each end of the Slider
. To achieve this horizontal layout, you’ll need an HStack
.
Command-click the Slider
view and select Embed in 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.
First, Control-Option-click the HStack
(probably easier to do this in the code editor) to open its attributes inspector. In the Padding section, click the left and right checkboxes.
Clicking the left or right checkbox adds the modifier padding(.leading)
or padding(.trailing)
to HStack
. Then, when you click the other (right or left) checkbox, the padding
value changes to .horizontal
. And now there’s space between the screen edges and the slider labels.
Note: The quickest way to add padding all around a view is to type
.padding()
in the code editor. The attributes inspector is useful when you want to set padding on only some edges.
Next, edit the Slider
value and add a modifier:
Slider(value: $guess.red)
.accentColor(.red)
The modifier sets the slider’s minimumTrackTintColor
to red.
But what’s with the $guess
? You’ll find out real soon, but first, check that it’s working.
Down in the preview code, change the red
value to something different from 0.8, like 0.3, then press Option-Command-P:
Awesome, guess.red
is 0.3, and the slider thumb is right where you’d expect it to be! The leading track is red, and the number labels aren’t squashed up against the edges.
Bindings
So back to that $
. It’s actually pretty cool and ultra-powerful for such a little symbol. By itself, guess.red
is just the value. It’s read-only. But $guess.red
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: 204 G: 76 B: 178")
to the following:
Text(
"R: \(Int(guess.red * 255.0))"
+ " G: \(Int(guess.green * 255.0))"
+ " B: \(Int(guess.blue * 255.0))")
Here, you’re only using (read-only) the guess values, not changing them, so you don’t need the $
prefix.
This string displays the color values of an RGB
object as integers between 0 and 255. The RGB
struct includes a method for this. Replace the multi-line Text
code with this:
Text(guess.intString())
Press Option-Command-P:
And now the R value is 76. That’s 255 * 0.3
, 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 $guess.red
to $guess.green
and .red
to .green
. So these are your parameters.
Command-click the 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,
ExtractedView
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 struct ColorSlider
, before the body
property:
@Binding var value: Double
var trackColor: Color
For the value
property, 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 $guess.red
with $value
and .red
with trackColor
:
Slider(value: $value)
.accentColor(trackColor)
Then go back up to the call to ColorSlider()
in the VStack
. Click the Missing arguments error icon to open it, then click the Fix button to add the missing arguments. Fill in these parameter values:
ColorSlider(value: $guess.red, trackColor: .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: $guess.green, trackColor: .green)
ColorSlider(value: $guess.blue, trackColor: .blue)
Refresh the preview to see all three sliders:
Everything’s working! You can’t wait to play the game? Coming right up!
First, set the guess
parameter in previews
to RGB()
:
ContentView(guess: RGB())
Live Preview
You don’t have to fire up Simulator to play the game: In the Preview toolbar, 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!
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
property 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.
The RGB
struct has a method difference(target:)
to compute the difference between the guess
and target
RGB
objects, and the Game
struct has a method check(guess:)
that uses difference(target:)
to compute the score.
You’ll call check(guess:)
in the action
of 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 a subview of ContentView
, and add a @State
property of type Bool
. Then you set the value of this property to true
when you want the Alert
view to appear. In this case, you do this in the Button
action. When the user dismisses the alert, the value changes to false
, and the alert disappears.
So add this @State
property, initialized to false
:
@State var showScore = false
Then rewrite your Button
to add the action
code:
Button("Hit Me!") {
showScore = true
game.check(guess: guess)
}
It turns out there are many ways to configure a Button
. The label can be either a single object or a closure, usually containing an Image
view and a Text
view. The action can be either a function call or a closure. If either the label or the action is a single statement, you can put it in the parentheses. The other parameter can be a trailing closure.
In this case, the label is just a String
, so you swap the positions of label and action to make the action the trailing closure.
Finally, add this alert
modifier to the Button
(after the closing curly brace):
.alert(isPresented: $showScore) {
Alert(
title: Text("Your Score"),
message: Text(String(game.scoreRound)),
dismissButton: .default(Text("OK")) {
game.startNewRound()
guess = RGB()
})
}
You pass the $showScore
binding because its value will change when the user dismisses the alert, and this changed value will update the UI: It will stop presenting the alert.
When the Button
action calls game.check(guess:)
, this method computes the score for this round. You create a String
from this number, to display in the alert’s message
.
The simplest Alert
initializer has a default dismiss button with label “OK”, so you only need to include the dismissButton
parameter when you want to configure an action. In this case, you start a new round, which sets a new target
color. Then you reset the guess
color to gray.
There’s one last bit of functionality you need to implement. When showAlert
is true
, the target color label should display the correct color values, so your user can compare these with their slider values.
Command-click Text
in this line:
Text("R: ??? G: ??? B: ???")
Select Make Conditional:
Note: SwiftUI has a lot of nested closures, so Xcode helps you keep your braces in order. If you need to enclose more than one line of code in a closure, select the other lines and press Option-Command-[ or Option-Command-] to move them up or down. These keyboard shortcuts are tremendously useful in SwiftUI. If you need to look them up, they’re listed in the Xcode menu under Editor▸Structure.
Now edit the if-else
to look like this:
if !showScore {
Text("R: ??? G: ??? B: ???")
.padding()
} else {
Text(game.target.intString())
.padding()
}
When the user taps the button to show the alert, the target color label shows the actual color values.
Refresh the live preview. You might have to turn off live preview, click Resume, then turn on live preview. See how high you can score:
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.
Making it prettier
Your app has all its functionality, so now’s a good time to start improving how it looks. Instead of colored rectangles, how about circles?
Replace the target Color
view with this colored Circle
:
Circle()
.fill(Color(rgbStruct: game.target))
And similarly for the guess Color
view:
Circle()
.fill(Color(rgbStruct: guess))
Refresh the preview to admire your circles:
In the next chapter, you’ll customize these circles a lot more, so it’s a good idea to extract another subview.
Challenge
Challenge: Create a ColorCircle subview
Create a ColorCircle
subview so that you can replace the Circle().fill...
lines with these:
ColorCircle(rgb: game.target)
ColorCircle(rgb: guess)
The ColorCircle
struct doesn’t need any bindings.
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
property’s value changes. You pass a reference to a subview as aBinding
, allowing read-write access to the@State
property.