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:
➤ If the iPad doesn’t fit in the canvas, zoom out:
➤ 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:
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:
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:
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:
➤ Select Large Title from the menu: Now “Squat” is even bigger!
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:
➤ 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:
➤ 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 typeFont?
and returns aText
view. The “_” means there’s no external parameter name — you call it withfont(.title)
, not withfont(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:
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:
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:
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
:
➤ 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:
➤ 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:
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:
➤ 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:
- Invalid redeclaration of ’HeaderView’.
- 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:
➤ 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:
➤ Now you’re all set to see the power of previews. In the preview canvas, click the Duplicate Preview button:
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
andpreviews
properties are computed properties. They must return a value of typesome View
, so what’s inside the closure must be a single view.
➤ Now you can modify the second preview. Click its Inspect Preview button:
The inspector lets you set Color Scheme and Dynamic Type.
➤ Set Color Scheme to Dark and Dynamic Type to accessibilityLarge.
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.
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
andVideoPlayer
; using bundle files; optional types; make conditional; usingGeometryReader
; 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 likeInt
orString
followed by a question mark:Int?
orString?
. If you declarevar index: Int?
,index
can contain anInt
or no value at all. If you declarevar index: Int
— with no?
—index
must always contain anInt
. Useif let index = index {...}
to check whether an optional has a value. Theindex
on the right of=
is the optional value. If it has a value, theindex
on the left of=
is anInt
and the condition istrue
. If the optional has no value, the assignment=
is not performed and the condition isfalse
. You can also checkindex != nil
, which returnstrue
ifindex
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 ofvideoNames[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:
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:
Adding padding
➤ The header looks a little squashed. Control-Option-click HeaderView
to add padding
to its bottom:
This gives you a new modifier padding(.bottom)
and now there’s space between the header and the video:
Note: You could have added padding to the
VStack
in HeaderView.swift, butHeaderView
is a little more reusable without padding. You can choose whether to add padding and how to customize it whenever you useHeaderView
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.
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 aDouble
value from anInt
andInt(myDoubleValue)
truncates aDouble
value to create anInt
. 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:
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:
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:
➤ 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:
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.
➤ Remember, you can use font
to specify the size of SF Symbols. So add this modifier to the Image
:
.font(.largeTitle)
Bigger is better!
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!
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.