Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Building Blocks of SwiftUI

Section 2: 6 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 iOS 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 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:

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

Select User Interface: SwiftUI
Select User Interface: SwiftUI

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.swift
SceneDelegate.swift

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 of ContentView, which is defined in ContentView.swift. It’s a struct that conforms to the View 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:

Preview Resume button
Preview Resume button

By default, the preview uses the currently active scheme.

Click Resume and wait a while to see the preview:

Hello World preview
Hello World preview

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).

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.

Preview iPhone SE in landscape
Preview iPhone SE in landscape

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

  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

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…:

Show SwiftUI Inspector for Text view
Show 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 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.

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

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.

Embed color block VStack in HStack
Embed color block VStack 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:

Add Button to code
Add Button to code

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:

Insert Slider into VStack
Insert Slider into VStack

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:

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, 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.

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

Non-gray color to check guess values
Non-gray color to check guess values

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:

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.

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:

Slider value 0.8
Slider value 0.8

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:

R value 204 = 255 * 0.8
R value 204 = 255 * 0.8

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:

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, 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:

Guess values work for sliders and guess text
Guess values work for sliders and guess text

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:

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

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:

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.

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.
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.