SwiftUI Basics

Build your first SwiftUI interfaces from views like Text, Image and Button, arrange them with VStack, HStack and ZStack, style them with modifiers, and preview them live in Xcode.

Lesson 22 of 43Advanced18 min read

SwiftUI is Apple's framework for building user interfaces on iPhone, iPad, Mac, Apple Watch, Apple TV and Vision Pro. Instead of creating buttons and labels and then changing them step by step, you describe what the screen should look like for the current data, and SwiftUI works out how to draw it and keep it up to date.

Everything you've learned in this course comes together here. A SwiftUI screen is a struct that conforms to the View protocol. Its body property returns some View, the opaque type from the generics lesson. Views are composed from smaller views using closures, and customised with method calls called modifiers.

This lesson covers the building blocks: text, images and buttons, laying them out with stacks, styling them with modifiers, and splitting a screen into small, reusable views. You'll need a Mac with Xcode to run SwiftUI; Xcode's preview canvas shows your interface live as you type.

A first view

Swift
import SwiftUI

struct WelcomeView: View {
    var body: some View {
        VStack {
            Image(systemName: "sparkles")
                .font(.largeTitle)
            Text("Welcome to Codevra")
                .font(.title)
                .bold()
        }
        .padding()
    }
}

#Preview {
    WelcomeView()
}
  • body describes the view's content; SwiftUI calls it whenever the view needs drawing.
  • Image(systemName:) uses SF Symbols, thousands of icons that ship with Apple's platforms.
  • #Preview shows the view in Xcode's canvas without running the app.

Stacks and spacing

ViewArranges children
VStackTop to bottom
HStackLeft to right (leading to trailing)
ZStackOn top of each other, back to front
Spacer()Takes up all the spare space, pushing other views apart

Stacks accept alignment: and spacing: arguments, such as VStack(alignment: .leading, spacing: 8). Stacks nest freely: a typical list row is an HStack containing an image and a VStack of two Texts.

Modifiers and their order

A modifier is a method that returns a new, changed view: .font(), .foregroundStyle(), .padding(), .background(), .frame(), .clipShape(). They chain, and order matters, because each wraps the result of the previous one:

Swift
Text("Sale")
    .padding()
    .background(.red)     // red includes the padding

Text("Sale")
    .background(.red)     // red only behind the text
    .padding()

Modifiers applied to a stack, such as .font(.headline), flow down to the views inside it unless a child sets its own.

Small views and data

Split screens into small view structs. Data goes in through properties, just like any struct:

Swift
struct PriceTag: View {
    let amount: Double
    var body: some View {
        Text(amount, format: .currency(code: "GBP"))
            .padding(6)
            .background(.yellow, in: .capsule)
    }
}

Use it as PriceTag(amount: 19.99). Because views are lightweight structs, creating many small ones costs almost nothing, and it keeps each body short and readable.

Text, Image and Label

Most screens are built from a handful of content views. These modifiers are the ones you'll reach for most often:

ViewCommon modifiersWhat they do
Text("...").font(.headline), .foregroundStyle(.secondary)Text style and colour
Text("...").lineLimit(2), .multilineTextAlignment(.center)Line count and alignment
Image("photo").resizable(), .scaledToFit()Let a picture grow or shrink to fit
Image(systemName: "star").font(.title), .imageScale(.large)Size an SF Symbol like text
Label("Settings", systemImage: "gear").labelStyle(.iconOnly)An icon and a title together

A photo from your asset catalogue keeps its original size until you add .resizable(), which is why a large photo can spill off the screen. Follow it with .scaledToFit() to keep its proportions. SF Symbols behave like text instead, so they follow the font size.

Showing views conditionally

body can use ordinary if and switch to decide what to show:

Swift
var body: some View {
    VStack {
        if items.isEmpty {
            Text("Nothing here yet")
        } else {
            Text("\(items.count) items")
        }
    }
}

This works because body is built with a view builder, which turns each branch into a view and combines them into one type. That's also why body can't contain arbitrary statements, such as a for loop or a plain print. Repeat views with ForEach, which you'll meet in the lists lesson.

SwiftUI Syntax

SwiftUI building blocks

struct MyView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Title").font(.headline)
            HStack {
                Image(systemName: "star.fill").foregroundStyle(.yellow)
                Spacer()
                Button("Tap me") { print("Tapped") }
            }
        }
        .padding()
        .frame(maxWidth: .infinity)
    }
}
#Preview { MyView() }

SwiftUI Summary

  • A SwiftUI view is a struct conforming to View, with a body that returns some View.
  • Text, Image and Button are basic views; SF Symbols provide icons.
  • VStack, HStack and ZStack arrange views; Spacer pushes them apart.
  • Modifiers return new views, and their order changes the result.
  • Split screens into small view structs, passing data in through properties.

SwiftUI Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A product card

Swift
import SwiftUI

struct ProductCard: View {
    let name: String
    let price: Double
    let symbol: String

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: symbol)
                .font(.title)
                .frame(width: 48, height: 48)
                .background(.blue.opacity(0.15), in: .rect(cornerRadius: 10))
            VStack(alignment: .leading) {
                Text(name).font(.headline)
                Text(price, format: .currency(code: "GBP"))
                    .foregroundStyle(.secondary)
            }
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundStyle(.tertiary)
        }
        .padding()
    }
}

#Preview {
    ProductCard(name: "Desk lamp", price: 24.5, symbol: "lamp.desk")
}
Output
[Preview: a row with a lamp icon in a light blue rounded square, "Desk lamp" in bold above "£24.50" in grey, and a chevron at the right edge]
Line-by-line explanation
  1. Lines 4 to 6Data comes in through properties, so the same card works for any product.
  2. Lines 10 to 13The icon gets a fixed square frame and a rounded tinted background.
  3. Lines 14 to 18A leading-aligned VStack puts the name above the price.
  4. Line 16The currency format adds the pound sign and two decimal places.
  5. Line 19Spacer pushes the chevron to the far right.
Example 2

Modifier order

Swift
import SwiftUI

struct BadgeDemo: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("New")
                .padding(10)
                .background(.green)
                .clipShape(.capsule)

            Text("New")
                .background(.green)
                .padding(10)

            Text("New")
                .padding(10)
                .background(.orange)
                .padding(4)
                .background(.purple)
        }
    }
}

#Preview { BadgeDemo() }
Output
[Preview: a green capsule around "New"; then "New" with green only behind the letters; then "New" in an orange box with a thin purple border]
Line-by-line explanation
  1. Lines 6 to 9Padding first, then background, gives a filled badge; clipShape rounds it.
  2. Lines 11 to 13Background first only colours the text's own size; the padding comes after, outside it.
  3. Lines 15 to 19Alternating padding and backgrounds layers colours, like a border.
Example 3

A button that changes text

Swift
import SwiftUI

struct GreetingView: View {
    @State private var greeting = "Hello"

    var body: some View {
        VStack(spacing: 16) {
            Text(greeting)
                .font(.largeTitle)
            Button("Change language") {
                greeting = greeting == "Hello" ? "Hola" : "Hello"
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

#Preview { GreetingView() }
Output
[Preview: "Hello" in large type above a filled blue "Change language" button; tapping it switches the text to "Hola" and back]
Line-by-line explanation
  1. Line 4@State stores a value the view owns; the next lesson covers it in depth.
  2. Lines 10 to 12A Button has a title and an action closure that runs when tapped.
  3. Line 11Changing state makes SwiftUI call body again and update the Text.
  4. Line 13buttonStyle picks one of the system button looks.

Common SwiftUI Mistakes

Expecting views to change when you edit a plain property

Don't
struct Counter: View {
    var count = 0
    var body: some View {
        Button("\(count)") { count += 1 }   // error: 'self' is immutable
    }
}
Do
struct Counter: View {
    @State private var count = 0
    var body: some View {
        Button("\(count)") { count += 1 }
    }
}

Fixed font sizes

Don't
Text("Order summary").font(.system(size: 17))   // ignores the user's text size setting
Do
Text("Order summary").font(.headline)

One enormous body

Don't
var body: some View {
    VStack { /* 150 lines of header, list, footer and buttons */ }
}
Do
var body: some View {
    VStack {
        HeaderView()
        OrderList(orders: orders)
        CheckoutBar(total: total)
    }
}

SwiftUI in Real Projects

SwiftUI in the industry

SwiftUI is now the default for new Apple platform projects, and it's the only way to build some features, such as widgets and many watchOS and visionOS apps. Teams build design systems from small reusable views like PriceTag and PrimaryButton, and use previews with sample data to review every state of a screen without running the app.

SwiftUI FAQ

What does some View mean in SwiftUI?

some View says that body returns one specific type that conforms to View, without making you write that type out. The real type is a long nested combination of every stack and modifier you used, and the compiler works it out for you.

Why are SwiftUI views structs and not classes?

Views are lightweight descriptions of what should appear, not the on-screen objects themselves. SwiftUI recreates these structs often and compares them to work out what changed, which is cheap for value types. Data that must persist lives in state, not in the view struct.

Does the order of SwiftUI modifiers matter?

Yes. Each modifier wraps the view produced by the one before it. .padding().background(.red) colours the padded area, while .background(.red).padding() colours only behind the content and then adds clear space around it.

Can I use SwiftUI without a Mac?

No. SwiftUI is part of Apple's platforms, so you need Xcode on a Mac, or Swift Playgrounds on an iPad or Mac, to build and preview SwiftUI views. The Swift language itself runs on Linux and Windows, but SwiftUI doesn't.

What is the difference between VStack, HStack and ZStack?

VStack arranges its children vertically, HStack horizontally, and ZStack layers them on top of each other, with later views in front. Nest them to build most layouts, and use Spacer() to push views apart.

SwiftUI Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the view so it conforms to SwiftUI's View protocol.

struct HelloView:  {
    var body: some View {
        Text("Hello")
    }
}
Multiple choiceeasy

Which stack arranges views from top to bottom?

Multiple choicemedium

What does Spacer() do inside an HStack?

Predict the outputmedium

Which of these gives a coloured box that includes space around the text?

Multiple choicehard

Why does SwiftUI's body return some View rather than a named type?

SwiftUI Quiz

5 questions to check what you learned.

  1. What kind of Swift type is usually used for a SwiftUI view?

    • class
    • struct
    • enum
    • actor
  2. What does Image(systemName: "star.fill") show?

    • An image from your assets folder
    • A photo from the library
    • An emoji
    • An SF Symbols icon
  3. Why does the order of modifiers matter?

    • Each modifier wraps the view produced by the previous one
    • It doesn't
    • Only the first modifier is applied
    • Modifiers are sorted alphabetically
  4. What does #Preview do?

    • Runs the app on a device
    • Takes a screenshot
    • Shows the view live in Xcode's canvas
    • Publishes the app
  5. How should text sizes usually be set in SwiftUI?

    • With fixed point sizes
    • In pixels
    • With semantic fonts like .headline and .body, which scale with the user's settings
    • They can't be changed