Chapters

Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

Section I: Your first app: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your second app: Cards

Section 2: 9 chapters
Show chapters Hide chapters

3. Prototyping the Main View
Written by Audrey Tam

Now for the fun part! In this chapter, you’ll start creating a prototype of your app, which has four full-screen views:

  • Welcome
  • Exercise
  • History
  • Success

Creating the Exercise view

You’ll start by laying out the Exercise view, because it contains the most subviews. Here’s the list of what your user sees in this view:

  • A title and page numbers are at the top of the view and a History button is at the bottom.
  • The page numbers indicate there are four numbered pages.
  • The exercise view contains a video player, a timer, a Start/Done button and rating symbols.

And here’s the list rewritten as a list of subviews:

  • Header with page numbers
  • Video player
  • Timer
  • Start/Done button
  • Rating
  • History button

You could sketch your screens in an app like Sketch or Figma before translating the designs into SwiftUI views. But SwiftUI makes it easy to lay out views directly in your project, so that’s what you’ll do.

The beauty of SwiftUI is it’s declarative: You just declare the views you want to display, in the order you want them to appear. If you’ve created web pages, it’s a similar experience.

Outlining the Exercise view

➤ Continue with your project from the previous chapter or open the project in this chapter’s starter folder.

There’s a lot to do in this view, so you’ll start by creating an outline with placeholder Text views.

➤ Open ExerciseView.swift.

➤ The canvas preview uses the run destination simulated device by default. You’ll start by laying out the interface for the iPad version of HIITFit, so select an iPad simulator:

Select an iPad simulator.
Select an iPad simulator.

➤ If the iPad doesn’t fit in the canvas, zoom out:

Zoom out to fit the iPad in the canvas.
Zoom out to fit the iPad in the canvas.

ExerciseView has six subviews, so duplicate the Text(exerciseNames[index]) view, then edit the arguments — in the code or in the canvas — to create this list:

VStack {
  Text(exerciseNames[index])
  Text("Video player")
  Text("Timer")
  Text("Start/Done button")
  Text("Rating")
  Text("History button")
}

The first Text view is the starting point for the Header view. You’ll create the Header and Rating views in their own files. The video player, timer and buttons are simple views, so you’ll just create them directly in ExerciseView.

Creating the Header view

Skills you’ll learn in this section: modifying views using the Attributes inspector or auto-suggestions; method signatures with internal and external parameter names; using SF Symbols; Image view; extracting and configuring subviews; working with previews

You’ll create this Header view by adding code to ExerciseView, then you’ll extract it as a subview and move it to its own file.

➤ To prepare for the later extraction, embed the first Text view in a VStack: Hold down the Command key, click Text(exerciseNames[index]) then select Embed in VStack:

Use the Command-click menu to embed this Text view in a VStack.
Use the Command-click menu to embed this Text view in a VStack.

Note: This version of the Command-click menu appears only when the canvas is open. If you don’t see the Embed in VStack option, press Option-Command-Return to open the canvas.

Now this Text view is inside a VStack:

VStack {
  Text(exerciseNames[index])
}

The many ways to modify a view

➤ In the canvas, select the “Squat” Text view. To open the Attributes inspector, click the inspectors button in the toolbar, then select the Attributes inspector:

Open the inspectors pane and select the Attributes inspector.
Open the inspectors pane and select the Attributes inspector.

This inspector has sections for the most commonly-used modifiers: Font, Padding and Frame. You could select a font size from the Font ▸ Font menu, but you’ll use the search field this time. This is a more general approach to adding modifiers.

➤ Click in the Add Modifier field, then type font and select Font from the menu:

Select Font from the Add Modifier menu.
Select Font from the Add Modifier menu.

The font size of “Squat” changes in both the canvas and in code:

.font(.title)

➤ Xcode suggests the font size title, but this is only a placeholder. To “accept” this value, click .title, then press Return to set it as the value.

Note: Xcode and SwiftUI do a good job of auto-suggesting or defaulting to an option that is probably what you want.

➤ To see other options, Control-Option-click font or title. This opens the font modifier’s pop-up Attributes inspector. In the Font section, click the selected Font option Title to see the Font menu:

Show the Font menu in the Attributes inspector for font or title.
Show the Font menu in the Attributes inspector for font or title.

➤ Select Large Title from the menu: Now “Squat” is even bigger!

Title with Large Title font
Title with Large Title font

Note: Putting the modifier on its own line is a SwiftUI convention. A view often has several modifiers, each on its own line. This makes it easy to move a modifier up or down, because sometimes the order makes a difference.

➤ Here’s another way to see the font menu. Select .largeTitle and replace it with . — Xcode’s standard auto-suggest mechanism lists the possible values:

Xcode’s auto-suggestions while you type code
Xcode’s auto-suggestions while you type code

➤ Select largeTitle from the menu.

➤ Once you’re familiar with SwiftUI modifiers, you might prefer to just type. Delete .font(.largeTitle) and type .font. Xcode auto-suggests two font methods:

Xcode’s auto-suggestions for methods
Xcode’s auto-suggestions for methods

➤ Select either method and Xcode auto-completes with (.title). Change this to .largeTitle.

Swift Tip: The method signature func font(_ font: Font?) -> Text indicates this method takes one parameter of type Font? and returns a Text view. The “_” means there’s no external parameter name — you call it with font(.title), not with font(font: .title).

Creating page numbers

In addition to the name of the exercise, the header should display the page numbers with the current page number highlighted. This replaces the TabView index dots.

You could just display Text("1"), Text("2") and so on, but Apple provides a wealth of configurable icons as SF Symbols.

➤ The SF Symbols app is the best way to view and search the collection. Download and install it from apple.co/3hWxn3G. Some symbols must be used only for specific Apple products like FaceTime or AirPods. You can check symbols for restrictions at sfsymbols.com.

➤ After installing the SF Symbols app, open it and select the Indices category. Scroll down to the numbers:

SF Symbols app: Indices category (partial)
SF Symbols app: Indices category (partial)

You can choose black numbers on a white background or the other way around, in a circle or a square. The fill version is a good choice to represent the current page, with no-fill numbers for the other pages.

➤ SF Symbol names can be long, but it’s easy to copy them from the app. Select a symbol, then open the app’s Edit menu:

How to copy the name of an SF Symbol from the app
How to copy the name of an SF Symbol from the app

Note: The keyboard shortcut is Shift-Command-C.

➤ Select the no-fill “1.circle” symbol, press Shift-Command-C, then use the name to add this line of code below the title Text:

Image(systemName: "1.circle")

Image is another built-in SwiftUI View, and it has an initializer that takes an SF Symbol name as a String.

➤ Before adding more numbers, Command-click this Image to embed it in an HStack, so the numbers will appear side by side. Then duplicate and edit more Image views to create the other three numbers:

HStack {
  Image(systemName: "1.circle")
  Image(systemName: "2.circle")
  Image(systemName: "3.circle")
  Image(systemName: "4.circle")
}

And here’s your header:

Header with title and page numbers
Header with title and page numbers

The page numbers look too small. Because SF Symbols are integrated into the San Francisco system font — that’s the “SF” in SF Symbols — you can treat them like text and use font to specify their size.

➤ You could add .font(.title) to each Image, but it’s quicker and neater to add it to the HStack container:

HStack {
  Image(systemName: "1.circle")
  Image(systemName: "2.circle")
  Image(systemName: "3.circle")
  Image(systemName: "4.circle")
}
.font(.title2)

The font size applies to all views in the HStack:

SF Symbols with title2 font size
SF Symbols with title2 font size

➤ You can modify an Image to override the HStack modifier. For example, modify the first number to make it extra large:

Image(systemName: "1.circle")
  .font(.largeTitle)

Now only the first symbol is larger:

Overriding the stack’s font size for the first symbol
Overriding the stack’s font size for the first symbol

➤ Delete the Image modifier, so all the numbers are the same size.

Your ExerciseView now has a header, which you’ll reuse in WelcomeView. So you’re about to extract the header code to create a HeaderView.

You’ll use Xcode’s refactoring tool, which works well. But it’s always a good idea to commit your code before a change like this, just in case. Select Source Control ▸ Commit… or press Option-Command-C.

Extracting a subview

OK, drum roll …

Command-click the VStack containing the title Text and the page numbers HStack, then select Extract Subview from the menu:

Command-click VStack, select Extract Subview.
Command-click VStack, select Extract Subview.

Xcode moves the whole VStack into the body property of a new view with the placeholder name ExtractedView. And ExtractedView() is where the VStack used to be:

Ready to rename the extracted view
Ready to rename the extracted view

➤ While the placeholders are still highlighted, type HeaderView and press Return. If you miss the moment, just edit both placeholders.

The error flag shows where you need a parameter. The index property is local to ExerciseView, so you can’t use it in HeaderView. You could just pass index to HeaderView and ensure it can access the exerciseNames array. But it’s always better to pass just enough information. This makes it easier to set up the preview for HeaderView. Right now, HeaderView needs only the exercise name.

➤ Add this property to HeaderView, above the body property:

let exerciseName: String

➤ And replace exerciseNames[index] in Text:

Text(exerciseName)

➤ Scroll up to ExerciseView, where Xcode is complaining about a missing argument in HeaderView(). Click the error icon to click Fix, then complete the line to read:

HeaderView(exerciseName: exerciseNames[index])

➤ Now press Command-N to create a new SwiftUI View file and name it HeaderView.swift. Because you were in ExerciseView.swift when you pressed Command-N, the new file appears below it and in the same group folder.

Your new file opens in the editor with two error flags:

  1. Invalid redeclaration of ’HeaderView’.
  2. Missing argument for parameter ’exerciseName’.

➤ To fix the first, in ExerciseView.swift, select the entire 17 lines of your new HeaderView and press Command-X to cut it — copy it to the clipboard and delete it from ExerciseView.swift.

➤ Back in HeaderView.swift, replace the 5-line boilerplate HeaderView with what’s in the clipboard.

➤ To fix the second error, in previews, let Xcode add the missing parameter, then enter any exercise name for the argument:

HeaderView(exerciseName: "Squat")

Because you pass only the exercise name to HeaderView, the preview doesn’t need access to the exerciseNames array.

Working with previews

The preview still uses the iPad simulator, which takes up a lot of space. You can modify the preview to show only the header.

➤ In HeaderView_Previews, Control-Option-click HeaderView(...) then type preview in the Add Modifier field:

Selecting Preview Layout from the Attributes inspector for Header view
Selecting Preview Layout from the Attributes inspector for Header view

➤ Select Preview Layout to add this modifier:

.previewLayout(.sizeThatFits)

➤ The placeholder value is sizeThatFits, and this is what you want, but you must “accept” it. Click sizeThatFits, then press Return to set it as the value.

➤ Resume the preview to see just the header:

Preview is just big enough to show the view.
Preview is just big enough to show the view.

➤ Now you’re all set to see the power of previews. In the preview canvas, click the Duplicate Preview button:

Duplicated previews, with Duplicate Preview button circled
Duplicated previews, with Duplicate Preview button circled

You’ve made a copy of the preview in the canvas and in the code:

Group {
  HeaderView(exerciseName: "Squat")
    .previewLayout(.sizeThatFits)
  HeaderView(exerciseName: "Squat")
    .previewLayout(.sizeThatFits)
}

Just like when you duplicated the Text view, Xcode embeds the two views in a container view. This time it’s a Group, which doesn’t specify anything about layout. Its only purpose is to wrap multiple views into a single view.

Swift Tip: The body and previews properties are computed properties. They must return a value of type some View, so what’s inside the closure must be a single view.

➤ Now you can modify the second preview. Click its Inspect Preview button:

Inspect the second preview.
Inspect the second preview.

The inspector lets you set Color Scheme and Dynamic Type.

➤ Set Color Scheme to Dark and Dynamic Type to accessibilityLarge.

Second preview with dark color scheme and accessibilityLarge font size
Second preview with dark color scheme and accessibilityLarge font size

That’s how easy it is to see how this view appears on a device with these settings.

Now return to ExerciseView.swift, where the header is just the way you left it.

Exercise view with Header view
Exercise view with Header view

Time to commit changes again: Select Source Control ▸ Commit… or press Option-Command-C. And this is the last time I’ll remind you. ;]

Next, you’ll set up the video player.

Playing a video

Skills you’ll learn in this section: using AVPlayer and VideoPlayer; using bundle files; optional types; make conditional; using GeometryReader; adding padding

➤ In ExerciseView.swift, add this statement just below import SwiftUI:

import AVKit

AVKit is a framework in Apple’s software development kits (SDKs). Importing it allows you to use high-level types like AVPlayer to play videos with the usual playback controls.

➤ Now replace Text("Video player") with this line:

VideoPlayer(player: AVPlayer(url: url))

Xcode complains it “cannot find ’url’ in scope”, so you’ll define this value next.

Getting the URL of a bundle file

You need the URL of the video file for this exercise. The videonames array lists the name part of the files. All the files have file extension .mp4.

These files are in the project folder, which you can access as Bundle.main. Its method url(forResource:withExtension:) gets you the URL of a file in the main app bundle if it exists. Otherwise, it returns nil which means no value. The return type of this method is an Optional type, URL?.

Swift Tip: Swift’s Optional type helps you avoid many hard-to-find bugs that are common in other programming languages. It’s usually declared as a type like Int or String followed by a question mark: Int? or String?. If you declare var index: Int?, index can contain an Int or no value at all. If you declare var index: Int — with no ?index must always contain an Int. Use if let index = index {...} to check whether an optional has a value. The index on the right of = is the optional value. If it has a value, the index on the left of = is an Int and the condition is true. If the optional has no value, the assignment = is not performed and the condition is false. You can also check index != nil, which returns true if index has a value.

Note: You’ll learn more about the app bundle in Chapter 8, “Saving Settings” and about optionals in Chapter 9, “Saving History Data”.

So you need to wrap an if let around the VideoPlayer. Yet another pair of braces! It can be hard to keep track of them all. But Xcode is here to help. ;]

Command-click VideoPlayer and select Make Conditional. And there’s an if-else closure wrapping VideoPlayer!

Xcode Tip: Take advantage of features like Embed in HStack and Make Conditional to let Xcode keep your braces matched. To adjust what’s included in the closure, use Option-Command-[ or Option-Command-] to move the closing brace up or down.

➤ Now replace if true { with:

if let url = Bundle.main.url(
  forResource: videoNames[index],
  withExtension: "mp4") {

➤ In the else closure, replace EmptyView() with:

Text("Couldn’t find \(videoNames[index]).mp4")
  .foregroundColor(.red)

Swift Tip: The string interpolation code \(videoNames[index]) inserts the value of videoNames[index] into the string literal.

➤ It’s easy to test this else code: Create a typo by changing the withExtension argument to np4, then refresh the preview:

Testing the Couldn’t-find message
Testing the Couldn’t-find message

Actually, it’s squat.np4 that isn’t in the app bundle.

➤ Undo the np4 typo.

➤ Now click Live Preview, then click the play button to watch the video. If the play button disappears, try this: Click on the video then press Space.

Getting the screen dimensions

The video takes up a lot of space on the screen. You could set the width and height of its frame to some constant values that work on most devices, but it’s better if these measurements adapt to the size of the device.

➤ In body, Command-click VStack and select Embed…. Change the Container { placeholder to this line:

GeometryReader { geometry in

GeometryReader is a container view that provides you with the screen’s measurements for whatever device you’re previewing or running on.

➤ Add this modifier to VideoPlayer:

.frame(height: geometry.size.height * 0.45)

The video player now uses only 45% of the screen height:

Video player uses 45% of screen height.
Video player uses 45% of screen height.

Adding padding

➤ The header looks a little squashed. Control-Option-click HeaderView to add padding to its bottom:

Add padding to Header view in Exercise view.
Add padding to Header view in Exercise view.

This gives you a new modifier padding(.bottom) and now there’s space between the header and the video:

Exercise view with padding under Header view
Exercise view with padding under Header view

Note: You could have added padding to the VStack in HeaderView.swift, but HeaderView is a little more reusable without padding. You can choose whether to add padding and how to customize it whenever you use HeaderView in another view.

Now head back to ContentView.swift and Live Preview your app. Swipe from one page to the next to see the different exercise videos.

HIITFit pages
HIITFit pages

Finishing the Exercise view

Skills you’ll learn in this section: Text with date and style parameters; types in Swift; Date(); Button, Spacer, foregroundColor; repeating a view; unused closure parameter

To finish off the Exercise view, add the timer and buttons, then create the Ratings view.

Creating the Timer view

➤ Add this property to ExerciseView, above body:

let interval: TimeInterval = 30

These are high-intensity interval exercises, so the timer counts down from 30 seconds.

➤ Replace Text("Timer") with this code:

Text(Date().addingTimeInterval(interval), style: .timer)
  .font(.system(size: 90))

The default initializer Date() creates a value with the current date and time. The Date method addingTimeInterval(_ timeInterval:) adds interval seconds to this value.

➤ The Swift Date type has a lot of methods for manipulating date and time values. Option-click Date and Open in Developer Documentation to scan what’s available. You’ll dive a little deeper into Date when you create the History view.

The timeInterval parameter’s type is TimeInterval. This is just an alias for Double. If you say interval is of type Double, you won’t get an error, but TimeInterval describes the value’s purpose more accurately.

Swift Tip: Swift is a strongly typed language. This means that you must use the correct type. When using numbers, you can usually pass a value of a wrong type to the initializer of the correct type. For example, Double(myIntValue) creates a Double value from an Int and Int(myDoubleValue) truncates a Double value to create an Int. If you write code in languages that allow automatic conversion, it’s easy to create a bug that’s very hard to find. Swift makes sure you, and people reading your code, know that you’re converting one type to another.

You’re using the Text view’s (_:style:) initializer for displaying dates and times. The timer and relative styles display the time interval between the current time and the date value, formatted as “mm:ss” or “mm min ss sec”, respectively. These two styles update the display every second.

You set the system font size to 90 points to make a really big timer.

➤ Click Live Preview to watch the timer count down from 30 seconds:

Exercise view with 30-second timer
Exercise view with 30-second timer

Because you set date to 30 seconds in the future, the displayed time interval decreases by 1 every second, as the current time approaches date. If you wait until it reaches 0 (change interval to 3 so you don’t have to wait so long), you’ll see it start counting up, as the current time moves away from date. Don’t worry, this Text timer is just for the prototype. You’ll replace it with a real timer in Chapter 7, “Observing Objects”.

Creating buttons

Creating buttons is simple, so you’ll do both now.

➤ Replace Text("Start/Done button") with this code:

Button("Start/Done") { }
  .font(.title3)
  .padding()

Here, you gave the Button the label Start/Done and an empty action. You’ll add the action in Chapter 7, “Observing Objects”. Then, you enlarged the font of its label and added padding all around it.

➤ Replace Text("History button") with this code:

Spacer()
Button("History") { }
  .padding(.bottom)

The Spacer pushes the History button to the bottom of the screen. The padding pushes it back up a little, so it doesn’t look squashed.

You’ll add this button’s action in Chapter 6, “Adding Functionality to Your App”.

Here’s what ExerciseView looks like now:

Exercise view with buttons
Exercise view with buttons

Now for the last subview in ExerciseView: RatingView.

Creating the Rating view

➤ Create a new new SwiftUI View file named RatingView.swift. This will be a small view, so add this modifier to its preview:

.previewLayout(.sizeThatFits)

A rating view is usually five stars or hearts, but the rating for an exercise should reflect the user’s exertion.

➤ To find a more suitable rating symbol, open the SF Symbols app and select the Health category:

SF Symbols Health category: ECG waveform
SF Symbols Health category: ECG waveform

➤ The ECG wave form seems just right for rating high-intensity exercises! Select it, then press Shift-Command-C to copy its name.

➤ Replace the boilerplate Text with this code, pasting the symbol name in between double quotation marks:

Image(systemName: "waveform.path.ecg")
  .foregroundColor(.gray)

You’ve added the SF Symbol as an Image and set its color to gray.

A rating view needs five of these symbols, arranged horizontally.

➤ In the canvas or in the editor, Command-click the Image and select Repeat from the menu:

Command-click Image, select Repeat.
Command-click Image, select Repeat.

Xcode gives you a loop, with suggested range 0 ..< 5:

ForEach(0 ..< 5) { item in
  Image(systemName: "waveform.path.ecg")
    .foregroundColor(.gray)
}

➤ Click this range and press Return to accept it.

In the canvas, you see five separate previews! Xcode should have embedded them in a stack, like when you duplicated a view, but it didn’t.

Command-click ForEach and embed it in an HStack.

Now your code looks like this:

HStack {
  ForEach(0 ..< 5) { item in
    Image(systemName: "waveform.path.ecg")
      .foregroundColor(.gray)
  }
}

That’s better! Now the symbols are all in a row. But they’re very small.

ECG waveform symbols in HStack
ECG waveform symbols in HStack

➤ Remember, you can use font to specify the size of SF Symbols. So add this modifier to the Image:

.font(.largeTitle)

Bigger is better!

SF Symbols with largeTitle font size
SF Symbols with largeTitle font size

One last detail: The code Xcode created for you contains an unused closure parameter item:

ForEach(0 ..< 5) { item in

➤ You don’t use item in the loop code, so replace item with _:

ForEach(0 ..< 5) { _ in

Swift Tip: It’s good programming practice to replace unused parameter names with _. The alternative is to create a throwaway name, which takes a non-zero amount of time and focus and will confuse you and other programmers reading your code.

➤ Now head back to ExerciseView.swift to use your new view. Replace Text("Rating") with this code:

RatingView()
  .padding()

Your ECG wave forms now march across the screen!

Exercise view with Rating subview
Exercise view with Rating subview

In Chapter 6, “Adding Functionality to Your App”, you’ll add code to let the user set a rating value and represent this value by setting the right number of symbols to red. And, in Chapter 8, “Saving Settings”, you’ll save the rating values so they persist across app launches.

Key points

  • SwiftUI is declarative: Simply declare views in the order you want them to appear.
  • Create separate views for the elements of your user interface. This makes your code easier to read and maintain.
  • Use the SwiftUI convention of putting each modifier on its own line. This makes it easy to move or delete a modifier.
  • Xcode and SwiftUI provide auto-suggestions and default values that are often what you want.
  • Let Xcode help you avoid errors: Use the Command-menu to embed a view in a stack or in an if-else closure, or extract a view into a subview.
  • The SF Symbols app provides icon images you can configure like text.
  • Previews are an easy way to check how your interface appears for different user settings.
  • Swift is a strongly typed programming language.
  • GeometryReader enables you to set a view’s dimensions relative to the screen dimensions.

Where to go from here?

Your Exercise view is ready. In the next chapter, you’ll lay out the other three full-screen views your app needs.

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.