SwiftUI: Moderne Interfaces Bouwen voor iOS

Een complete handleiding voor het bouwen van moderne gebruikersinterfaces met SwiftUI: declaratieve syntaxis, componenten, animaties en best practices voor iOS 18.

SwiftUI-handleiding voor het bouwen van moderne iOS-interfaces

SwiftUI heeft de ontwikkeling van interfaces op alle Apple-platformen ingrijpend veranderd. Met zijn declaratieve syntaxis en native integratie stelt dit framework ontwikkelaars in staat om elegante applicaties te bouwen met aanzienlijk minder code dan voorheen. iOS 18 brengt belangrijke verbeteringen op het gebied van prestaties en nieuwe mogelijkheden.

Why SwiftUI in 2026?

SwiftUI is met iOS 18 volwassen geworden en biedt geoptimaliseerde rendering, verbeterd geheugenbeheer en vereenvoudigde UIKit-integratie. Het is de standaard voor nieuwe iOS-applicaties.

Het Declaratieve Paradigma Begrijpen

Voordat er code geschreven wordt, is het belangrijk te begrijpen wat SwiftUI anders maakt. Met UIKit (het oudere framework) moest de ontwikkelaar iOS stap voor stap vertellen hoe de interface gebouwd moest worden: "maak een label, plaats het hier, verander de kleur wanneer de gebruiker tikt". Dit is het imperatieve paradigma.

SwiftUI werkt anders: de ontwikkelaar beschrijft wat er weergegeven moet worden, en het framework regelt de rest. Het verschil is vergelijkbaar met bocht-voor-bocht routebeschrijvingen geven (imperatief) versus simpelweg de bestemming opgeven (declaratief).

De Eerste View

In SwiftUI is elk interface-element een View. Een View is een struct die beschrijft wat er op het scherm moet verschijnen. Hier volgt het eerste scherm met een titel, ondertitel en knop:

ContentView.swiftswift
import SwiftUI

// Each screen is a struct that implements the View protocol
struct ContentView: View {

    // The "body" property describes what the view displays
    // "some View" means "a type of View, but Swift figures it out automatically"
    var body: some View {

        // VStack = "Vertical Stack": stacks elements vertically
        // spacing: 20 = 20 points of space between each element
        VStack(spacing: 20) {

            // A simple text with style modifiers
            Text("Welcome to SharpSkill")
                .font(.largeTitle)      // Large title font
                .fontWeight(.bold)      // Bold text

            Text("Prepare for your iOS interviews")
                .font(.subheadline)     // Smaller font
                .foregroundColor(.secondary)  // Gray secondary color

            // Button with an action (closure) and a label
            Button("Get Started") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)  // Filled blue button style
        }
        .padding()  // Adds margins around the VStack
    }
}

Let op de structuur: de gewenste elementen worden gedeclareerd (teksten, knop), verticaal gestapeld (VStack), en stijlen worden toegepast via modifiers (.font(), .padding()).

Belangrijk inzicht: In SwiftUI worden er geen UI-objecten "aangemaakt" -- de gewenste interface wordt beschreven. SwiftUI beheert het aanmaken, bijwerken en vernietigen van de werkelijke elementen zelfstandig.

Modifiers: Views Transformeren

Modifiers zijn methoden die na een view geketend worden om deze te transformeren. Ze werken als filters die een voor een worden toegepast. De volgorde is van belang omdat elke modifier een nieuwe view creert die de vorige omhult.

Hier volgt een voorbeeld dat illustreert waarom de volgorde cruciaal is:

ModifiersOrder.swiftswift
// Example 1: padding THEN background
Text("SwiftUI")
    .padding()           // 1. Adds 16pt of space around the text
    .background(.blue)   // 2. Blue background covers text + padding
    .foregroundColor(.white)
// Result: white text on blue rectangle with margins

// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
    .background(.blue)   // 1. Tight blue background around text only
    .padding()           // 2. Transparent padding around blue background
    .foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty space

Het verschil? In het eerste geval zit de padding "binnen" de achtergrond. In het tweede geval zit het "buiten". Een subtiel maar essentieel onderscheid voor het beheersen van layouts.

Interfaces Organiseren met Stacks

SwiftUI biedt drie hoofdcontainers voor het organiseren van views. Ze kunnen worden gezien als dozen die hun inhoud op verschillende manieren schikken.

VStack, HStack en ZStack

  • VStack (Vertical Stack): stapelt elementen van boven naar beneden
  • HStack (Horizontal Stack): plaatst elementen van links naar rechts
  • ZStack (Z-axis Stack): legt elementen over elkaar heen

Hier wordt een profielkaart gebouwd die alle drie de stacks combineert. Het doel: een foto tonen met een verificatiebadge, gevolgd door de naam en rol van de gebruiker.

ProfileCard.swiftswift
struct ProfileCard: View {
    var body: some View {
        // Main HStack: photo on left, info on right
        HStack(spacing: 16) {

            // ZStack to overlay the badge on the photo
            ZStack(alignment: .bottomTrailing) {
                // Profile image (circle)
                Image("avatar")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                // Green "verified" badge at bottom right
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                    .background(Circle().fill(.white))  // White circle behind
            }

            // VStack to stack name and role vertically
            VStack(alignment: .leading, spacing: 4) {
                Text("Marie Dupont")
                    .font(.headline)

                Text("iOS Developer")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            // Spacer pushes everything to the left
            Spacer()

            // Chevron on right indicates it's tappable
            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

De truc hier is Spacer(): een onzichtbaar element dat alle beschikbare ruimte inneemt. Zonder dit element zouden de onderdelen gecentreerd worden. Dankzij de Spacer worden ze naar links geduwd en blijft de chevron rechts verankerd.

Xcode Tip

Gebruik ⌘ + click op een willekeurige view in Xcode om de visuele inspector te openen. Modifiers kunnen worden toegevoegd zonder code te typen.

State Beheren met @State en @Binding

Statebeheer vormt het hart van SwiftUI. State is elke waarde die kan veranderen en die de interface moet bijwerken. Wanneer state verandert, herberekent SwiftUI automatisch de betrokken views.

@State: De Lokale State van een View

@State is een property wrapper die SwiftUI vertelt: "houd deze variabele in de gaten en ververs de view wanneer deze verandert". Het is ideaal voor de lokale state van een enkele view.

Hier volgt een interactieve teller om het mechanisme te begrijpen:

CounterView.swiftswift
struct CounterView: View {
    // @State creates a "source of truth" for this view
    // private because state shouldn't be modified from outside
    @State private var count = 0

    var body: some View {
        VStack(spacing: 30) {
            // This Text updates automatically when count changes
            Text("\(count)")
                .font(.system(size: 72, weight: .bold))

            HStack(spacing: 40) {
                // Decrement button
                Button(action: {
                    count -= 1  // Modifies state → view refreshes
                }) {
                    Image(systemName: "minus.circle.fill")
                        .font(.largeTitle)
                }

                // Increment button
                Button(action: {
                    count += 1
                }) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
        }
    }
}

Wanneer op een knop getikt wordt, verandert count. SwiftUI detecteert deze wijziging en voert body opnieuw uit om de weergave bij te werken. Het is niet nodig om het label handmatig bij te werken -- dat gaat volledig automatisch.

@Binding: State Delen Tussen Views

Soms moet een child view de state van een parent view aanpassen. Dat is waar @Binding van pas komt: het creert een tweerichtingsverbinding naar een bestaande @State.

Hier volgt een concreet voorbeeld: een invoerveld voor een gebruikersnaam met real-time validatie.

UsernameValidation.swiftswift
// Parent view: owns the state
struct SignupForm: View {
    @State private var username = ""       // Source of truth
    @State private var isValid = false     // Validation state

    var body: some View {
        VStack(spacing: 20) {
            // We pass BINDINGS (with $) to the child view
            UsernameField(username: $username, isValid: $isValid)

            Button("Create Account") {
                // Submit the form
            }
            .disabled(!isValid)  // Disabled if invalid
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

De child view ontvangt bindings en kan deze aanpassen:

swift
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
    @Binding var username: String   // Connection to parent's @State
    @Binding var isValid: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
                .onChange(of: username) { oldValue, newValue in
                    // Validation: at least 3 characters
                    isValid = newValue.count >= 3
                }

            // Visual feedback
            HStack {
                Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
                Text("Minimum 3 characters")
            }
            .font(.caption)
            .foregroundColor(isValid ? .green : .red)
        }
    }
}

Wanneer de gebruiker typt in het TextField, wordt username gewijzigd via de binding. De parent view ziet deze wijziging en kan deze gebruiken. Het is een nette tweerichtingscommunicatie.

Warning

Gebruik @State alleen voor eenvoudige, lokale state. Voor gegevens die over meerdere schermen gedeeld worden of voor complexe logica is het beter om @Observable (iOS 17+) of MVVM-patronen te gebruiken.

Klaar om je iOS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Dynamische Lijsten Weergeven

Lijsten zijn overal in mobiele apps. SwiftUI biedt List voor het weergeven van datacollecties met native iOS-styling (scheidingslijnen, veegacties, enz.).

Een Eenvoudige Lijst Maken

Voor het weergeven van een lijst zijn twee dingen nodig: data en een manier om deze te identificeren. Het Identifiable-protocol laat SwiftUI weten welke view bij welk data-element hoort.

Eerst wordt het datamodel gedefinieerd:

Models/Interview.swiftswift
// Identifiable lets SwiftUI track each element
struct Interview: Identifiable {
    let id = UUID()           // Auto-generated unique identifier
    let technology: String
    let difficulty: String
    let questionCount: Int
}

Nu de lijst. Het idee is om over de data te itereren met ForEach en een rij te maken voor elk element:

InterviewListView.swiftswift
struct InterviewListView: View {
    // Data to display (in reality, this would come from an API)
    @State private var interviews = [
        Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
        Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
        Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
    ]

    var body: some View {
        // NavigationStack enables the navigation bar
        NavigationStack {
            List {
                // ForEach iterates over each interview
                // Thanks to Identifiable, no need to specify id:
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
            }
            .navigationTitle("My Interviews")
        }
    }
}

Hier volgt de view voor elke rij, geextraheerd in een apart component voor leesbaarheid:

InterviewRow.swiftswift
struct InterviewRow: View {
    let interview: Interview

    var body: some View {
        HStack {
            // Left column: title and subtitle
            VStack(alignment: .leading, spacing: 4) {
                Text(interview.technology)
                    .font(.headline)

                Text(interview.difficulty)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            Spacer()

            // Badge with question count
            Text("\(interview.questionCount) Q")
                .font(.caption)
                .fontWeight(.medium)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(Color.blue.opacity(0.1))
                .cornerRadius(8)
        }
        .padding(.vertical, 4)
    }
}

Door InterviewRow in een aparte struct te plaatsen, wordt de code leesbaarder en herbruikbaar. Dit is een gevestigde best practice in SwiftUI.

Acties Toevoegen: Verwijderen en Herschikken

iOS-lijsten ondersteunen van nature verwijderen (naar links vegen) en herschikken (slepen en neerzetten). SwiftUI maakt dit triviaal met .onDelete en .onMove:

InterviewListView.swift (enhanced version)swift
struct InterviewListView: View {
    @State private var interviews = [/* ... data ... */]

    var body: some View {
        NavigationStack {
            List {
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
                // Swipe to delete
                .onDelete(perform: deleteInterview)
                // Drag and drop to reorder
                .onMove(perform: moveInterview)
            }
            .navigationTitle("My Interviews")
            .toolbar {
                // "Edit" button that activates edit mode
                EditButton()
            }
        }
    }

    // Deletes elements at specified indices
    private func deleteInterview(at offsets: IndexSet) {
        interviews.remove(atOffsets: offsets)
    }

    // Moves elements from one position to another
    private func moveInterview(from source: IndexSet, to destination: Int) {
        interviews.move(fromOffsets: source, toOffset: destination)
    }
}

Met slechts 4 extra regels code (.onDelete, .onMove, EditButton en de twee functies) ontstaat een volledig interactieve lijst. Dat is de kracht van SwiftUI.

Interfaces Animeren

SwiftUI blinkt uit in animaties. In tegenstelling tot UIKit, waar animaties veel code vereisten, is hier alles declaratief: de eindtoestand wordt beschreven en SwiftUI animeert de overgang.

Impliciete Animaties met withAnimation

De eenvoudigste manier om te animeren is een statewijziging inpakken in withAnimation. SwiftUI detecteert wat er verandert en animeert automatisch de betrokken visuele eigenschappen.

AnimatedCard.swiftswift
struct AnimatedCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                // Dimensions change based on state
                .frame(
                    width: isExpanded ? 300 : 150,
                    height: isExpanded ? 200 : 100
                )

            Button(isExpanded ? "Collapse" : "Expand") {
                // withAnimation animates ALL visual changes
                // resulting from this state change
                withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }
            .padding(.top)
        }
    }
}

Wanneer op de knop getikt wordt, schakelt isExpanded om. Dankzij withAnimation wordt de grootteverandering van de rechthoek geanimeerd met een veereffect. Het is niet nodig om te specificeren wat er geanimeerd moet worden -- SwiftUI bepaalt dat zelf.

Transities: Verschijning en Verdwijning Animeren

Transities bepalen hoe een view verschijnt of verdwijnt. Standaard is dit een fade (opacity), maar het kan worden aangepast:

TransitionDemo.swiftswift
struct TransitionDemo: View {
    @State private var showDetails = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Show Details") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetails.toggle()
                }
            }

            // This view appears/disappears with a transition
            if showDetails {
                DetailCard()
                    // Asymmetric transition: different for entry and exit
                    .transition(
                        .asymmetric(
                            insertion: .scale.combined(with: .opacity),  // Entry: zoom + fade
                            removal: .slide                               // Exit: slide
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Interview Details")
                .font(.headline)
            Text("25 questions • 45 minutes")
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(16)
    }
}

In dit voorbeeld verschijnt de kaart door in te zoomen vanuit het midden (scale + opacity) en verdwijnt door opzij te schuiven (slide). Deze micro-animaties geven de interface een levendig en professioneel karakter.

Performance

SwiftUI optimaliseert animaties automatisch. Geef de voorkeur aan .spring() voor een natuurlijk gevoel en vermijd te lange animaties (> 0.5s) die gebruikers frustreren.

Asynchrone Data Laden

In echte applicaties komen gegevens vaak van een API. Swift Concurrency (async/await) integreert naadloos met SwiftUI via de .task-modifier.

Het Loading / Error / Success Patroon

Hier volgt het standaardpatroon voor het weergeven van API-data. Er worden drie toestanden afgehandeld: laden, fout en succes.

AsyncDataView.swiftswift
struct AsyncDataView: View {
    @State private var questions: [Question] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    var body: some View {
        Group {
            if isLoading {
                // State: loading in progress
                ProgressView("Loading...")

            } else if let error = errorMessage {
                // State: error
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                    Text(error)
                    Button("Retry") {
                        Task { await loadQuestions() }
                    }
                }

            } else {
                // State: success, display data
                List(questions) { question in
                    Text(question.title)
                }
            }
        }
        // .task runs automatically when the view appears
        .task {
            await loadQuestions()
        }
        // Pull-to-refresh
        .refreshable {
            await loadQuestions()
        }
    }

    private func loadQuestions() async {
        isLoading = true
        errorMessage = nil

        do {
            // Async API call
            questions = try await QuestionService.shared.fetchQuestions()
        } catch {
            errorMessage = "Unable to load questions"
        }

        isLoading = false
    }
}

De .task-modifier is essentieel: deze start een asynchrone taak wanneer de view verschijnt en annuleert deze automatisch wanneer de view verdwijnt. Geheugenlekken zijn daardoor uitgesloten.

Conclusie

SwiftUI is onmisbaar geworden voor moderne iOS-ontwikkeling. Met iOS 18 heeft het framework een volwassenheid bereikt die het uitermate geschikt maakt voor productieapplicaties.

Belangrijkste Inzichten

  • Declaratief paradigma: beschrijven wat gewenst is, niet hoe het gebouwd moet worden
  • @State en @Binding: state reactief beheren en tussen views propageren
  • Stacks: VStack, HStack en ZStack combineren voor flexibele layouts
  • List: collecties weergeven met minimale code en native interacties
  • Animaties: withAnimation gebruiken voor automatische vloeiende overgangen
  • Async/await: data laden met .task en laad-/foutstaten afhandelen

Checklist

  • Het verschil tussen imperatief (UIKit) en declaratief (SwiftUI) begrijpen
  • Modifiers en hun toepassingsvolgorde beheersen
  • Weten wanneer @State vs @Binding vs @Observable te gebruiken
  • Layouts bouwen met Stacks
  • Lijsten implementeren met acties (verwijderen, verplaatsen)
  • Statewijzigingen animeren met withAnimation

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

SwiftUI opent de deuren naar het bouwen van elegante applicaties voor het gehele Apple-ecosysteem. De beste manier om te leren is oefenen: een klein persoonlijk project opzetten en experimenteren met elk concept uit dit artikel.

Tags

#swiftui
#ios
#swift
#ui
#apple

Delen

Gerelateerde artikelen