Skip to content
SwiftUI: Building iOS Apps with Declarative UI

SwiftUI: Building iOS Apps with Declarative UI

DodaTech Updated Jun 20, 2026 8 min read

SwiftUI is Apple’s declarative framework for building iOS, macOS, watchOS, and tvOS user interfaces using Swift code — replacing UIKit’s imperative approach with concise, reactive view definitions that automatically update when data changes.

What You’ll Learn

You’ll build layouts with VStack, HStack, and ZStack, manage state with @State, @Binding, and @ObservedObject, navigate with NavigationStack, create lists and forms, apply modifiers, use previews for rapid development, and integrate UIKit components when needed.

Why SwiftUI Matters

SwiftUI represents Apple’s future of UI development. Doda Browser for iOS could be rebuilt entirely in SwiftUI with half the code of UIKit. Apple pushes SwiftUI aggressively — new APIs and features in iOS 17+ are SwiftUI-first or SwiftUI-only. Learning SwiftUI today prepares you for the next decade of Apple platform development.

SwiftUI Learning Path

    flowchart LR
  A[Mobile Development Overview] --> B[iOS Development]
  B --> C[SwiftUI Basics]
  C --> D[Data Flow & State]
  D --> E[Navigation & Lists]
  E --> F[Networking & Storage]
  C:::current
  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: Familiarity with Swift basics (variables, functions, structs, classes). Xcode 15+ installed from the Mac App Store.

Understanding SwiftUI’s Declarative Model

UIKit is imperative — you tell the system step-by-step what to do:

// UIKit: Create a label, set text, add to view
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
label.font = .systemFont(ofSize: 18)
view.addSubview(label)

SwiftUI is declarative — you describe what the UI should look like, and the system figures out how to create and update it:

// SwiftUI: Describe the UI, system handles the rest
Text("Hello")
    .foregroundColor(.blue)
    .font(.system(size: 18))

The difference: you describe what (the UI state), not how (create, configure, layout). SwiftUI automatically re-renders views when state changes.

Your First SwiftUI View

import SwiftUI

struct GreetingView: View {
    @State private var name = ""

    var body: some View {
        VStack(spacing: 20) {
            Text("Welcome to DodaTech")
                .font(.largeTitle)
                .fontWeight(.bold)

            TextField("Enter your name", text: $name)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)

            if !name.isEmpty {
                Text("Hello, \(name)!")
                    .font(.title2)
                    .foregroundColor(.blue)
                    .transition(.opacity)
            }

            Button("Tap Me") {
                name = ""
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

What’s happening:

  • @State declares mutable state — when name changes, SwiftUI re-renders the view
  • $name is a binding to the state — TextField reads and writes it
  • The if statement conditionally shows/hides the greeting, animated with .transition(.opacity)
  • VStack arranges children vertically with 20pt spacing

Expected output:

  • A screen with “Welcome to DodaTech” title, a text field, and a button
  • As the user types, “Hello, [name]!” appears below the field
  • Tapping “Tap Me” clears the field and hides the greeting

Layout Containers: VStack, HStack, ZStack

struct LayoutExample: View {
    var body: some View {
        VStack(spacing: 16) {
            Text("Vertical Stack")

            HStack(spacing: 12) {
                RoundedRectangle(cornerRadius: 8)
                    .fill(.blue)
                    .frame(width: 50, height: 50)
                RoundedRectangle(cornerRadius: 8)
                    .fill(.green)
                    .frame(width: 50, height: 50)
                RoundedRectangle(cornerRadius: 8)
                    .fill(.orange)
                    .frame(width: 50, height: 50)
            }

            ZStack {
                Circle()
                    .fill(.yellow)
                    .frame(width: 100, height: 100)
                Text("Overlay")
                    .foregroundColor(.black)
                    .fontWeight(.bold)
            }
        }
    }
}
ContainerDirectionUse Case
VStackVerticalForms, profiles, any top-to-bottom layout
HStackHorizontalToolbars, row items, button groups
ZStackDepth (z-axis)Badges, overlays, backgrounds

@State, @Binding, and @ObservedObject

SwiftUI provides property wrappers to manage data flow:

// 1. @State — local view state (value type)
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
        }
    }
}

// 2. @Binding — passes state to child views (no ownership transfer)
struct ChildView: View {
    @Binding var count: Int

    var body: some View {
        Button("Add") { count += 1 }
    }
}

// 3. @ObservedObject — external reference type model
class UserSettings: ObservableObject {
    @Published var username = ""
    @Published var notificationsEnabled = true
}

struct SettingsView: View {
    @ObservedObject var settings = UserSettings()

    var body: some View {
        Toggle("Notifications", isOn: $settings.notificationsEnabled)
        TextField("Username", text: $settings.username)
    }
}

SwiftUI Data Flow Diagram

    flowchart TD
    subgraph Source of Truth
        State["@State (local)"]
        Observed["@ObservedObject / @StateObject"]
        Env["@EnvironmentObject"]
    end
    State -->|$binding| Child1[Child View]
    Observed -->|$binding| Child2[Child View]
    Env -->|passed down| Child3[Any descendant]
    Child1 -->|changes propagate| SwiftUI[SwiftUI re-renders]
    Child2 --> SwiftUI
    Child3 --> SwiftUI
  

NavigationStack and List

NavigationStack (iOS 16+) replaces the older NavigationView:

struct ProductListView: View {
    let products = ["MacBook Pro", "iPhone 15", "AirPods", "iPad Air"]

    var body: some View {
        NavigationStack {
            List(products, id: \.self) { product in
                NavigationLink(destination: ProductDetailView(name: product)) {
                    HStack {
                        Image(systemName: "shippingbox")
                            .foregroundColor(.blue)
                        Text(product)
                    }
                }
            }
            .navigationTitle("DodaTech Store")
        }
    }
}

struct ProductDetailView: View {
    let name: String
    @State private var quantity = 1

    var body: some View {
        VStack(spacing: 20) {
            Text(name)
                .font(.title)

            Stepper("Quantity: \(quantity)", value: $quantity, in: 1...10)

            Text("Total: $\(quantity * 99, specifier: "%.2f")")
                .font(.headline)

            Button("Add to Cart") {
                print("Added \(quantity) x \(name)")
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .navigationTitle("Product Details")
    }
}

Modifiers

Modifiers are methods that return a modified view. Order matters because each modifier wraps the previous view:

Text("Hello")
    .font(.title)        // 1. Apply title font
    .foregroundColor(.blue)  // 2. Make text blue
    .padding()           // 3. Add default padding
    .background(.yellow) // 4. Add yellow background (behind padding)
    .cornerRadius(12)    // 5. Round corners
    .shadow(radius: 5)   // 6. Add shadow

vs.

Text("Hello")
    .padding()
    .background(.yellow)
    .cornerRadius(12)
    // ✅ Padding is inside the background — the whole padded area is yellow

Common modifiers:

  • .padding() — adds space around the view
  • .frame(width:height:) — sets explicit size
  • .foregroundColor() — text/icon color
  • .background() — background view
  • .clipShape() — clips to a shape
  • .opacity() — transparency (0-1)

Previews

Xcode previews show your SwiftUI code rendered in real time:

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

#Preview("Light Mode") {
    ContentView()
}

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

#Preview("Small Device") {
    ContentView()
        .previewDevice("iPhone SE (3rd generation)")
}

Integrating UIKit

When SwiftUI doesn’t support something, wrap UIKit with UIViewRepresentable:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

// Usage in SwiftUI
struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://dodatech.com")!)
    }
}

Common SwiftUI Errors

1. Modifier Order Mistakes

Putting .padding() after .background() applies the background only to the unpadded view. The order of modifiers creates the view hierarchy — apply padding first for full-coverage backgrounds.

2. Forgetting $ for Bindings

TextField("Name", text: name)  // ❌ Compiler error: expects Binding
TextField("Name", text: $name) // ✅ Correct binding syntax

3. State Mutation in Body

var body: some View {
    // ❌ Never mutate state during view rendering
    count += 1
    return Text("Count: \(count)")
}

Mutating state in body causes infinite re-rendering. Move mutations to button actions or event handlers.

4. Using @State for Reference Types

@State works with value types (structs, strings, ints). For classes, use @StateObject or @ObservedObject.

5. Preview Crashes from Missing Dependencies

Previews that access network, Core Data, or complex services crash without mock data. Provide sample data in previews.

6. NavigationStack Inside TabView

TabView {
    NavigationStack {
        HomeView()
    }
    .tabItem { Label("Home", systemImage: "house") }
}

Each tab needs its own NavigationStack — sharing one across tabs breaks navigation.

7. Ignoring Accessibility

SwiftUI provides accessibility modifiers: .accessibilityLabel(), .accessibilityHint(), .accessibilityValue(). Skipping them excludes users with disabilities.

Practice Questions

1. What’s the difference between @State and @ObservedObject?

@State is for local value-type state owned by the view. @ObservedObject is for reference-type (class) models owned externally, with @Published properties that trigger updates.

2. Why does modifier order matter in SwiftUI?

Each modifier wraps the previous view. .padding().background(.red) gives the padded area a red background. .background(.red).padding() gives only the unpadded view a red background, leaving the padding transparent.

3. How do you integrate a UIKit view in SwiftUI?

Wrap the UIKit view in a struct conforming to UIViewRepresentable, implementing makeUIView() and updateUIView().

4. What’s the purpose of NavigationStack?

It manages a stack of views for drill-down navigation, automatically adding back buttons and handling the navigation bar.

5. Challenge: Build a todo list app.

Build a SwiftUI todo app with: @State for the list, TextField for new items, List with swipe-to-delete, and UserDefaults persistence. Add a button to mark items as complete with a strikethrough effect.

FAQ

Can I use SwiftUI with UIKit in the same project?
Yes. SwiftUI views can be embedded in UIKit via UIHostingController, and UIKit views can be wrapped with UIViewRepresentable. Many production apps use both.
Does SwiftUI work on iOS 15 and earlier?
SwiftUI was introduced in iOS 13 but gained major features in iOS 15 (SearchBar, .badge) and iOS 16 (NavigationStack, Charts). You can use SwiftUI with deployment targets back to iOS 14.0, but some APIs require iOS 16+.
Is SwiftUI ready for production?
Yes, for most apps. Companies like Airbnb and Lyft use SwiftUI in production. However, complex custom drawing or heavy list performance may still need UIKit integration.
How do I handle Core Data with SwiftUI?
Use @FetchRequest property wrapper to automatically fetch and observe Core Data objects. Changes to the store automatically update the UI.

Try It Yourself

Build a simple SwiftUI app:

  1. Create a new iOS app in Xcode using the SwiftUI template
  2. Add a List of items with navigation to a detail view
  3. Add a form with TextField, Toggle, and Picker
  4. Use @State and @Binding to pass data between views
  5. Run on the simulator and test the interactions

What’s Next

SwiftUI is the future of Apple development. Start by rebuilding a small UIKit screen in SwiftUI to see how much cleaner the code becomes. Then explore NavigationStack, Core Data integration, and animation APIs for your next iOS project.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro