# 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. - Published: 2026-01-05 - Updated: 2026-04-02 - Author: Anthony Fillion-Maillet - Tags: swiftui, ios, swift, ui, apple - Reading time: 12 min --- 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: ```swift // ContentView.swift 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: ```swift // ModifiersOrder.swift // 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. ```swift // ProfileCard.swift 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: ```swift // CounterView.swift 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. ```swift // UsernameValidation.swift // 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. ## 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: ```swift // Models/Interview.swift // 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: ```swift // InterviewListView.swift 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: ```swift // InterviewRow.swift 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`: ```swift // InterviewListView.swift (enhanced version) 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. ```swift // AnimatedCard.swift 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: ```swift // TransitionDemo.swift 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. ```swift // AsyncDataView.swift 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 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/ios/swiftui-building-modern-interfaces