Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 015 / 100

SwiftUI tutorial: your first view — a step-by-step guide

Declarative UI in Swift.

Expona Admin7 min read

Introduction

By day 15 of building iOS apps, you've learned Swift fundamentals—but now comes the part where it all becomes visible. SwiftUI is Apple's modern declarative UI framework, and it changes how we think about building interfaces. This SwiftUI tutorial: your first view — a step-by-step guide walks you through creating your first screen, understanding how SwiftUI's reactive model works, and composing views that scale from simple text to complex layouts.

A view — A struct with a body

In SwiftUI, everything you see on screen is a view. A view is a struct that conforms to the View protocol. That's it. No inheritance chains, no manual layout code, no Interface Builder—just a struct with a required body property that returns some View.

Here's the absolute minimum:

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI")
    }
}

That single Text view is enough to render text on screen. The body property is a computed property that describes what should appear. When you mark it as some View, you're using an opaque return type—Swift knows the exact concrete type but hides it from callers, which makes SwiftUI's type system flexible.

When you need multiple views on screen, you don't add them as separate properties. Instead, you use container views like VStack (vertical stack), HStack (horizontal), or ZStack (layering). Here's a practical example from a login screen we built recently:

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Welcome back")
            TextField("Email", text: .constant(""))
            Button("Tap me") {
                print("Button tapped")
            }
        }
    }
}

The VStack arranges the text, text field, and button vertically. Each element inside is a view; each view has its own body. This is declarative—you're describing the structure, not imperatively placing coordinates.

Notice the TextField: it requires a binding to state, which we'll cover in depth later. For now, .constant("") creates a read-only binding, perfect for static previews.

Previews — #Preview shows live UI in Xcode

One of SwiftUI's biggest productivity wins is live preview. As you type your view code, Xcode renders it in real time on the right side of the editor. No need to build and run to the simulator constantly—you see changes instantly.

To enable preview, add a #Preview macro at the bottom of your file:

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, SwiftUI")
            Button("Tap me") {
                print("tapped")
            }
        }
    }
}

#Preview {
    ContentView()
}

When you add this and Xcode finishes indexing, the preview canvas appears. You'll see your view rendered as it would appear on device. Change the text, adjust spacing, modify colors—every keystroke updates the preview instantly. This tight feedback loop is why many teams found their build iteration time dropped by 60–70% after switching to SwiftUI.

You can also preview different device sizes or configurations:

swift
#Preview("iPhone 15") {
    ContentView()
        .preferredColorScheme(.light)
}

#Preview("Dark mode") {
    ContentView()
        .preferredColorScheme(.dark)
}

This is invaluable when designing for accessibility or testing layouts across device families. We always run previews for both light and dark modes before shipping.

Modifiers — font, padding and foregroundStyle customize views

SwiftUI views start minimal. To customize appearance, you apply modifiers. A modifier is a method that returns a new view with adjusted properties. They chain together, reading left to right.

Here's a text view with basic modifiers:

swift
Text("Hello, SwiftUI")
    .font(.title)
    .foregroundStyle(.blue)
    .padding()

Order matters. Each modifier wraps the previous result. .font(.title) makes the text large, .foregroundStyle(.blue) colors it blue, and .padding() adds space around it.

For a real-world scenario: on a product listing screen, we needed card-style buttons with consistent styling. Instead of repeating modifiers everywhere, we created a custom modifier:

swift
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.white)
            .cornerRadius(12)
            .shadow(radius: 4)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

Now any view can call .cardStyle() to get that exact appearance:

swift
VStack {
    Text("Product name")
        .font(.headline)
    Text("$19.99")
        .font(.subheadline)
        .foregroundStyle(.gray)
}
.cardStyle()

Common modifiers you'll use constantly:

  • .font(.body, .headline, .title, .caption) — Typography
  • .foregroundStyle(.blue, .red, .green) — Text color
  • .padding(), .padding(.horizontal), .padding(.vertical, 20) — Spacing
  • .background(.white) — Background color or shape
  • .cornerRadius(12) — Rounded corners
  • .shadow(radius: 4) — Drop shadow
  • .frame(width: 200, height: 50) — Explicit size
  • .opacity(0.5) — Transparency

Modifiers are the primary way to style in SwiftUI. There's no CSS or separate style sheets—everything is code, which means it's type-safe and refactorable.

Composition — Small views combine into screens

The real power emerges when you break screens into small, reusable views. Instead of one massive ContentView, you compose it from focused pieces. This mirrors how we'd structure components in React or Flutter.

Let's say you're building a user profile screen. Rather than one 200-line view, extract components:

swift
struct ProfileView: View {
    let userName: String
    let avatarURL: String
    
    var body: some View {
        VStack(spacing: 16) {
            ProfileHeader(avatarURL: avatarURL, userName: userName)
            ProfileStats()
            ActionButtons()
        }
        .padding()
    }
}

struct ProfileHeader: View {
    let avatarURL: String
    let userName: String
    
    var body: some View {
        VStack {
            AsyncImage(url: URL(string: avatarURL)) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                Color.gray
            }
            .frame(width: 80, height: 80)
            .clipShape(Circle())
            
            Text(userName)
                .font(.headline)
        }
    }
}

struct ProfileStats: View {
    var body: some View {
        HStack(spacing: 20) {
            VStack {
                Text("2.1K").font(.headline)
                Text("Followers").font(.caption).foregroundStyle(.gray)
            }
            Divider()
            VStack {
                Text("847").font(.headline)
                Text("Following").font(.caption).foregroundStyle(.gray)
            }
        }
        .padding()
        .background(.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

struct ActionButtons: View {
    var body: some View {
        HStack(spacing: 12) {
            Button("Follow") {}
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .background(.blue)
                .foregroundStyle(.white)
                .cornerRadius(8)
            
            Button("Message") {}
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .background(.gray.opacity(0.2))
                .cornerRadius(8)
        }
    }
}

#Preview {
    ProfileView(
        userName: "Alice Chen",
        avatarURL: "https://example.com/avatar.jpg"
    )
}

Each smaller view is testable in isolation via preview. You can see ProfileStats in light mode or ActionButtons with different sizes—all without running the full app. When you change ProfileHeader, only that preview refreshes, not the whole screen.

This composition pattern scales from small apps to large codebases. On a recent e-commerce project, we had 60+ view components that combined into 15 screens. Because each component was small and focused, onboarding new engineers was straightforward—they could understand ProductCard in isolation before seeing how it fits into ProductGridView.

Common mistakes

Modifiers in the wrong order: If you apply .padding() before .background(), the background won't extend to the padding. Always think of modifiers as wrapping: .padding().background() means "add padding, then draw background around that padded content." Usually you want padding inside the background.

Trying to mutate view state directly: SwiftUI views are structs (value types), not classes. You can't do self.name = "new" inside a button tap. Use @State for local state (coming day 16) or lift state to a parent view.

Over-nesting stacks: It's tempting to nest VStack and HStack deeply. Stop at 2–3 levels. If you're nesting more, extract a component. Deep nesting makes previews slower and code harder to reason about.

Forgetting the Preview macro: Leaving off #Preview means you lose the live feedback loop. Always include it, even for small utility views. The time saved during iteration pays for itself quickly.

Key takeaways

  • A view is a struct conforming to View with a body property returning some View—simple and type-safe.
  • Use #Preview for instant visual feedback as you code; it's one of SwiftUI's biggest productivity wins.
  • Modifiers chain to customize appearance; order matters because each wraps the previous result.
  • Compose screens from small, focused views rather than one large view; this improves testability, reusability, and team velocity.
  • Extract custom modifiers when you repeat styling patterns across multiple views.
  • Stacks (VStack, HStack, ZStack) are your layout tools; keep nesting shallow and extract components early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles