SwiftUI Basics

Reviewed & published by Brayan K

By the end of this lesson you'll be able to build a real iOS screen by hand: describe it with a View, stack and style the pieces with modifiers, add a tappable button, and make the screen update itself with @State.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

🧠 Declarative vs Imperative

In the old imperative style you wrote every step: create a label, set its text, add it to the view, then later find that label and change it by hand each time the data moved. That's a lot of bookkeeping, and bugs creep in when the screen and the data drift out of sync.

SwiftUI is declarative: you write one body that says what the screen looks like for the current state. When the state changes, SwiftUI re-runs body and updates only what actually changed. You describe the what; the framework handles the how.

1️⃣ Your First View: Text, Image & Modifiers

Everything you see in SwiftUI is a View — a lightweight struct that describes something on screen. Every view struct has a body that returns the content. You customise a view by chaining modifiers: methods like .font(), .foregroundColor(), and .padding() that each return a new, tweaked view. Read this worked example, then build it.

2️⃣ Layout with Stacks

You arrange views with three stacks. VStack lays children out top-to-bottom, HStack left-to-right, and ZStack layers them back-to-front. You nest stacks inside each other to build any layout — no manual constraints required. The spacing: argument controls the gap between children.

import SwiftUI

// Three stacks arrange views; you nest them to build any layout:
//   VStack — top to bottom     HStack — left to right
//   ZStack — back to front (layered)
struct ProfileCard: View {
    var body: some View {
        VStack(spacing: 12) {                  // vertical, 12pt between rows
            Image(systemName: "person.circle.fill")
                .font(.system(size: 70))
                .foregroundColor(.blue)

            Text("Alice Johnson")
                .font(.title2)
                .bold()

            // A horizontal row of two labels, side by side.
            HStack(spacing: 30) {              // left to right
                Text("142 Posts")
                Text("1.2k Followers")
            }
            .font(.subheadline)
            .foregroundColor(.gray)

            // ZStack stacks views on top of each other (back to front).
            ZStack {
                Color.blue                     // back layer: a blue rectangle
                Text("iOS Developer")          // front layer: text on top
                    .foregroundColor(.white)
                    .padding(8)
            }
            .cornerRadius(8)
        }
        .padding()
    }
}

Your turn. The view below is almost finished — fill in the three blanks marked ___ using the hints, then preview it.

import SwiftUI

struct GreetingView: View {
    var body: some View {
        // 🎯 YOUR TURN — replace each ___ then preview it in Xcode.

        Text("Good morning!")
            // 1) Make the text use the large-title style
            .font(___)                  // 👉 .largeTitle
            // 2) Colour the text green
            .foregroundColor(___)       // 👉 .green
            // 3) Add space around the text so it isn't cramped
            .___()                      // 👉 padding

        // ✅ Expected on screen:
        //    large green "Good morning!" text with padding around it.
    }
}

3️⃣ Buttons & @State: a Reactive Counter

A Button takes a title and an action — a closure (a block of code in { }) that runs on tap. To make the screen respond, you store data in a property marked @State. A SwiftUI view is a struct that gets rebuilt constantly, so @State tells SwiftUI to keep that value safe between rebuilds and to re-run body automatically whenever it changes. That automatic refresh is what "reactive" means.

import SwiftUI

// @State holds a value that BELONGS to this view. When it changes,
// SwiftUI automatically re-runs body and redraws the screen.
struct CounterView: View {
    @State private var count = 0          // start at 0, owned by the view

    var body: some View {
        VStack(spacing: 20) {
            // \(count) drops the current value into the text.
            Text("Count: \(count)")
                .font(.largeTitle)

            // A Button takes a title and an action closure { ... }.
            Button("Tap me") {
                count += 1                // change state -> view refreshes
            }
            .buttonStyle(.borderedProminent)   // a filled, tappable button
        }
        .padding()
    }
}

Now wire up your own state. Fill in the two blanks so the heart toggles between empty and filled each time the button is tapped:

import SwiftUI

struct LikeButton: View {
    // 1) Declare a @State Bool that starts as false
    @State private var liked = ___        // 👉 false

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: liked ? "heart.fill" : "heart")
                .font(.largeTitle)
                .foregroundColor(.red)

            Button("Toggle like") {
                // 2) Flip the value so the heart fills / empties
                liked = ___                // 👉 !liked
            }
        }
        // ✅ Expected on screen: an outline heart that becomes a
        //    filled red heart (and back) each time you tap the button.
    }
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — SwiftUI

PieceUsage
TextText("Hi").font(.title)
ImageImage(systemName: "star.fill")
VStackVStack { Text("A"); Text("B") }
HStackHStack { ... } (side by side)
ZStackZStack { ... } (layered)
ButtonButton("Tap") { count += 1 }
@State@State private var count = 0
padding.padding() / .padding(16)

Mini-Challenge: Quantity Stepper

No blanks this time — just a brief and an outline. Build a small view that shows a number with a minus button on its left and a plus button on its right, where tapping each updates the number live. You'll combine a VStack, an HStack, two Buttons, and one @State value.

import SwiftUI

struct StepperCard: View {
    // 🎯 MINI-CHALLENGE: a quantity picker
    // 1. Add a @State Int called "quantity" starting at 1.
    // 2. In a VStack, show Text("Quantity: \(quantity)") in a title font.
    // 3. Add an HStack with two buttons: "−" and "+".
    //    - "+" does  quantity += 1
    //    - "−" does  quantity -= 1   (don't worry about going below 0 yet)
    //
    // ✅ Expected on screen: a number with a − button on its left and a
    //    + button on its right; tapping each changes the number live.

    var body: some View {
        // your code here
        Text("Replace me")
    }
}

🎉 Lesson Complete!

Practice quiz

Every SwiftUI view struct must provide which property?

  • render
  • content
  • body
  • view

Answer: body. A View must supply a body property describing its content.

Which protocol must a SwiftUI screen struct conform to?

  • Screen
  • Renderable
  • UIView
  • View

Answer: View. A SwiftUI view is a struct conforming to the View protocol.

Which stack arranges its children top to bottom?

  • VStack
  • HStack
  • ZStack
  • List

Answer: VStack. VStack lays children out vertically, top to bottom.

Which stack layers views back-to-front?

  • VStack
  • ZStack
  • HStack
  • Grid

Answer: ZStack. ZStack overlaps views, layering them back to front.

What makes a stored value redraw the UI when it changes?

  • @Binding
  • @Published
  • @State
  • @Environment

Answer: @State. @State stores the value and re-runs body whenever it changes.

What does a Button's action { ... } represent?

  • A view
  • A modifier
  • A protocol
  • A closure run on tap

Answer: A closure run on tap. The trailing closure is the action that runs when the button is tapped.

Does the order of modifiers matter?

  • Yes, almost always
  • No, never
  • Only for text
  • Only in HStack

Answer: Yes, almost always. Modifiers wrap from the inside out, so order changes the result.

Which call shows an SF Symbol icon?

  • Icon("swift")
  • Image(systemName: "swift")
  • Symbol("swift")
  • SFImage("swift")

Answer: Image(systemName: "swift"). Image(systemName:) renders a built-in SF Symbol.

SwiftUI describes the screen for the current state. This style is called...

  • Imperative
  • Procedural
  • Declarative
  • Object-oriented

Answer: Declarative. Declarative means you describe the result and the framework builds it.

If body returns two views at the top level, you should...

  • Use two bodies
  • Mark it @State
  • Return nil
  • Wrap them in a stack

Answer: Wrap them in a stack. body must return one view, so wrap multiple views in a VStack or similar.

Continue this course

Frequently asked questions

What is the difference between declarative and imperative UI?

Imperative UI (like older UIKit) means you write step-by-step instructions to create a label, set its text, add it to the screen, and then update it by hand whenever data changes. Declarative UI (SwiftUI) means you describe what the screen should look like for any given state, and the framework figures out the steps and the updates for you. You write what, not how.

Why does my view say it must conform to 'some View'?

Every SwiftUI view is a struct that adopts the View protocol, and that protocol requires a body property of type 'some View'. If you forget ': View' on the struct, or your body returns nothing, the compiler complains. The fix is to declare 'struct MyView: View' and make sure body returns exactly one view (wrap multiple views in a stack).

Why can't I just change a normal variable to update the screen?

A SwiftUI view is a value type (a struct) that gets thrown away and rebuilt constantly, so a plain 'var' would lose its value on every redraw and changing it would not trigger anything. Marking a property with @State tells SwiftUI to store that value outside the struct and re-run body whenever it changes — that is what makes the UI reactive.

Does the order of modifiers actually matter?

Yes, almost always. Modifiers wrap the view from the inside out, so .padding().background(.blue) adds space first and then colours the larger area (you get a blue border), whereas .background(.blue).padding() colours the view first and then adds clear space around it. When a layout looks wrong, try swapping two modifiers.

Do I need a Mac and Xcode to follow along?

SwiftUI renders to a real iOS/macOS screen, so the proper tool is Xcode on a Mac, where the Canvas preview shows your view live as you type. If you only want to experiment with the Swift code itself you can use an online playground, but to actually see the interface you will want Xcode's preview.