Voorbereiding op iOS-interviews

iOS-interviewvragen

20 veelgestelde iOS-interviewvragen. iOS-ontwikkeling is een veelgevraagd vakgebied voor Swift-, SwiftUI- en UIKit-apps, met lifecycle-, concurrency-, netwerk-, opslag-, performance-, test- en App Store-releasetaken. De vragen behandelen verschillende niveaus, en je kunt hardop oefenen met antwoorden in onze interviewtrainer.

Start een iOS AI-interviewGeen creditcard nodig. 1 gratis sessie beschikbaar.
Oefen technische interviews in het EngelsEen modus waarin anderstaligen kunnen oefenen met het afleggen van technische interviews.

Vragen voor beginners

1Welke verantwoordelijkheden moet een UIViewController hebben in een typische iOS MVC-architectuur (Model-View-Controller)?

In klassieke iOS-MVC (Model-View-Controller) fungeert UIViewController als de Controller die bemiddelt tussen View-componenten en Model-gegevens. De primaire verantwoordelijkheden omvatten het beheren van de view-levenscyclus (bijvoorbeeld viewDidLoad, viewWillAppear), het opzetten en bijwerken van UI-elementen, het afhandelen van directe gebruikersinteracties (zoals knoptikken, delegates en target-actions) en het orkestreren van schermovergangen of presentaties. Verantwoordelijkheden die niet aan de UI zijn gerelateerd — zoals directe netwerkcommunicatie, persistentie en zware bedrijfslogica — moeten worden gedelegeerd aan specifieke service- of model-objecten om te voorkomen dat de view controller verandert in een Massive View Controller.

class UserProfileViewController: UIViewController {
    private let userService: UserServiceProtocol
    private let profileView = UserProfileView()
    
    init(userService: UserServiceProtocol) {
        self.userService = userService
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    override func loadView() {
        view = profileView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        profileView.editButton.addTarget(self, action: #selector(didTapEdit), for: .touchUpInside)
        fetchProfile()
    }
    
    private func fetchProfile() {
        userService.fetchCurrentUser { [weak self] result in
            DispatchQueue.main.async {
                if case .success(let user) = result {
                    self?.profileView.configure(with: user)
                }
            }
        }
    }
    
    @objc private func didTapEdit() {
        // Handle user action or trigger navigation
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

2Wat is de rol van een provisioning profile bij de release van een iOS-app?

Bij een iOS-release fungeert een provisioning profile als een cryptografisch ondertekend pakket dat de beveiligingsvereisten van Apple koppelt aan de app-binary en doelapparaten. De primaire rol is om het iOS-besturingssysteem en App Store Connect te laten weten dat de applicatie geautoriseerd is om te draaien onder specifieke distributieregels. Een provisioning profile bundelt verschillende essentiële onderdelen: de App ID (Bundle Identifier), de geautoriseerde ondertekeningscertificaten die bevestigen wie de app heeft gebouwd, de rechten en mogelijkheden (entitlements en capabilities) die aan de app zijn toegekend (zoals Push Notifications, Sign in with Apple of iCloud), en — bij niet-App Store-profielen zoals Development of Ad-Hoc — een lijst van toegestane apparaat-UDID's (Unique Device Identifiers). Voor App Store-releaseprofielen worden individuele UDID's weggelaten, omdat Apple distributie naar elk consumentenapparaat toestaat via de App Store.

Provisioning Profile (.mobileprovision)
├── App ID / Bundle Identifier (e.g., com.example.app)
├── Certificates (Public key / Distribution Certificate)
├── Entitlements (e.g., Push Notifications, Associated Domains)
└── Device UDIDs (Present for Development/Ad-Hoc; omitted for App Store distribution)
Probeer deze vraag te beantwoorden met een AI-begeleider

3Wat betekent het voor een iOS-app om werk op de achtergrond uit te voeren, en welke basislimieten stelt het systeem aan dat werk?

Het uitvoeren van werk op de achtergrond in iOS betekent dat code wordt uitgevoerd wanneer de app op dat moment niet actief op het scherm of zichtbaar voor de gebruiker is. Wanneer een gebruiker een app verlaat, gaat deze over van de actieve voorgrondstatus naar de achtergrond, en kort daarna naar een onderbroken status (suspended) waarin de uitvoering wordt gepauzeerd, hoewel het geheugen in het RAM behouden blijft. Om de batterijduur, responsiviteit van het systeem en apparaatbronnen te sparen, controleert iOS de achtergronduitvoering strikt. Het systeem beperkt hoelang een app code kan uitvoeren zodra deze naar de achtergrond is verplaatst (meestal beperkt tot een kort tijdsvenster van ongeveer 30 seconden, tenzij specifieke achtergrondmodi of geplande taken worden gebruikt), verlaagt de prioriteit van de CPU en het netwerk, en kan onderbroken apps beëindigen als het apparaat onder geheugendruk komt te staan.

import UIKit

NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    print("App entered background. Code execution will pause soon unless extended.")
}
Probeer deze vraag te beantwoorden met een AI-begeleider

4Wat is een Combine-publisher, en hoe verschilt deze van een subscriber in een typische iOS-gegevensstroom?

In Combine zendt een Publisher in de loop van de tijd een stroom waarden uit en kan deze eindigen met een voltooiingsgebeurtenis (hetzij succesvol voltooid, hetzij mislukt met een fout). De Publisher declareert twee gekoppelde typen: `Output` (het type gegevens dat wordt geproduceerd) en `Failure` (het type fout dat kan worden gegenereerd). Een Subscriber ontvangt waarden en levenscyclusgebeurtenissen van een publisher. In een typische iOS-gegevensstroom fungeren publishers als de bron of producent van asynchrone gebeurtenissen (zoals netwerkresponsen, notificaties of gebruikersinvoer), terwijl subscribers fungeren als de consumenten die reageren op uitgezonden waarden, fouten afhandelen en reageren op voltooiing (zoals het bijwerken van de UI-status of het schrijven naar een database). De subscriber koppelt zich aan een publisher, ontvangt een abonnementstoken en vraagt om elementen.

import Combine

// Publisher: emits a sequence of integers
let numberPublisher = [1, 2, 3].publisher

// Subscriber: consumes the emitted integers
let cancellable = numberPublisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    },
    receiveValue: { value in
        print("Received: \(value)")
    }
)
Probeer deze vraag te beantwoorden met een AI-begeleider

5Wat betekent `async/await` in Swift, en hoe gebruik je dit om een asynchrone API aan te roepen vanuit een iOS-app?

`async/await` is de ingebouwde syntaxis van Swift om asynchrone code op een lineaire, leesbare en sequentiële manier te schrijven in plaats van geneste closures en completion handlers te gebruiken. Door een functie te markeren met `async` weet de compiler dat de functie zijn uitvoering kan onderbreken terwijl er wordt gewacht tot langdurig werk (zoals netwerkverzoeken of bestands-I/O) is voltooid. Het sleutelwoord `await` markeert een onderbrekingspunt (suspension point): de uitvoering van de huidige functie pauzeert, waardoor de onderliggende thread vrijkomt om ander werk te doen, en wordt hervat zodra het verwachte resultaat of de fout beschikbaar is. Om een asynchrone API aan te roepen in een iOS-app, roep je de `async`-methode aan voorafgegaan door `await` (en `try await` als de functie fouten kan opwerpen) vanuit een asynchrone context, zoals binnen een andere `async`-functie of een `Task`-blok.

func fetchUserData(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    
    return try JSONDecoder().decode(User.self, from: data)
}
Probeer deze vraag te beantwoorden met een AI-begeleider

6Wanneer gebruik je UserDefaults in een iOS-app, en welke soorten gegevens horen daar niet in thuis?

UserDefaults is ontworpen voor het opslaan van kleine, lichte gebruikersvoorkeuren, instellingen en vlaggen die bewaard moeten blijven over sessies heen (zoals themavoorkeuren, volumeniveau of een `hasSeenOnboarding`-boolean). Het ondersteunt property list-typen zoals String, Int, Double, Bool, Date, Data, Array en Dictionary. Je hoort het volgende NIET op te slaan in UserDefaults: 1. Gevoelige gegevens (zoals wachtwoorden, privésleutels of authenticatietokens), omdat UserDefaults gegevens ongeëncripteerd opslaat in een platte-tekst plist-bestand. 2. Grote datasets, documenten of mediabestanden (zoals afbeeldingen, audio of grote JSON-payloads), omdat het volledige plist-bestand bij het opvragen in het geheugen wordt geladen. Dit leidt tot een hoog geheugengebruik en vertraagt het opstarten van de app.

// Saving simple preference values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set("dark", forKey: "preferredTheme")

// Reading values back
let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let theme = UserDefaults.standard.string(forKey: "preferredTheme") ?? "system"
Probeer deze vraag te beantwoorden met een AI-begeleider

7Wat is ARC (Automatic Reference Counting) in Swift, en hoe beheert het de levensduur van class-instanties in een iOS-app?

ARC (Automatic Reference Counting) is het geheugenbeheermechanisme van Swift tijdens het compileren om de levensduur van class-instanties (referentietypen) te volgen en te beheren. Telkens wanneer een nieuwe instantie van een class wordt aangemaakt en aan een sterke referentie wordt toegewezen, houdt ARC een interne referentietelling bij voor die instantie. Zolang er ten minste één sterke referentie naar een instantie bestaat, houdt ARC de instantie in het geheugen. Wanneer alle sterke referenties zijn verwijderd en de referentietelling naar nul zakt, dealloceert ARC de instantie onmiddellijk om geheugen vrij te maken, waarbij de `deinit`-methode van de instantie automatisch net vóór de deallocatie wordt aangeroepen. In tegenstelling tot garbagecollection tijdens uitvoering in omgevingen zoals Java of .NET, voert ARC geen periodieke sweep-cycli uit; de Swift-compiler voegt tijdens het compileren de juiste retain- en release-aanroepen in de binary in.

class User {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    deinit {
        print("\(name) deallocated")
    }
}

var ref1: User? = User(name: "Alice") // count = 1
var ref2: User? = ref1               // count = 2

ref1 = nil                           // count = 1
ref2 = nil                           // count = 0 -> deinit called
Probeer deze vraag te beantwoorden met een AI-begeleider

8Hoe voer je een eenvoudig HTTP (Hypertext Transfer Protocol) GET-verzoek uit met URLSession in een iOS-app?

Om een eenvoudig HTTP GET-verzoek uit te voeren in een iOS-app, maak je een `URL`-object aan en geef je dit door aan een `URLSession`-instantie, zoals `URLSession.shared`. Je kunt het verzoek uitvoeren met een completion handler op een data-task (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) of via moderne Swift-concurrency (`let (data, response) = try await URLSession.shared.data(from: url)`). Bij de traditionele data-task-aanpak moet je `.resume()` aanroepen op de taak om deze te starten, eventuele transportfouten afhandelen, de statuscode in `HTTPURLResponse` controleren en zorgen dat eventuele UI-updates op de main thread worden uitgevoerd.

guard let url = URL(string: "https://api.example.com/items") else { return }

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        print("Network error: \(error.localizedDescription)")
        return
    }
    
    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
        print("Server error or invalid status code")
        return
    }
    
    if let data = data {
        DispatchQueue.main.async {
            // Update UI on main thread
        }
    }
}
task.resume()
Probeer deze vraag te beantwoorden met een AI-begeleider

9Wat is Instruments in Xcode en hoe gebruik je het om een eenvoudig prestatieprobleem in een iOS-app te onderzoeken?

Instruments is het profilerings- en analysehulpmiddel van Apple dat wordt meegeleverd met Xcode. Hiermee kunnen ontwikkelaars runtimegedrag, CPU-gebruik, geheugentoewijzingen, geheugenlekken en UI-renderingprestaties monitoren. Om een eenvoudig prestatieprobleem te onderzoeken (zoals haperingen in de gebruikersinterface of een hoog CPU-gebruik), start je Instruments vanuit Xcode (via Product -> Profile), kies je een passend sjabloon (zoals Time Profiler voor CPU-knelpunten of Allocations/Leaks voor geheugenproblemen) en neem je een trace op terwijl je de problematische gebruikersstroom op een fysiek apparaat reproduceert. Na de opname analyseer je de call tree en de zwaarste stack traces om exact te achterhalen welke methoden vertragingen of overmatig brongebruik veroorzaken. Zo kun je het knelpunt nauwkeurig meten en lokaliseren voordat je codewijzigingen doorvoert.

1. In Xcode, select Product -> Profile (Builds with Release optimizations).
2. Select the 'Time Profiler' template.
3. Click Record and perform the sluggish action in the app.
4. Stop recording.
5. Inspect the Call Tree tab:
   - Enable 'Separate by Thread' and 'Hide System Libraries'.
   - Trace the heaviest call stack on the Main Thread to identify the blocking function.
Probeer deze vraag te beantwoorden met een AI-begeleider

10Wat is APNs (Apple Push Notification service) en welke rol speelt het bij het afleveren van pushnotificaties aan een iOS-app?

APNs (Apple Push Notification service) is de cloudgebaseerde tussenliggende dienst van Apple die pushnotificaties veilig routeert van backendservers (provider-servers) naar Apple-apparaten. Omdat mobiele apparaten niet met de backend van elke afzonderlijke app een continue, open socketverbinding kunnen onderhouden zonder de batterij leeg te trekken en overmatig netwerkbandbreedte te verbruiken, onderhoudt Apple één persistente, energiezuinige verbinding tussen elk apparaat en APNs. Wanneer de app zich registreert voor notificaties op afstand, genereert APNs een uniek, opaak apparaattotoken dat die specifieke app-installatie op dat specifieke apparaat identificeert. De app stuurt dit token naar de backend-provider-server. Wanneer de provider de gebruiker wil notificeren, stelt deze een payload samen en verstuurt deze samen met het apparaattotoken via HTTP/2 naar APNs. APNs zoekt vervolgens de actieve verbinding voor dat apparaat op en levert de pushnotificatie-payload rechtstreeks af aan iOS, dat vervolgens de melding weergeeft of de app op de achtergrond activeert zoals geconfigureerd.

[Provider Server] ---> (HTTP/2 Request + Device Token + Payload) ---> [APNs]
                                                                           |
                                                               (Persistent APNs Connection)
                                                                           v
                                                                    [iOS Device / App]
Probeer deze vraag te beantwoorden met een AI-begeleider

Vragen voor gevorderden

11Hoe zou je een grote UIViewController herstructureren die UI-updates (User Interface), validatie, netwerkverkeer en navigatie afhandelt?

Het refactoren van een Massive View Controller (MVC) moet stapsgewijs gebeuren om risico's te beperken en afzonderlijke verantwoordelijkheden te scheiden in specifieke lagen: 1. Netwerkverkeer en gegevens extraheren: Verplaats API-aanroepen, gegevenspersistentie en JSON-parsing vanuit de viewcontroller naar aparte Service- of Repository-klassen met protocol-abstracties. 2. Presentatiestatus en validatie extraheren: Introduceer een ViewModel (of Presenter). Verplaats invoervalidatie, stringformattering en UI-statusbeheer naar het ViewModel, zodat deze logica geïsoleerd met unit-tests kan worden getest. 3. Navigatie extraheren: Pas het Coordinator-patroon toe (of Router / Flow Controller) om push/present-aanroepen en navigatiestromen uit de viewcontroller te verwijderen. 4. De Viewcontroller slank houden: Behoud uitsluitend UI-layout, subview-configuratie, lifecycle-hooks en het binden van UI-componenten aan het ViewModel. 5. Incrementele uitvoering met tests: Voeg tijdens het refactoren unit-tests toe voor nieuw geëxtraheerde services en ViewModels om te garanderen dat bestaand gedrag ongewijzigd blijft.

// 1. Extracted Navigation
protocol LoginCoordinatorProtocol: AnyObject {
    func showHomeScreen()
}

// 2. Extracted Business Logic & State
final class LoginViewModel {
    private let authService: AuthServiceProtocol
    private weak var coordinator: LoginCoordinatorProtocol?
    
    init(authService: AuthServiceProtocol, coordinator: LoginCoordinatorProtocol) {
        self.authService = authService
        self.coordinator = coordinator
    }
    
    func login(email: String, password: String) {
        guard email.contains("@") else { return }
        authService.login(email: email, password: password) { [weak self] result in
            if case .success = result { self?.coordinator?.showHomeScreen() }
        }
    }
}

// 3. Lean UIViewController: Only handles UI and bindings
final class LoginViewController: UIViewController {
    private let viewModel: LoginViewModel
    init(viewModel: LoginViewModel) { self.viewModel = viewModel; super.init(nibName: nil, bundle: nil) }
    required init?(coder: NSCoder) { fatalError() }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

12Hoe zou je problemen oplossen bij een mislukte iOS-archiefupload veroorzaakt door signing- of provisioningfouten?

Het oplossen van problemen bij een mislukte upload van een iOS-archief door signing- of provisioningproblemen omvat het controleren van drie hoofdgebieden: distributiecertificaten, provisioningprofielen en target-entitlements. 1. Diagnostische logs inspecteren: Bekijk de distributielogs in de Xcode Organizer, gedetailleerde validatiefouten bij de upload of consolelogs van CI (Continuous Integration, bijv. `altool`/`notarytool`) om de exacte reden van afwijzing te achterhalen. 2. Certificaat- en identiteitsverificatie: Controleer of er een geldig Apple Distribution-certificaat aanwezig is in de Keychain samen met de bijbehorende privésleutel, en dat dit niet is verlopen, ingetrokken of het tussenliggende Apple WWDR-certificaat (Worldwide Developer Relations) mist. 3. Overeenstemming van provisioningprofiel: Zorg ervoor dat het profiel dat wordt gebruikt voor export een App Store Distribution-profiel is dat exact overeenkomt met de Bundle Identifier. Als functionaliteiten zoals pushberichten of Associated Domains worden gebruikt, controleer dan of een expliciete App ID is geconfigureerd in plaats van een niet-ondersteunde wildcard. 4. Uitlijning van entitlements: Controleer op afwijkingen tussen het `.entitlements`-bestand van de target en de functionaliteiten die zijn ingeschakeld voor de App ID in het Apple Developer Portal. Als lokale entitlements rechten claimen die niet zijn geregistreerd in het profiel op het portal, mislukt de upload. 5. Signing-configuratie: Bij gebruik van automatische signing, controleer het geselecteerde Team en de Apple ID-inloggegevens in Xcode Settings. Bij handmatige signing (of CI-exportopties), controleer of `ExportOptions.plist` elke bundle-ID koppelt aan het juiste distributiecertificaat en profiel.

# Decode the provisioning profile embedded in the archive
security cms -D -i MyApp.xcarchive/Products/Applications/MyApp.app/embedded.mobileprovision > profile.plist

# Read entitlements embedded in the profile
/usr/libexec/PlistBuddy -c "Print :Entitlements" profile.plist

# Inspect signature and entitlements on the built binary
codesign -d --entitlements :- MyApp.xcarchive/Products/Applications/MyApp.app
Probeer deze vraag te beantwoorden met een AI-begeleider

13Hoe zou je kiezen tussen BGAppRefreshTask, BGProcessingTask en een achtergrond-URLSession voor een achtergrondsynchronisatiefunctie in productie?

Het kiezen van de juiste achtergrond-API hangt af van de taakduur, eventuele vereisten voor stroomvoorziening en netwerkverbinding, en de aard van het netwerkverkeer: 1. **BGAppRefreshTask**: Het meest geschikt voor korte, lichte inhoudsupdates (zoals het verversen van nieuwsfeeds, sociale tijdlijnen of gebruikersdashboards) die ongeveer 15–30 seconden duren. Het systeem plant deze in op basis van gebruikspatronen van de gebruiker, zodat nieuwe inhoud klaarstaat vlak voordat de gebruiker de app doorgaans opent. 2. **BGProcessingTask**: Ontworpen voor langlopende, niet-urgente, zware bewerkingen zoals data-indexering, database-opschoning/-migraties, training van ML-modellen (Machine Learning) of grote datasynchronisaties. Deze taak kan minuten duren en kan vereisen dat het apparaat oplaadt (`requiresExternalPower = true`) en verbonden is met wifi of een netwerk (`requiresNetworkConnectivity = true`), waarbij de uitvoering meestal 's nachts plaatsvindt. 3. **Achtergrond-URLSession (`URLSessionConfiguration.background`)**: De juiste keuze bij het overdragen van grote bestanden (het uploaden van foto's/video's of het downloaden van grote asset-bundels) die buiten het app-proces moeten doorgaan, zelfs als de app door het besturingssysteem is gepauzeerd of afgesloten. Hierbij wordt de netwerkoverdracht overgedragen aan de `nsurlsessiond`-daemon in plaats van dat er app-code actief in het geheugen blijft.

import BackgroundTasks

func scheduleDatabaseMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.db_cleanup")
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true // Run overnight on charger
    request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 3600)
    
    try? BGTaskScheduler.shared.submit(request)
}

func scheduleTimelineRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.feed_refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    try? BGTaskScheduler.shared.submit(request)
}
Probeer deze vraag te beantwoorden met een AI-begeleider

14Hoe bouw je een Combine-pipeline voor een zoektekstveld die invoer debouncet, duplicaten negeert, een netwerkverzoek uitvoert en de UI veilig bijwerkt?

Om een veilige zoekpipeline in Combine te bouwen: 1. **Debounce en ontdubbelen:** Neem de publisher van de zoekopdracht (bijvoorbeeld `$queryText`), pas `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` toe om te wachten op pauzes tijdens het typen, en gebruik `.removeDuplicates()` om ongewijzigde zoektermen te negeren. 2. **Netwerkverzoek en annulering:** Map elke zoekopdracht naar een netwerkverzoek-publisher en gebruik `.switchToLatest()` om deze plat te slaan. Dit annuleert lopende verzoeken automatisch wanneer er een nieuwe zoekopdracht binnenkomt, wat race conditions en verouderde resultaten voorkomt. 3. **Fouten isoleren:** Vang netwerkfouten op binnen de binnenste publisher (bijvoorbeeld `.catch { _ in Just([]) }`), zodat netwerkfouten de buitenste zoektekststroom niet beëindigen. 4. **Afhandeling op de hoofdthread:** Pas `.receive(on: DispatchQueue.main)` toe voordat de toestand wordt bijgewerkt of waarden aan UI-eigenschappen worden toegewezen.

import Combine
import Foundation

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var searchResults: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .map { query -> AnyPublisher<[String], Never> in
                guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.performSearch(query)
                    .catch { _ in Just([]) }
                    .eraseToAnyPublisher()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$searchResults)
    }
    
    private func performSearch(_ term: String) -> AnyPublisher<[String], Error> {
        let url = URL(string: "https://api.example.com/search?q=\(term)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

15Hoe zou je Swift async/await en Task-annulering gebruiken om een scherm te implementeren dat data ophaalt via het netwerk en voorkomt dat de UI (User Interface) wordt bijgewerkt nadat de gebruiker is weg genavigeerd?

Ik zou het ophalen via het netwerk definiëren als een `async`-functie op een service of viewmodel en deze uitvoeren vanuit een `Task` waarvan de levensduur is gekoppeld aan het scherm. In UIKit betekent dat meestal dat je zoiets als `var loadTask: Task<Void, Never>?` opslaat op de viewcontroller of het viewmodel, deze start wanneer het scherm moet laden en annuleert in `viewWillDisappear`, `deinit` of voordat er een nieuwere aanvraag wordt gestart, afhankelijk van de gewenste levensduur. In SwiftUI geef ik waar mogelijk de voorkeur aan `.task` of `.task(id:)`, omdat SwiftUI deze automatisch annuleert wanneer de view verdwijnt of het id verandert. Binnen de taak roep ik de asynchrone netwerk-API aan met `try await`, handel ik annuleringen afzonderlijk van echte fouten af en controleer ik op annulering voordat resultaten worden toegepast als er meerdere await-stappen of verwerkingen zijn. Wijzigingen in de UI-status moeten plaatsvinden op de main actor, door het viewmodel te annoteren met `@MainActor` of door `await MainActor.run` te gebruiken. Om verouderde UI-statussen te voorkomen, annuleer ik eerdere taken, vermijd ik losgekoppelde of globale taken (`Task.detached`) voor schermgebonden werk en vergelijk ik eventueel een request-id of het huidige item-id voordat het resultaat wordt doorgevoerd.

@MainActor
final class UsersViewController: UIViewController {
    private var loadTask: Task<Void, Never>?
    private let service: UserService

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUsers()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        loadTask?.cancel()
    }

    private func loadUsers() {
        loadTask?.cancel()
        loadingView.isHidden = false

        loadTask = Task { [weak self] in
            do {
                let users = try await self?.service.fetchUsers() ?? []
                try Task.checkCancellation()

                guard let self else { return }
                self.loadingView.isHidden = true
                self.tableModel = users
                self.tableView.reloadData()
            } catch is CancellationError {
                // User navigated away or a new load replaced this one; do not update UI.
            } catch {
                guard let self else { return }
                self.loadingView.isHidden = true
                self.showError(error)
            }
        }
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

16Hoe maak je een keuze tussen UserDefaults, Keychain, bestanden, SQLite en Core Data voor verschillende typen app-gegevens?

Het kiezen van het juiste iOS-opslagmechanisme hangt af van gegevensgevoeligheid, omvang, relationele complexiteit, queryvereisten en de back-uplevenscyclus: 1. **Keychain**: Voor gevoelige gegevens (authenticatietokens, wachtwoorden, encryptiesleutels, biometrische geheimen). Het biedt door hardware ondersteunde encryptie, blijft behouden na het opnieuw installeren van de app en blijft veilig, zelfs in gecompromitteerde sandbox-omgevingen. 2. **UserDefaults**: Voor lichte, niet-gevoelige voorkeuren en vlaggen (bijvoorbeeld `hasCompletedOnboarding`, themavoorkeur). Omdat dit volledig in het geheugen wordt geladen als een property list, mag het niet worden gebruikt voor grote datasets, media of gevoelige tokens. 3. **Bestandssysteem (Documents / Caches / Application Support)**: Voor losse bestanden en grote binaire blobs (gedownloade afbeeldingen, audio, pdf's). `Documents` is zichtbaar voor de gebruiker en wordt geback-upt naar iCloud; `Caches` is voor wisbare inhoud die opnieuw kan worden gedownload; `Application Support` is voor niet-gebruikersgerichte persistente bestanden. 4. **SQLite**: Voor gestructureerde, tabulaire gegevens die snelle geïndexeerde query's, bulkoperaties of crossplatform-database-uitwisseling vereisen zonder de overhead van een objectgraaf. 5. **Core Data / SwiftData**: Voor complexe objectgrafen met relaties, wijzigingsdetectie, 'faulting', 'undo'-beheer en directe integratie met UIKit (`NSFetchedResultsController`) of SwiftUI (`@Query`).

- Auth tokens / API keys          -> Keychain (Hardware-backed encrypted store)
- App settings / UI flags         -> UserDefaults (Lightweight key-value, non-sensitive)
- Downloaded videos / PDFs        -> File System (Documents / Caches directory)
- 50k items with complex queries   -> SQLite / Core Data / SwiftData (Indexed, relationship-aware)
Probeer deze vraag te beantwoorden met een AI-begeleider

17Hoe bepaal je in een UIKit-productiescherm met callbacks vanuit een viewmodel of je self vastlegt als weak, unowned of strong in closure-handlers?

Binnen een UIKit-architectuur waarin een view controller communiceert met een viewmodel via callbacks, wordt de capture-strategie bepaald door eigenaarschap en levensduur: 1. `[weak self]`: De standaard en veiligste aanpak voor opgeslagen of 'escaping' closures (zoals event-callbacks van het viewmodel of asynchrone netwerk-/data-completionhandlers). Omdat de view controller eigenaar is van het viewmodel, veroorzaakt het sterk vastleggen van `self` in een callback die door het viewmodel wordt opgeslagen een sterke referentiecyclus (`VC -> VM -> closure -> VC`). `[weak self]` zet `self` om in een optional (`UIViewController?`), waardoor `self` netjes kan worden vrijgegeven en geheugenlekken worden voorkomen. 2. `[unowned self]`: Gaat ervan uit dat `self` nooit nil zal zijn wanneer de closure wordt uitgevoerd. Dit moet uiterst voorzichtig worden gebruikt in UI-schermen. Als een asynchrone taak of callback wordt voltooid nadat de view controller is gesloten en gedealloceerd, leidt toegang tot `unowned self` tot een fatale runtime-crash. Daarom heeft `[weak self]` in productie-UIKit-callbacks vrijwel altijd de voorkeur boven `unowned`. 3. **Sterke capture (standaard, geen capture-lijst):** Geschikt wanneer de closure non-escaping is (bijv. standaard collectiebewerkingen zoals `map`/`filter`) of wanneer de closure van korte duur is en geen eigenaarschapscyclus terug naar `self` veroorzaakt.

final class ProfileViewController: UIViewController {
    private let viewModel: ProfileViewModel

    init(viewModel: ProfileViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // VM stores the callback -> capture weak self to break retain cycle
        viewModel.onDataUpdated = { [weak self] state in
            guard let self else { return }
            self.updateUI(with: state)
        }
    }

    private func updateUI(with state: ProfileState) { /* Update views */ }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

Vragen voor ervaren kandidaten

18Je stapt in bij een bestaande, omvangrijke iOS-app met honderden schermen, veel massive view controllers en trage releasecycli. Hoe zou je een strategie voor incrementele architectuurverbetering opzetten zonder de oplevering van nieuwe features stil te leggen?

Een effectieve incrementele migratiestrategie vermijdt volledige herschrijvingen en koppelt refactoring direct aan de doorlopende levering van functionaliteiten (volgens het Strangler Fig-patroon). Je begint met het vaststellen van een overeengekomen doelarchitectuur (zoals MVVM of VIPER met dependency injection en modulaire coordinators) en het opzetten van testbaselines (karakteriseringstests, snapshot- en unittests) rond bestaande code voordat je deze aanpast. De prioritering moet worden gestuurd door wijzigingsfrequentie en risico: herstructureer schermen waaraan teams actief werken of onderdelen met veel fouten, in plaats van stabiele verouderde code. Ontkoppel bij het refactoren van Massive View Controllers de bedrijfs- en presentatielogica naar specifieke ViewModels/Presenters en introduceer protocolgebaseerde adapters of coordinators om de legacy-UI te isoleren van nieuwe code. Bewaak ten slotte de ontwikkelsnelheid en kwaliteit via architectuurgovernance: bied 'golden sample'-referentie-implementaties aan, handhaaf scheidingen via CI-linters of pull-requestrichtlijnen, en reserveer continu capaciteit voor technische schuld (bijv. 15–20% van de capaciteit of het combineren van refactorings met gerelateerd featurewerk), terwijl je statistieken zoals bouwtijden, crashpercentages en testdekking nauwlettend volgt.

protocol ProfileDisplaying: AnyObject {
    func updateProfile(name: String, avatarUrl: URL?)
}

extension LegacyProfileViewController: ProfileDisplaying {
    func updateProfile(name: String, avatarUrl: URL?) {
        self.nameLabel.text = name
    }
}

final class ProfilePresenter {
    private weak var view: ProfileDisplaying?
    private let userUseCase: FetchUserUseCaseProtocol
    
    init(view: ProfileDisplaying, userUseCase: FetchUserUseCaseProtocol) {
        self.view = view
        self.userUseCase = userUseCase
    }
    
    func onViewLoaded() async {
        guard let user = try? await userUseCase.execute() else { return }
        await MainActor.run {
            view?.updateProfile(name: user.name, avatarUrl: user.avatarURL)
        }
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

19Je brengt een drukbezochte iOS-app uit met een databasemigratie en een API-wijziging aan de backend; hoe ontwerp je het uitrolplan om de impact op gebruikers tot een minimum te beperken?

Het ontwerpen van een uitrolplan voor een veelgebruikte iOS-app met databasemigraties en backend-API-wijzigingen vereist dat de implementatie wordt losgekoppeld van de functieactivatie. Dit komt door de beperkingen van het iOS-clientplatform: gebruikers werken asynchroon bij en geforceerde binaire rollbacks aan de clientkant zijn onmogelijk. 1. Backend-compatibiliteit: Pas een expand-and-contract-strategie toe. Implementeer API v2 naast API v1, zodat zowel oudere als bijgewerkte clientversies gelijktijdig blijven functioneren zonder 'breaking changes'. 2. Robuuste databasemigratie: Zorg ervoor dat lokale schemamigraties (zoals SQLite, Core Data of SwiftData) idempotent en niet-destructief zijn, en veilig overgangen over meerdere versies tegelijk verwerken (bijvoorbeeld direct migreren van versie N-3 naar N) zonder de hoofdthread te blokkeren of crash-loops bij het opstarten te veroorzaken. 3. Dynamische functie-inschakeling: Lever nieuwe clientfuncties 'dark' op achter servergestuurde feature-flags of remote configuration. Houd de feature-flag uitgeschakeld tijdens de initiële distributie. 4. Gefaseerde uitrol en monitoring: Distribueer de app via App Store Phased Release (een gefaseerde uitrol over 7 dagen). Houd telemetrie, crashpercentages, slagingspercentages van databasemigraties en API-foutpercentages continu in de gaten. Pauzeer bij afwijkingen de gefaseerde uitrol in App Store Connect en schakel de feature-flags uit zonder dat er een binaire noodrollback nodig is.

Phase 1 (Backend Expand): Deploy API v2 supporting both new and legacy payloads.
Phase 2 (Client Distribution & Migration): Release client v2.0 via App Store Phased Release (1% -> 100%). DB migrates safely on first launch. Feature flag remains OFF.
Phase 3 (Validation & Feature Enablement): Monitor crash rates & migration success. Incrementally ramp Remote Config flag (10% -> 50% -> 100%).
Phase 4 (Backend Contract): Once legacy app adoption drops below deprecation threshold, sunset API v1.
Probeer deze vraag te beantwoorden met een AI-begeleider

20Je ontwerpt offline-first-synchronisatie voor een notitie-app waarbij bewerkingen uiteindelijk tussen apparaten gesynchroniseerd moeten worden, maar iOS achtergrondtaken kan uitstellen of annuleren. Welke achtergronduitvoeringsarchitectuur kies je?

Een offline-first-synchronisatiearchitectuur beschouwt de lokale database (zoals SQLite, Core Data of SwiftData) als de directe bron van waarheid voor de UI-status, terwijl mutaties worden toegevoegd aan een persistente outbox-wachtrij op schijf zodat niet-verzonden bewerkingen behouden blijven bij procesbeëindiging. Om de opportunistische en niet-deterministische achtergronduitvoering van iOS te beheren, wordt het achtergrondwerk gestructureerd over meerdere lagen: 1. Outbox legen bij overgang naar de achtergrond: Geïnitieerd via `UIApplication.shared.beginBackgroundTask(expirationHandler:)` om lopende of snelle openstaande mutaties af te ronden. 2. Periodiek geplande synchronisatie: Geregistreerd bij `BGTaskScheduler` met `BGAppRefreshTask` voor lichte synchronisatie van metadata en delta's, en `BGProcessingTask` voor zwaardere synchronisaties. 3. Synchronisatie van grote bestanden: Buiten het hoofdproces afgehandeld via een achtergrond-`URLSessionConfiguration` met uploads en downloads van bestanden. 4. Door de server getriggerde synchronisatie: Opportunistische wake-ups via stille pushnotificaties (`content-available: 1`). Aangezien achtergrondtaken op elk moment kunnen worden onderbroken of opnieuw ingepland, moeten operaties door de client gegenereerde idempotentietoetsen (zoals UUID's) gebruiken om veilige herhalingen zonder dubbele data mogelijk te maken. Als de `expirationHandler` van een taak wordt aangeroepen, moet actieve verwerking onmiddellijk worden geannuleerd, moeten niet-doorgevoerde bewerkingen weer als openstaand worden gemarkeerd en moet `setTaskCompleted(success: false)` worden aangeroepen. Uiteindelijke consistentie wordt gewaarborgd met conflictoplossingsmechanismen zoals CRDT's (Conflict-free Replicated Data Types), versievectoren of last-write-wins met deletion tombstones, waardoor externe delta's geen niet-doorgevoerde lokale outbox-mutaties overschrijven.

import BackgroundTasks
import Foundation

final class NoteSyncCoordinator {
    static let shared = NoteSyncCoordinator()
    private let syncTaskID = "com.notesapp.sync.refresh"

    func registerSyncTask() {
        BGTaskScheduler.shared.register(forTaskWithIdentifier: syncTaskID, using: nil) { task in
            guard let refreshTask = task as? BGAppRefreshTask else { return }
            self.handleAppRefresh(task: refreshTask)
        }
    }

    func scheduleNextSync() {
        let request = BGAppRefreshTaskRequest(identifier: syncTaskID)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        try? BGTaskScheduler.shared.submit(request)
    }

    private func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleNextSync()
        let syncTask = Task {
            do {
                try await SyncEngine.shared.flushPersistentOutboxAndFetchDeltas()
                task.setTaskCompleted(success: true)
            } catch {
                task.setTaskCompleted(success: false)
            }
        }

        task.expirationHandler = {
            syncTask.cancel()
        }
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider