Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

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:

Playing the game
Playing the game

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:

UIKit RGBullsEye starter app
UIKit RGBullsEye starter app

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:

Preview Resume button
Preview Resume button

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:

Hello World preview
Hello World 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:

Editor options
Editor options

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 and VStack container views. There are other container views, including ZStack and Group. 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:

  1. Embed the Text view in a VStack and edit the text.
  2. 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:

Embed Text view in VStack
Embed Text view 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:

Control-Option-click shows SwiftUI Inspector for Text view
Control-Option-click shows SwiftUI Inspector for Text view

Then edit the text in the inspector:

Edit text in inspector
Edit text in 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 Stacknot 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.

Insert Color into VStack
Insert Color into VStack

And there’s your Color view inside the VStack, in both the canvas and your code!

Color view in VStack
Color view in VStack

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:

Add Button to code
Add Button to code

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:

Insert Slider into VStack
Insert Slider into VStack

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:

Button & Slider in VStack
Button & Slider in VStack

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.

Random target color
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:

Guess color set in preview
Guess color set in preview

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:

Slider from 0 to 255
Slider from 0 to 255

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.

Add horizontal padding
Add horizontal padding

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:

Red slider value 0.3
Red slider value 0.3

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:

R value 76 = 255 * 0.3
R value 76 = 255 * 0.3

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:

Extract HStack to subview
Extract HStack to 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:

Three sliders
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:

Live preview button
Live preview button

Wait for the Preview spinner to stop; if necessary, click Try Again.

Now move those sliders to match the color!

Playing the game
Playing the game

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:

Embed Text view in an if-else
Embed Text view in an if-else

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:

Score!
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:

Color circles
Color 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 a Binding, allowing read-write access to the @State property.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.