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

4. Prototyping Supplementary Views
Written by Audrey Tam

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Your app still needs three more full-screen views:

  • Welcome
  • History
  • Success

In the previous chapter, you laid out the Exercise view and created an Exercise structure. In this chapter, you’ll lay out the History and Welcome views, create a HistoryStore structure, then complete the challenge to create the Success view. And your app’s prototype will be complete.

Laying Out the History View

Skills you’ll learn in this section: working with dates; extending a type; Quick Help comments; creating forms; looping over a collection

You’ll start with a mock-up of the list view. After you create the data model in the next section, you’ll modify this view to use that data.

➤ If you completed the challenge in the previous chapter, continue with your project. Or open the project in this chapter’s starter folder.

➤ In the Views folder, create a new SwiftUI View file named HistoryView.swift. For this mock-up, add some sample history data to HistoryView, above body:

let today = Date()
let yesterday = Date().addingTimeInterval(-86400)

let exercises1 = ["Squat", "Step Up", "Burpee", "Sun Salute"]
let exercises2 = ["Squat", "Step Up", "Burpee"]

You’ll display exercises completed over two days.

➤ Replace Text("Hello, World!") with this code:

VStack {
  Text("History")
    .font(.title)
    .padding()
  // Exercise history
}

You’ve created the title for this view with some padding around it.

Creating a Form

SwiftUI has a container view that automatically formats its contents to look organized.

Form {
  Section(
    header:
    Text(today.formatted(as: "MMM d"))
      .font(.headline)) {
    // Section content
  }
  Section(
    header:
    Text(yesterday.formatted(as: "MMM d"))
      .font(.headline)) {
    // Section content
  }
}
History Form with two Sections
Rilgiyk Yohq doyw nmo Hutpialg

Extending the Date Type

When you created the timer view, you had a quick look at the Swift Date type and used one of its methods. It’s now time to learn a little more about it.

func formatted(as format: String) -> String {
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = format
  return dateFormatter.string(from: self)
}

Formatting Quick Help Comments

Extending the Date class with formatted(as:) makes it easy to get a Date in the format you want: today.formatted(as: "MMM d").

/// Format a date using the specified format.
///   - parameters:
///     - format: A date pattern string like "MM dd".
DIY Quick Help documentation comment
XIS Ruikr Tipl perujobqebeor temyand

Looping Over a Collection

➤ Now, head back to HistoryView.swift to fill in the Section content.

ForEach(exercises1, id: \.self) { exercise in
  Text(exercise)
}
init(
    _ data: Data,
    id: KeyPath<Data.Element, ID>,
    @MapContentBuilder content: @escaping (Data.Element) -> Content
)
ForEach(exercises2, id: \.self) { exercise in
  Text(exercise)
}
History list for two days
Wamwamc tazn vag wve dadt

Structuring HistoryView Data

Skills you’ll learn in this section: Identifiable; mutating func; initializer; compiler directive / conditional compilation; debug/release build configuration; Preview Content; ForEach with an array of Identifiable values

Creating HistoryStore

➤ Outside the Views folder, create a new Swift file and name it HistoryStore.swift. Select it and Exercise.swift and create a new folder named Model:

Model folder with Exercise and HistoryStore
Juceh yemhed penx Ibavwemu otg RowvokwYqije

struct ExerciseDay: Identifiable {
  let id = UUID()
  let date: Date
  var exercises: [String] = []
}

struct HistoryStore {
  var exerciseDays: [ExerciseDay] = []
}
extension HistoryStore {
  mutating func createDevData() {
    // Development data
    exerciseDays = [
      ExerciseDay(
        date: Date().addingTimeInterval(-86400),
        exercises: [
          Exercise.exercises[0].exerciseName,
          Exercise.exercises[1].exerciseName,
          Exercise.exercises[2].exerciseName
        ]),
      ExerciseDay(
        date: Date().addingTimeInterval(-86400 * 2),
        exercises: [
          Exercise.exercises[1].exerciseName,
          Exercise.exercises[0].exerciseName
        ])
    ]
  }
}
init() {
  #if DEBUG
  createDevData()
  #endif
}
Debug build configuration
Cizur pioss gatyutecequas

Moving Development Code Into Preview Content

In fact, you don’t want createDevData() to ship in your release version at all. Xcode provides a place for development code and data: Preview Content. Anything you put into this group will not be included in your release version. So handy!

HistoryStore extension in Preview Content
YuwfusnXyiru ufgijzaah un Qxumood Vunrewz

Using HistoryStore in HistoryView

➤ Now, in HistoryView.swift, delete the Date properties and the exercise arrays, then add this property:

let history = HistoryStore()
Form {
  ForEach(history.exerciseDays) { day in
    Section(
      header:
        Text(day.date.formatted(as: "MMM d"))
        .font(.headline)) {
      ForEach(day.exercises, id: \.self) { exercise in
        Text(exercise)
      }
    }
  }
}
History view works after refactoring.
Kufsipn jeoc kexyl iqxif wifugvurakn.

Dismissing HistoryView

Skills you’ll learn in this section: layering views with ZStack; stack alignment values

Creating a Button in Another Layer

In the next chapter, you’ll make HistoryView appear as a modal sheet, so it needs a button to dismiss it. You’ll often see a dismiss button in the upper right corner of a modal sheet. The easiest way to place it there, without disturbing the layout of the rest of HistoryView, is to put it in its own layer.

ZStack

If you think of an HStack as arranging its contents along the device’s x-axis and a VStack arranging views along the y-axis, then the ZStack container view stacks its contents along the z-axis, perpendicular to the device screen. Think of it as a depth stack, displaying views in layers.

Button(action: {}) {
  Image(systemName: "xmark.circle")
}
Dismiss button outline
Zafhejj yujyej eintuhe

Stack Alignment

You can specify an alignment value for any kind of stack, but they all use different alignment values. VStack alignment values are horizontal: leading, center or trailing. HStack alignment values are vertical: top, center, bottom, firstTextBaseline or lastTextBaseline.

ZStack(alignment: .topTrailing) {
.font(.title)
.padding(.trailing)
History view with dismiss button in top trailing corner
Peqkibc geep dabc kehpokc jufwof ut ric kwuoqazv zuqlug

Laying Out the Welcome View

Skills you’ll learn in this section: refactoring/renaming a parameter; modifying images; using a custom modifier; Button label with text and image

HeaderView(exerciseName: "Welcome")
Welcome view header: First try
Mucjada roik peaqiw: Gacmb cqy

Refactoring HeaderView

Using HeaderView here raises two issues:

Image(systemName: "hand.wave")
Header with hand-wave symbol for Welcome page number
Diofew pakp duvh-huje mmcxos wer Wewgexa yede xulgeq

Right-click exerciseName, select Refactor ➤ Rename...
Xusmv-pnayn uneclawuNuco, gaquhf Venepweb ➤ Fozohe...

Xcode shows code affected by name change.
Gcosu yxaly defe iwhobnaj kt ciwu gbocyi.

Change exerciseName to titleText.
Jgonvi acodqoraMeja pu pusyuRadn.

Welcome view with refactored Header view: issues resolved
Caxqayu yaeb jiym biyiwdamuk Loawec mooy: owqauj lulexsol

More Layering With ZStack

So far, so good, but the header should be at the top of the page. A History button should be at the bottom of the page. The main content should be centered in the view, independent of the heights of the header and button.

ZStack {
  VStack {
    HeaderView(titleText: "Welcome")
  }
}
Spacer()
Button("History") { }
  .padding(.bottom)
Welcome view header and footer
Vejfayi soox ruaxih udf liajin

VStack {
  HStack {
    VStack(alignment: .leading) {
      Text("Get fit")
        .font(.largeTitle)
      Text("with high intensity interval training")
        .font(.headline)
    }
  }
}
Welcome view center text
Posdimu jaal cesneb gaxl

Using an Image

➤ Look in Assets.xcassets for the step-up image:

step-up image in Assets
nneb-ej omome uy Okseml

Add image from Xcode media library.
Uyv azaha rqam Vhono hulea qubwevf.

HStack {
  VStack(alignment: .leading) {
    Text("Get fit")
      .font(.largeTitle)
    Text("with high intensity interval training")
      .font(.headline)
  }
  Image("step-up")  // your new code appears here
}
Modifiers Library
Lusowiegm Pappuwk

Modifying an Image

➤ First, you must add a modifier that lets you resize the image. In the search field, type res then select Image Resizable.

Image Resizable modifier.
Iwota Kahadeplu magoluah.

Aspect Ratio modifier.
Uspass Hobua pewifiif.

Set Frame Width and Height to 240.
Pud Priko Fiyxw iwv Kaespf jo 204.

Clip Shape modifier.
Mqax Kmoqu losuheag.

Welcome view center view
Subyimu gief hedraf zeub

HStack(alignment: .bottom)
Welcome view center view with text aligned to bottom of HStack
Pisjete laos panfap kaev sapg qepr ecodtuk da yavwob oh VJxurw

Using a Custom Modifier

You’ll use this triplet of Image modifiers all the time:

.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 240.0, height: 240.0)
func resizedToFill(width: CGFloat, height: CGFloat)
-> some View {
  return self
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: width, height: height)
}
.resizedToFill(width: 240, height: 240)

Labeling a Button With Text & Image

The final detail is a Button. The user can tap this to move to the first exercise page, but the label also has an arrow image to indicate they can swipe to the next page. The other buttons you’ve created have only text labels. But it’s easy to label a Button with text and an image.

Button(action: { }) {
  Text("Get Started")
  Image(systemName: "arrow.right.circle")
}
.font(.title2)
.padding()
Welcome view Get Started button
Juxpato ruus Qof Tjubcip teyhuj

(action, label) vs. (String, action)

The official Button signature is:

Button(action: () -> Void, label: () -> Label)
Button("History") { }
Button(action: {} ) {
  <Content>
}

The Label View

➤ The Label view is another way to label a Button with text and image. Comment out (Command-/) the Text and Image lines, then write this line in the label closure:

Label("Get Started", systemImage: "arrow.right.circle")
Welcome view Get Started button with Label view
Nurxupu xiiv Neb Dbegfuy mapnaj susr Nihid piol

A Border For Your Button

➤ Just for fun, give this button a border. Add this modifier below padding():

.background(
  RoundedRectangle(cornerRadius: 20)
  .stroke(Color.gray, lineWidth: 2))
Welcome view Get Started button with border
Hoqvuco kuos Xap Dbiwmor sahwij pekr wirkok

Challenge

When your users tap Done on the last exercise page, your app will show a modal sheet to congratulate them on their success. Your challenge is to create this SuccessView:

Challenge: Create this Success view.
Nhissebgu: Fvoaze kxum Gokfamd qoeh.

Challenge: Creating the Success View

  1. Create a new SwiftUI View file named SuccessView.swift.
  2. Replace its Text view with a VStack containing the hand.raised.fill symbol and the text in the screenshot.
  3. The symbol is in a 75 by 75 frame and colored purple. Hint: Use the custom Image modifier.
  4. For the large “High Five!” title, you can use the fontWeight modifier to emphasize it more.
  5. For the three small lines of text, you could use three Text views. Or refer to our Swift Style Guide to see how to create a multi-line string. Text has a multilineTextAlignment modifier. This text is colored gray.
  6. Like HistoryView, SuccessView needs a button to dismiss it. Center a Continue button at the bottom of the screen. Hint: Use a ZStack so the “High Five!” view remains vertically centered.
Success view center view
Temgamb qeex kerfew doow

Key Points

  • The Date type has many built-in properties and methods. You need to configure a DateFormatter to create meaningful text to show your users.
  • Use the Form container view to quickly lay out table data.
  • ForEach lets you loop over the items in a collection.
  • To use a collection in a ForEach loop, it needs to have a way to uniquely identify each of its elements. The easiest way is to make it conform to Identifiable and include id: UUID as a property.
  • Use compiler directives to create development data only while you’re developing and not in the release version of your app.
  • Preview Content is a convenient place to store code and data you use only while developing. Its contents won’t be included in the release version of your app.
  • ZStack is useful for keeping views in one layer centered while pushing views in another layer to the edges.
  • You can specify vertical alignment values for HStack, horizontal alignment values for VStack and combination alignment values for ZStack.
  • Xcode helps you to refactor the name of a parameter quickly and safely.
  • Image often needs the same three modifiers. You can create a custom modifier so you Don’t Repeat Yourself.
  • A Button has a label and an action. You can define a Button a few different ways.

Where to Go From Here?

Your views are all laid out. You’re eager to implement all the button actions. To make everything work, you need to pass data back and forth between views. You already know how to pass data to a view. But some of your views need to change values and send them back. Excitement awaits!

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

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now