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 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.
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:
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:
// 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 spaceHet 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.
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.
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:
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.
// 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:
// 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.
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:
// 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:
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:
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:
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.
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:
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.
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.
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:
withAnimationgebruiken voor automatische vloeiende overgangen - Async/await: data laden met
.tasken 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
Delen
Gerelateerde artikelen

SwiftUI Performance: LazyVStack en Complexe Lijsten Optimaliseren
Optimalisatietechnieken voor LazyVStack en SwiftUI-lijsten. Verminder geheugengebruik, verbeter scrollprestaties en vermijd veelvoorkomende valkuilen.

Custom SwiftUI ViewModifiers: herbruikbare patterns voor design systems
Bouw custom ViewModifiers in SwiftUI voor een consistent design system. Patterns, best practices en praktische voorbeelden om iOS-views efficiënt te stijlen.

SwiftUI @Observable vs @State: Wanneer Wat Gebruiken in 2026
Beheers de verschillen tussen @Observable en @State in SwiftUI om de juiste tool voor state management in iOS-apps te kiezen.