SwiftUI Basics — Building Declarative User Interfaces
In this tutorial, you will learn about SwiftUI Basics. We cover key concepts, practical examples, and best practices to help you master this topic.
SwiftUI is Apple's declarative UI framework where you describe what your interface should look like and how it should behave using simple Swift code, and the framework handles layout, rendering, and updates automatically.
What You'll Learn
- Views and modifiers: the building blocks of SwiftUI
- Layout containers: VStack, HStack, ZStack
- Core views: Text, Image, Button, TextField
- @State for local state management
- @Binding for child-to-parent communication
- Building and previewing SwiftUI views
Why It Matters
SwiftUI replaces UIKit's imperative approach with a declarative model. You describe the UI for each state, and SwiftUI computes the transitions. This leads to fewer bugs, less code, and automatic support for dynamic type, dark mode, and Accessibility. SwiftUI is the future of Apple platform development.
Real-World Use
A food delivery app's order screen is built entirely in SwiftUI: a VStack containing the restaurant name (Text), a horizontal scroll of menu item photos (HStack with ScrollView), a list of selected items (List), and a "Place Order" button at the bottom. Each component updates automatically when the underlying data changes.
Learning Path
flowchart LR A[Combine Framework
Lesson 24] --> B[SwiftUI Basics
You are here] B --> C[SwiftUI Data Flow
Lesson 26] B --> D[SwiftUI Navigation
Lesson 27] style B fill:#f90,color:#fff
Your First SwiftUI View
Every SwiftUI view conforms to the View protocol and returns a body property.
import SwiftUI
struct GreetingView: View {
var body: some View {
Text("Hello, SwiftUI!")
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
.background(Color.yellow.opacity(0.3))
.cornerRadius(12)
}
}
// In a real app:
// struct ContentView: View {
// var body: some View {
// GreetingView()
// }
// }
The view declares what to show: a Text with modifiers applied. Modifiers return a new view with the applied change. The order of modifiers matters because each wraps the previous result.
Layout Containers
SwiftUI provides three primary layout containers: VStack (vertical), HStack (horizontal), and ZStack (depth-based).
import SwiftUI
struct LayoutExample: View {
var body: some View {
VStack(spacing: 16) {
Text("Profile Card")
.font(.title)
.fontWeight(.bold)
HStack(spacing: 12) {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text("Alice Johnson")
.font(.headline)
Text("iOS Developer")
.font(.subheadline)
.foregroundColor(.secondary)
Text("San Francisco, CA")
.font(.caption)
.foregroundColor(.gray)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
HStack(spacing: 40) {
StatView(value: "1.2K", label: "Followers")
StatView(value: "340", label: "Following")
StatView(value: "89", label: "Posts")
}
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue)
.frame(height: 44)
Text("Edit Profile")
.foregroundColor(.white)
.fontWeight(.semibold)
}
}
.padding()
}
}
struct StatView: View {
let value: String
let label: String
var body: some View {
VStack(spacing: 2) {
Text(value)
.font(.headline)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
VStack and HStack distribute children along their axis. ZStack overlays children, useful for badges, gradients, and layered content.
State with @State
@State creates mutable state that SwiftUI observes. When the state changes, the view re-renders automatically.
import SwiftUI
struct CounterView: View {
@State private var count = 0
@State private var isOn = false
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
Text("Count: \(count)")
.font(.largeTitle)
HStack(spacing: 20) {
Button("Decrement") {
count -= 1
}
.buttonStyle(.bordered)
.disabled(count <= 0)
Button("Increment") {
count += 1
}
.buttonStyle(.borderedProminent)
}
Toggle("Switch is \(isOn ? "ON" : "OFF")", isOn: $isOn)
.padding(.horizontal, 40)
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 40)
if !name.isEmpty {
Text("Hello, \(name)!")
.font(.title2)
.foregroundColor(.green)
}
}
.padding()
}
}
@State properties are private and owned by the view. The $ prefix creates a binding ($count, $isOn, $name) that passes read-write access to child controls like Toggle and TextField.
@Binding for Child Communication
@Binding lets a child view read and write a state variable owned by a parent.
import SwiftUI
struct ParentView: View {
@State private var volume: Double = 0.5
@State private var isMuted = false
var body: some View {
VStack(spacing: 20) {
Text("Volume Control")
.font(.title)
VolumeSliderView(volume: $volume, isMuted: $isMuted)
Text("Volume: \(Int(volume * 100))%")
.font(.headline)
Image(systemName: isMuted ? "speaker.slash.fill" : "speaker.wave.2.fill")
.font(.largeTitle)
.foregroundColor(isMuted ? .red : .blue)
}
.padding()
}
}
struct VolumeSliderView: View {
@Binding var volume: Double
@Binding var isMuted: Bool
var body: some View {
VStack(spacing: 12) {
Slider(value: $volume, in: 0...1)
.tint(isMuted ? .gray : .blue)
Button(isMuted ? "Unmute" : "Mute") {
isMuted.toggle()
if isMuted {
volume = 0
} else {
volume = 0.5
}
}
.buttonStyle(.bordered)
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
}
}
The child view does not own the state — it only has read-write access through the @Binding. When the child changes the binding, both the parent and the child re-render.
Common Views
Text and Image
struct TextImageExample: View {
var body: some View {
VStack(spacing: 20) {
Text("Styling Text")
.font(.system(.title, design: .monospaced))
.fontWeight(.black)
.italic()
.underline()
.strikethrough()
Text("Multiline text that automatically wraps to the next line when it reaches the available width of the container.")
.lineSpacing(8)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Image(systemName: "cloud.sun.rain.fill")
.font(.system(size: 60))
.foregroundStyle(.blue, .yellow, .cyan)
.symbolRenderingMode(.palette)
AsyncImage(url: URL(string: "https://picsum.photos/200")) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(12)
case .failure:
Image(systemName: "photo")
.font(.largeTitle)
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
}
.padding()
}
}
Button Styles
struct ButtonStylesExample: View {
var body: some View {
VStack(spacing: 16) {
Button("Default Button") { print("tapped") }
Button("Bordered") { }
.buttonStyle(.bordered)
Button("Bordered Prominent") { }
.buttonStyle(.borderedProminent)
Button("Borderless") { }
.buttonStyle(.borderless)
Button(role: .destructive) {
print("Delete tapped")
} label: {
Label("Delete", systemImage: "trash")
}
.buttonStyle(.borderedProminent)
Button {
print("Custom tapped")
} label: {
HStack {
Image(systemName: "star.fill")
Text("Custom Button")
}
.padding()
.background(
LinearGradient(colors: [.purple, .blue],
startPoint: .leading,
endPoint: .trailing)
)
.foregroundColor(.white)
.cornerRadius(12)
}
}
.padding()
}
}
Modifier Order Matters
The order of modifiers changes the result significantly.
struct ModifierOrderExample: View {
var body: some View {
VStack(spacing: 20) {
// Correct order: background inside padding
Text("Correct order")
.padding(20)
.background(Color.yellow)
.cornerRadius(8)
// Wrong order: padding outside background
Text("Wrong order")
.background(Color.yellow)
.cornerRadius(8)
.padding(20)
// Frame then background
Text("Frame first")
.frame(width: 200, height: 60)
.background(Color.blue)
.foregroundColor(.white)
// Background then frame
Text("Background first")
.background(Color.red)
.foregroundColor(.white)
.frame(width: 200, height: 60)
}
.padding()
}
}
Each modifier wraps the previous view. Think of modifiers as a chain: view.modifier1().modifier2() applies modifier1 to the view, then modifier2 to the result.
Common Mistakes
Putting logic in body: The
bodyproperty should only describe the UI. Move logic to computed properties, methods, or view models.Forgetting $ prefix for bindings: Controls like
TextField,Slider, andToggleneed aBinding. Use$prefix on@Statevariables.Using too many ZStacks: Complex ZStack nesting can hurt performance. Prefer overlay and background modifiers.
Not handling optional data: When displaying optional strings or images, unwrap with
if letor use??default values.Ignoring dynamic type: Test your UI with larger accessibility text sizes. Use
font(.body)instead of fixed sizes where possible.
Practice Questions
- What is the difference between @State and @Binding?
- Why does modifier order matter in SwiftUI?
- How do VStack, HStack, and ZStack differ?
- What does the
$prefix do in front of a @State variable? - Challenge: Build a "Tip Calculator" with a TextField for bill amount, a Slider for tip percentage (0-30%), a Toggle to split the bill, and a Stepper for number of people. Display the total bill, tip amount, and per-person cost.
Mini Project
Create a ProfileCardView with:
- An AsyncImage loading a profile picture from a URL
- Text fields for name, bio, and location
- HStack showing follower stats
- A "Follow" button that toggles between "Follow" and "Following"
- A custom color theme using gradients
- Proper padding, corner radius, and shadow styling
FAQ
What's Next
Master data flow with SwiftUI Data Flow using ObservableObject and EnvironmentObject, or learn screen transitions in SwiftUI Navigation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro