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:
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:
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:
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:
#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:
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:
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:
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:
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
Viewwith abodyproperty returningsome View—simple and type-safe. - Use
#Previewfor 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.






Comments
Loading comments…