Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

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

2. Planning a Paged App
Written by Audrey Tam

In Section 1 of this book, you’ll build an app to help you do high-intensity interval training. Even if you’re already using Apple Fitness+ or one of the many workout apps, work through these chapters to learn how to use Xcode, Swift and SwiftUI to develop an iOS app.

In this chapter, you’ll plan your app, then set up the paging interface. You’ll start using the SwiftUI Attributes inspector to add modifiers. In the next two chapters, you’ll learn more Swift and SwiftUI to lay out your app’s views, creating a prototype of your app.

Making Lists: Views & Actions

The finished app will have several screens. Here’s a sample to show you what it will look like:

HIITFit screens
HIITFit screens

There’s a lot going on in these screens, especially the one with the exercise video. You might feel overwhelmed, wondering where to start. Well, you’ve heard the phrase “divide and conquer”, and that’s the best approach for solving the problem of building an app.

First, you need an inventory of what you’re going to divide. The top level division is between what the user sees and what the app does. Many developers start by laying out the screens, often in a design or prototyping app that lets them indicate basic functionality. For example, when the user taps this button, the app shows this screen. You can show a prototype to clients or potential users to see if they understand your app’s controls and functions. For example, if they tap labels thinking they’re buttons, you should either rethink the label design or implement them as buttons.

Listing What Your User Sees

To start, list the screens you need to create and describe their contents:

  • A Welcome screen with text, images and a button.

  • A title and page numbers are at the top of the Welcome screen and a History button is at the bottom. These are also on the screen with the exercise video. The page numbers indicate there are four numbered pages after this page. The waving hand symbol is highlighted.

  • The screen with the exercise video also has a timer, a Start/Done button and rating symbols. One of the page numbers is highlighted.

  • The History screen shows the user’s exercise history as a list and as a bar chart. It has a title but no page numbers and no History button.

  • The High Five! screen has an image, some large text and some small gray text. Like the History screen, it has no page numbers and no History button.

In this chapter and the next, you’ll lay out the basic elements of these screens. In Chapter 9, “Refining Your App”, you’ll fine-tune the appearance to look like the screenshots above.

Listing What Your App Does

Next, list the functionality of each screen, starting with the last two.

  • The History and High Five! screens are modal sheets that slide up over the Welcome or Exercise screen. Each has a button the user taps to dismiss it, either a circled “X” or a Continue button.
  • On the Welcome and Exercise screens, the matching page number is white text or outline on a black background. Tapping the History button displays the History screen.
  • The Welcome page Get Started button displays the next page.
  • On an Exercise page, the user can tap the play button to play the video of the exercise.
  • On an Exercise page, tapping the Start Exercise button starts a countdown timer, and the button label changes to Done. Ideally, the Done button is disabled until the timer reaches 0. Tapping Done adds this exercise to the user’s history for the current day.
  • On an Exercise page, tapping one of the five rating symbols changes the color of that symbol and all those preceding it.
  • Tapping Done on the last exercise shows the High Five! screen.
  • Nice to have: Tapping a page number goes to that page. Tapping Done on an Exercise page goes to the next Exercise page. Dismissing the High Five! screen returns to the Welcome page.

You’ll implement all of these in the next three chapters.

There’s also the overarching page-based structure of HIITFit. This is quite easy to implement in SwiftUI, so you’ll do it first, before you create any screens.

Creating Pages

Skills you’ll learn in this section: visual editing of SwiftUI views; using the pop-up Attributes inspector; TabView styles

The main purpose of this section is to set up the page-based structure of HIITFit, but you’ll also learn a lot about using Xcode, Swift and SwiftUI. The short list of Skills at the start of each section helps you keep track of what’s where.

➤ Open the starter project for this chapter. Use the Xcode menu Source Control ▸ New Git Repositories… to add a repository.

Canvas & Editor Always in Sync

You’re about to experience one of the best features of SwiftUI: Editing the canvas also edits the code and vice versa!

And here’s your first SwiftUI vocabulary term: Everything you can see on the device screen is a view, with larger views containing subviews.

Your next SwiftUI term is modifier: SwiftUI has an enormous number of methods you can use to modify the appearance or behavior of a view.

➤ First, in ContentView.swift, delete .padding() from the body closure: It’s a modifier that adds space around the Text view, and you don’t need it for now.

Editing the View in Canvas Selectable Mode

In the canvas, refresh the preview if necessary, click the Selectable button, then double-click the Text view: This selects “Hello, world!” in the code:

Selection in canvas selects text in code editor.
Selection in canvas selects text in code editor.

Note: A single click selects the Text view; double-click selects the content of the view.

➤ Now, in the code editor, replace Hello, world! with Welcome!: The text changes in the canvas, too.

Editing the view in the code also changes the canvas.
Editing the view in the code also changes the canvas.

A Text view simply displays a string of characters. It’s useful for listing the views you plan to create, as a kind of outline. You’ll use multiple Text views now, to see how to implement paging behavior.

Xcode Tip: Xcode 16 introduced predictive code completion, which works even in strings, so you might see suggestions for “Welcome to SwiftUI” or “Welcome, world!”. Press Esc to dismiss suggestions or turn off Predictive code completion in Settings ➤ Text Editing ➤ Editing:

You can turn off predictive code completion.
You can turn off predictive code completion.

➤ Still in the code editor, right-click Text and select Embed in VStack:

Embed the Text view in a VStack.
Embed the Text view in a VStack.

Your code changes to:

VStack {
  Text("Welcome")
}

VStack means vertical stack, and it’s the default arrangement when you have more than one view in body. To take advantage of the VStack, press Command-D to duplicate the Text view.

Command-D duplicates a line of code.
Command-D duplicates a line of code.

You get two Text views and, in the preview, they appear in a vertical stack.

➤ Change “V” to “H” to see the two views displayed in a horizontal stack:

HStack stacks views horizontally.
HStack stacks views horizontally.

➤ Type Command-Z to undo this change. SwiftUI’s defaults tend to match up well with what most people want to do.

➤ Still in the code editor, change the second Welcome! to Exercise 1. Then duplicate Text("Exercise 1") and change the third string to Exercise 2.

Three Text views in a VStack
Three Text views in a VStack

You now have three distinct views to use in a TabView.

Using TabView

Here’s how easy it is to create a TabView:

In the code editor, change VStack to TabView:

A TabView has a tab bar.
A TabView has a tab bar.

Where did your Exercises go!? Well, they’re now the second and third tabs of a tab view, and there’s a tab bar at the bottom of the screen. It’s blank, because you haven’t labeled the tabs yet.

Labeling Tabs

Here’s how you label the tabs.

➤ Open the Library (press Shift-Command-L or click +) and, in the Modifiers tab, search for tab. Drag Tab Item into the code editor and hover/nudge the first Text line until a new line opens beneath it, then release Tab Item:

Drag a Tab Item modifier from the Library.
Drag a Tab Item modifier from the Library.

A new tabItem modifier appears in the code editor, with a placeholder for the Item Label:

Text("Welcome")
  .tabItem { Item Label }

And a blue Label appears in the tab bar:

A tab item with placeholder label
A tab item with placeholder label

➤ Select the Item Label placeholder and type Text(“Welcome”):

.tabItem { Text("Welcome") }

And there it is in the tab bar:

Result of replacing the placeholder tab item label
Result of replacing the placeholder tab item label

➤ Replace the entire TabView with the following to add the labels for the other tabs:

TabView {
  Text("Welcome")
    .tabItem { Text("Welcome") }
  Text("Exercise 1")
    .tabItem { Text("Exercise 1") }
  Text("Exercise 2")
    .tabItem { Text("Exercise 2") }
}

Now you can see the three tab labels:

Three tab items with labels
Three tab items with labels

Interacting With Live Preview

At this point, you probably want to see your TabView in action. It’s time to switch back to Live Preview.

➤ Click the Live Preview button, then tap an Exercise tab label to switch to that tab:

In Live Preview, tab buttons work.
In Live Preview, tab buttons work.

➤ This is the way tab views normally operate. To make the tabs behave like pages, add this modifier to the TabView:

.tabViewStyle(PageTabViewStyle())

And now your tab labels are gone!

The page style uses small index dots, but they’re white on white, so you can’t see them.

➤ To make them show up, add this modifier below tabViewStyle:

.indexViewStyle(
  PageIndexViewStyle(backgroundDisplayMode: .always))

Now you can see the index dots:

TabView page style index dots
TabView page style index dots

➤ In Live Preview, just swipe left or right and each page snaps into place.

Live Preview: TabView page style in mid-swipe
Live Preview: TabView page style in mid-swipe

➤ You won’t be using tabItem labels for this app, so delete them. This is now all the code inside the TabView closure:

TabView {
  Text("Welcome")
  Text("Exercise 1")
  Text("Exercise 2")
}

OK, you’ve set up the paging behavior, but you want the pages to be actual Welcome and Exercise views, not just text. To keep your code organized and easy to read, you’ll create each view in its own file and group all the view files in a folder.

Grouping Files

Skills you’ll learn in this section: grouping project files in a folder.

You’re about to create Welcome and Exercise subviews by combining smaller subviews. SwiftUI encourages you to create reusable subviews for the same reason you create functions: Don’t Repeat Yourself. Even if you don’t reuse a subview, it makes your code much easier to read. And SwiftUI compiles subviews into efficient machine code, so you can create all the subviews you need and not worry about performance.

➤ Select ContentView.swift in the Project navigator. Create a new SwiftUI View file named WelcomeView.swift. Then, create another new SwiftUI View file named ExerciseView.swift.

Your Project navigator now contains three view files:

Project navigator after you add two SwiftUI view files
Project navigator after you add two SwiftUI view files

You’ll create several more view files, so now you’ll create a folder with these three and name it Views. This works the same as in Finder because that’s where it’s happening — Xcode works directly with Finder to keep track of your project’s files.

➤ Hold down the Command key to select the three view files, then right-click and select New Group from Selection:

Create a new folder containing the three view files.
Create a new folder containing the three view files.

➤ Name the group Views.

Folders help you organize the files in your project. In Chapter 4, “Prototyping Supplementary Views”, you’ll create another folder for your app’s data models.

Passing Parameters

Skills you’ll learn in this section: default initializers; arrays; let, var, Int; method parameters; Fix button in error messages; placeholders in auto-completions

➤ Now, in ContentView, replace the first two Text placeholders with your new views:

TabView {
  WelcomeView()   // was Text("Welcome")
  ExerciseView()  // was Text("Exercise 1")
  Text("Exercise 2")
}

Swift Tip: A View is a structure, shortened to struct in Swift code. Like a class, it’s a complex data type that encapsulates properties and methods. If a View has no uninitialized properties, you can create an instance of it with its default initializer. For example, WelcomeView() creates an instance of WelcomeView.

Now what? Your app will use ExerciseView to display the name and video for several different exercises, so you need a way to index this data and pass each index to ExerciseView.

Actually, first you need some sample exercise data. In the Videos folder, you’ll find four videos. Here’s one of them:

One of the exercise videos
One of the exercise videos

Note: If you prefer to use your own videos, drag them from Finder into the Project navigator. Be sure the HIITFit check box is checked.

Options when adding your own videos
Options when adding your own videos

➤ In Chapter 3, “Prototyping the Main View”, you’ll create an Exercise data type but, for this prototype, in ExerciseView.swift, simply create two arrays at the top of ExerciseView, just above var body:

let videoNames = ["squat", "step-up", "burpee", "sun-salute"]
let exerciseNames = ["Squat", "Step Up", "Burpee", "Sun Salute"]

Swift Tip: An array is an ordered collection of primitive types, structure instances or class objects. All items in an array are the same type.

The video names match the names of the video files. The exercise names are visible to your users, so you use title capitalization and spaces.

➤ Still inside ExerciseView, above var body, add this property:

let index: Int

You declare a constant integer value named index.

Swift Tip: Swift distinguishes between creating constants with let and creating variables with var.

Xcode now complains about ExerciseView() in #Preview, because it’s missing the index parameter.

➤ Click the red error icon to display more information:

Open the error to show the Fix button.
Open the error to show the Fix button.

Xcode often suggests one or more ways to fix an error. Many times, its suggestion is correct, and this is one of those times.

➤ Click Fix to let Xcode fill in the index parameter.

➤ Now there’s a placeholder for the index value — a grayed-out Int. Click it to turn it blue, then type 0. So now you have this line of code:

ExerciseView(index: 0)

Swift Tip: Like other languages descended from the C programming language, Swift arrays start counting from 0, not 1.

Now use your index property to display the correct name for each exercise.

➤ Change the "Hello, World!" placeholder to the exercise name for this index value.

Text(exerciseNames[index])

The canvas has been complaining it Failed to build the scheme “HIITFit” and, back in ContentView.swift, Xcode is also complaining about the missing argument for parameter index in the call to ExerciseView().

Another error to fix
Another error to fix

➤ Fix this error the same way you did in ExerciseView.swift.

Now there’s a placeholder for the index value: What should you type there?

Looping

Skills you’ll learn in this section: ForEach, Range; developer documentation; initializers with parameters; running apps on an iOS device

Well, you could pass the first array index:

ExerciseView(index: 0)

Then copy-paste and edit to specify the other three exercises, but there’s a better way. You’re probably itching to use a loop. Here’s how you scratch that itch. ;]

➤ Replace the second and third lines in the TabView closure with this code:

ForEach(0 ..< 4) { index in
  ExerciseView(index: index)
}

ForEach loops over the range 0 to 4 but, because of that < symbol, not including 4. Each integer value 0, 1, 2 and 3 creates an ExerciseView with that index value.

The local variable name index is up to you. You could write this code instead:

ForEach(0 ..< 4) { number in
  ExerciseView(index: number)
}

Developer Documentation

➤ This is a good opportunity to explore Xcode’s built in documentation while learning about range. Hold down the Option key, then click ForEach:

Option-click the ForEach keyword.
Option-click the ForEach keyword.

You’re viewing Xcode’s pop-up Quick Help for the ForEach keyword. You can also view this information in the Quick Help inspector.

➤ To see more detailed information, scroll down to the bottom of the Quick Help text, click Open in Developer Documentation, then open the ForEach item in the documentation navigator:

Developer Documentation for ForEach
Developer Documentation for ForEach

The initializer you’re using to loop over the array indices was in the Xcode 14 documentation, but isn’t in the Xcode 16 documentation.

init(Range<Int>, content: (Int) -> Content)

➤ Type range in the search field and click the Suggested item:

Search Developer Documentation for Range.
Search Developer Documentation for Range.

The Range documentation page appears, but the navigator hasn’t moved from ForEach. It’s helpful to see where an item “lives” in the enormous Apple code universe — if you’re looking for a better way to solve a problem, you can start by exploring nearby items.

Right-click anywhere in the documenation page and select Reveal in Navigator:

Reveal Range in Navigator.
Reveal Range in Navigator.

And here’s where Range fits:  

Range in Navigator
Range in Navigator

It’s part of the Swift Standard Library, in the Collections section, which includes Arrays, Sets and Strides.

Now, back to the documentation:

Developer Documentation for Range
Developer Documentation for Range

Range is “A half-open interval from a lower bound up to, but not including, an upper bound”, and “You create a Range instance by using the half-open range operator (..<)”, which is what you did in the ForEach argument.

➤ Close the documentation window.

➤ You won’t need the TabView index dots. Open ContentView.swift and change:

.tabViewStyle(PageTabViewStyle())
.indexViewStyle(
  PageIndexViewStyle(backgroundDisplayMode: .always))

to:

.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

Now, you’ll never show the index dots.

Xcode Tip: This is a good place to commit the changes you’ve made to your project into your local Git repository. Select Integrate ▸ Commit… or press Option-Command-C. If asked, check all the changed files. Enter a commit message like “Set up paging tab view”, then click Commit.

➤ You’re still in ContentView, so Live Preview your app. Swipe from one page to the next to see the different exercise names.

HIITFit pages
HIITFit pages

Running Your Apps on an iOS Device

Note: This book’s apps expect your iOS device is running iOS 18.

Live Preview is a convenient way to see what your app looks like and give you some idea how it behaves. But some features don’t work in Live Preview, so then you need to build and run your app on a simulator.

If your app doesn’t look or behave quite right on the simulated device, running it on a real device is the final word. It might look just as you expect, or it might agree with the preview and simulator that you’ve got more work to do.

Also, there are features like motion and camera that you can’t test in a simulator. For these, you must install your app on a real device. Plus, it’s fun to have something on your iPhone that you built yourself!

Enabling Developer Mode on Your iOS Device

First, you must enable Developer Mode on your iOS device. Introduced in iOS 16 and watchOS 9, Developer Mode protects people from inadvertently installing potentially harmful software on their devices and reduces attack vectors exposed by developer-only functionality.

➤ Open the Settings app and tap Privacy & Security. Scroll down and tap Developer Mode, then tap the switch to turn it on.

Enable Developer Mode in Privacy & Security setting.
Enable Developer Mode in Privacy & Security setting.

You’ll see an alert warning you that Developer Mode reduces the security of your device.

➤ Tap the alert’s Restart button. After your device restarts and you unlock it, you’ll see an alert asking you to confirm that you want to turn on Developer Mode:

Turn on Developer Mode after restarting device.
Turn on Developer Mode after restarting device.

➤ Tap Enable to acknowledge the reduction in security protection in exchange for allowing Xcode and other tools to execute code, then enter your device passcode when prompted.

Your device is now ready to install and run apps from Xcode. After enabling Developer Mode, Xcode doesn’t ask again unless you disable Developer Mode by turning off the switch in Privacy & Security and restarting your device.

➤ Connect your device to your Mac with a cable. Use an Apple cable, as other-brand cables might not work for this purpose. Select your device from the run destination menu: It appears near the top, above the simulators:

Select your device as the run destination.
Select your device as the run destination.

Xcode will start preparing your device for development. This can take a while, so continue to the next step while it’s busy.

Getting a Signing Certificate

Apple does its best to protect its users from malicious apps. Part of this protection is ensuring Apple knows who is responsible for every app on your device. Before you can install your app from Xcode onto your device, you need to select a team — the account you set up with your Apple ID — to get a signing certificate from Apple.

➤ In the project page, select the target. In the Signing & Capabilities tab, change the organization name in the Bundle Identifier to something that’s uniquely yours, like org.mataharimau for me:

Personalize the Bundle Identifier.
Personalize the Bundle Identifier.

Note: The apps in this book have starter projects with com.yourcompany as the organization. If you want to run these apps on an iOS device, you need to personalize the bundle identifier. This is because one of the authors has already signed the app with the original bundle identifier, and you’re not a member of our teams.

➤ Next, check Automatically manage signing, tap Enable Automatic in the confirmation dialog, then select your account from the Team menu:

Enable Automatic and select Team.
Enable Automatic and select Team.

After some activity spinning, you’ll see a Provisioning Profile and a Signing Certificate. Xcode has created these and stored the certificate in your Mac’s keychain.

Provisioning Profile and a Signing Certificate
Provisioning Profile and a Signing Certificate

➤ Unlock your device, then build and run your project. Keep your device screen active until the app launches on your device. You might need to enter your Mac’s login password:

Allow codesign access to keychain.
Allow codesign access to keychain.

Trusting Yourself

Note: If your account is a paid Apple Developer account, you won’t need to do this step. Running your app on your device will just work. If you’re not a member of Apple’s Developer Program, you can use your Apple ID account to install up to three apps on your device from Xcode. The app works for seven days after you install it. Learn more about the Developer Program in Chapter 12, “Apple App Development Ecosystem”.

If this is the first time you’re running an app on this device, Apple makes you perform one more step to make sure nothing nasty installs itself on your device.

The app icon appears on the home screen of your device, but error messages appear in Xcode and on your device:

Could not launch / Untrusted Developer
Could not launch / Untrusted Developer

The Xcode message tells you exactly what to do: Open Settings ▸ General ▸ VPN & Device Management, select your Developer App certificate and trust it:

Settings ▸ General ▸ VPN & Device Management
Settings ▸ General ▸ VPN & Device Management

Apple really doesn’t want just anyone installing potentially malicious apps on your device. To install an app from a non-paid developer account, you have to say it’s OK.

➤ After you tap Trust “, tap Allow in the alert:

Allow apps from this developer...
Allow apps from this developer...

You won’t need to do this again unless you delete all your apps from this device.

➤ Now close Settings and tap the HIITFit icon:

HIITFit running on an iPhone
HIITFit running on an iPhone

Note: If your device uses dark mode, the background and text will have a different color. By default, SwiftUI respects the device configuration and uses colors accordingly.

The app doesn’t actually look or behave any different to Live Preview, but you’re now all set up to run your own projects on this device. When you really want to get something running right away, you won’t have to stop and deal with any of this Trust business.

Key Points

  • Plan your app by listing what the user will see and what the app will do.
  • Build your app with views and subviews, customized with modifiers.
  • The canvas and code editor are always in sync: Changes you make in one also appear in the other.
  • Layout multiple views vertically in a VStack or horizontally in an HStack.
  • The Attributes inspector helps you to modify a view or a preview.
  • ForEach lets you loop over a half-open range of numbers.
  • TabView can behave like a tab view or like a page controller.
  • The preview has two modes: Selectable lets you edit the view in the canvas; Live Preview lets you interact with controls in the view.
  • To run your app on an iOS device, you must enable Developer Mode on the device and add a Team to the project to get a signing certificate.
  • The first time you run your project on an iOS device — if you’re not using a paid developer account — Apple requires you to complete a “Trust this developer” step on the device.

Where to Go From Here?

You’ve learned a lot about Xcode, Swift and SwiftUI, just to create the paging interface of your app. Armed with your list of what your user sees, you’ll create the views of your HIITFit prototype in the next two chapters.

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.
© 2026 Kodeco Inc.