Dezember 21, 2025
18 Min. Lesezeit

Leitfaden für Vorstellungsgespräche: Senior Mobile Entwickler (iOS)

interview
career-advice
job-search
Leitfaden für Vorstellungsgespräche: Senior Mobile Entwickler (iOS)
MB

Milad Bonakdar

Autor

Meistern Sie die fortgeschrittene iOS-Entwicklung mit wichtigen Fragen für Vorstellungsgespräche, die Architekturmuster, Leistungsoptimierung, Parallelverarbeitung, Core Data und Systemdesign für erfahrene Entwickler abdecken.


Einführung

Von erfahrenen iOS-Entwicklern wird erwartet, dass sie robuste, skalierbare Anwendungen entwerfen und gleichzeitig hohe Codequalität und -leistung gewährleisten. Diese Rolle erfordert fundierte Kenntnisse der iOS-Frameworks, Entwurfsmuster, Speicherverwaltung und die Fähigkeit, fundierte architektonische Entscheidungen zu treffen.

Dieser umfassende Leitfaden behandelt wichtige Interviewfragen für erfahrene iOS-Entwickler, die fortgeschrittene Swift-Konzepte, Architekturmuster, Leistungsoptimierung, Concurrency und Systemdesign umfassen. Jede Frage enthält detaillierte Antworten, eine Seltenheitseinschätzung und Schwierigkeitsgrade.


Fortgeschrittene Swift- und Sprachfunktionen (6 Fragen)

1. Erklären Sie Swifts Speicherverwaltung und ARC (Automatic Reference Counting).

Antwort: ARC verwaltet den Speicher automatisch, indem es Referenzen auf Klasseninstanzen verfolgt und verwaltet.

  • Funktionsweise: Jede Klasseninstanz hat einen Referenzzähler. Wenn der Zähler Null erreicht, wird die Instanz freigegeben.
  • Starke Referenzen: Standard. Erhöht den Referenzzähler.
  • Schwache Referenzen: Erhöhen den Referenzzähler nicht. Werden automatisch nil, wenn die Instanz freigegeben wird.
  • Unowned Referenzen: Erhöhen den Referenzzähler nicht, gehen aber davon aus, dass die Instanz immer existiert.
  • Retain Cycles: Treten auf, wenn zwei Objekte starke Referenzen aufeinander halten, was die Freigabe verhindert.
class Person {
    var name: String
    var apartment: Apartment?
    
    init(name: String) { self.name = name }
    deinit { print("\(name) wird freigegeben") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // weak, um Retain Cycle zu verhindern
    
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) wird freigegeben") }
}

Seltenheit: Sehr häufig Schwierigkeit: Schwer


2. Was sind Generics in Swift und warum sind sie nützlich?

Antwort: Generics ermöglichen es Ihnen, flexible, wiederverwendbare Funktionen und Typen zu schreiben, die mit jedem Typ arbeiten können.

  • Vorteile: Code-Wiederverwendbarkeit, Typsicherheit, Leistung (kein Runtime-Overhead)
  • Type Constraints: Beschränken generische Typen auf bestimmte Protokolle oder Klassen
  • Associated Types: Werden in Protokollen verwendet, um Platzhaltertypen zu definieren
// Generische Funktion
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

// Generischer Typ
struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

// Type Constraint
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

Seltenheit: Häufig Schwierigkeit: Mittel


3. Erklären Sie den Unterschied zwischen escaping und non-escaping Closures.

Antwort:

  • Non-escaping (Standard): Die Closure wird ausgeführt, bevor die Funktion zurückkehrt. Der Compiler kann besser optimieren.
  • Escaping (@escaping): Die Closure überlebt die Funktion (wird in einer Eigenschaft gespeichert, asynchron aufgerufen). Muss self explizit erfassen.
class NetworkManager {
    var completionHandlers: [() -> Void] = []
    
    // Escaping Closure - wird für später gespeichert
    func fetchData(completion: @escaping (Data?) -> Void) {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            completion(data)  // Wird aufgerufen, nachdem die Funktion zurückgekehrt ist
        }.resume()
    }
    
    // Non-escaping Closure - wird sofort ausgeführt
    func processData(_ data: Data, transform: (Data) -> String) -> String {
        return transform(data)  // Wird aufgerufen, bevor die Funktion zurückkehrt
    }
}

Seltenheit: Häufig Schwierigkeit: Mittel


4. Was ist der Unterschied zwischen map, flatMap und compactMap?

Antwort: Dies sind Higher-Order-Funktionen zur Transformation von Sammlungen:

  • map: Transformiert jedes Element und gibt ein Array von Ergebnissen zurück
  • compactMap: Wie map, filtert aber nil-Werte heraus
  • flatMap: Glättet verschachtelte Arrays zu einem einzigen Array
let numbers = [1, 2, 3, 4, 5]

// map
let doubled = numbers.map { $0 * 2 }
// [2, 4, 6, 8, 10]

// compactMap - entfernt nil
let strings = ["1", "2", "three", "4"]
let validNumbers = strings.compactMap { Int($0) }
// [1, 2, 4]

// flatMap - glättet Arrays
let nested = [[1, 2], [3, 4], [5]]
let flattened = nested.flatMap { $0 }
// [1, 2, 3, 4, 5]

Seltenheit: Häufig Schwierigkeit: Einfach


5. Erklären Sie Property Wrappers in Swift.

Antwort: Property Wrappers fügen eine Trennschicht zwischen dem Code hinzu, der verwaltet, wie eine Eigenschaft gespeichert wird, und dem Code, der eine Eigenschaft definiert.

  • Integrierte Beispiele: @State, @Published, @AppStorage in SwiftUI
  • Benutzerdefinierte Wrapper: Definieren wiederverwendbare Eigenschaftenverhaltensweisen
@propertyWrapper
struct Capitalized {
    private var value: String = ""
    
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

var user = User()
user.firstName = "john"
print(user.firstName)  // "John"

// UserDefaults Wrapper
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct Settings {
    @UserDefault(key: "username", defaultValue: "Guest")
    static var username: String
}

Seltenheit: Mittel Schwierigkeit: Schwer


6. Was ist der Result-Typ und wie wird er verwendet?

Antwort: Result ist eine Enum, die entweder Erfolg oder Misserfolg darstellt und die Fehlerbehandlung expliziter macht.

  • Definition: enum Result<Success, Failure: Error>
  • Vorteile: Typsichere Fehlerbehandlung, klarere API-Verträge, besser als Throwing Functions für asynchronen Code
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        completion(.failure(.invalidURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data = data else {
            completion(.failure(.noData))
            return
        }
        
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(.decodingError))
        }
    }.resume()
}

// Verwendung
fetchUser(id: 1) { result in
    switch result {
    case .success(let user):
        print("User: \(user.name)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Seltenheit: Häufig Schwierigkeit: Mittel


Architekturmuster (5 Fragen)

7. Erklären Sie das MVVM-Muster (Model-View-ViewModel).

Antwort: MVVM trennt die UI-Logik von der Geschäftslogik, wodurch der Code testbarer und wartbarer wird.

Loading diagram...
  • Model: Daten und Geschäftslogik
  • View: UI (UIViewController, SwiftUI View)
  • ViewModel: Präsentationslogik, transformiert Modelldaten für die View
  • Vorteile: Testbar (ViewModel hat keine UI-Abhängigkeiten), wiederverwendbare ViewModels, klare Trennung der Verantwortlichkeiten
// Model
struct User {
    let id: Int
    let firstName: String
    let lastName: String
}

// ViewModel
class UserViewModel {
    private let user: User
    
    var fullName: String {
        "\(user.firstName) \(user.lastName)"
    }
    
    var displayText: String {
        "User #\(user.id): \(fullName)"
    }
    
    init(user: User) {
        self.user = user
    }
}

// View
class UserViewController: UIViewController {
    private let viewModel: UserViewModel
    private let label = UILabel()
    
    init(viewModel: UserViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = viewModel.displayText
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


8. Was ist das Coordinator-Muster und warum sollte man es verwenden?

Antwort: Das Coordinator-Muster trennt die Navigationslogik von den View Controllern.

  • Problem: Massive View Controller mit Navigationslogik, die mit UI-Logik vermischt ist
  • Lösung: Coordinators verwalten den Navigationsfluss
  • Vorteile: Wiederverwendbare View Controller, testbare Navigation, klarer App-Fluss
protocol Coordinator {
    var navigationController: UINavigationController { get }
    func start()
}

class AppCoordinator: Coordinator {
    let navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        showLogin()
    }
    
    func showLogin() {
        let loginVC = LoginViewController()
        loginVC.coordinator = self
        navigationController.pushViewController(loginVC, animated: true)
    }
    
    func showHome() {
        let homeVC = HomeViewController()
        homeVC.coordinator = self
        navigationController.pushViewController(homeVC, animated: true)
    }
}

Seltenheit: Mittel Schwierigkeit: Schwer


9. Erklären Sie Dependency Injection in iOS.

Antwort: Dependency Injection ist ein Entwurfsmuster, bei dem Abhängigkeiten einem Objekt bereitgestellt werden, anstatt intern erstellt zu werden.

  • Vorteile: Testbarkeit (Inject Mocks), Flexibilität, lose Kopplung
  • Typen:
    • Constructor Injection: Übergabe von Abhängigkeiten über den Initializer (am häufigsten)
    • Property Injection: Setzen von Abhängigkeiten nach der Initialisierung
    • Method Injection: Übergabe von Abhängigkeiten als Methodenparameter
// Protokoll für Abstraktion
protocol NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

// Konkrete Implementierung
class URLSessionNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // Echter Netzwerkaufruf
    }
}

// Mock für Tests
class MockNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        completion(.success(Data()))
    }
}

// ViewModel mit Dependency Injection
class UserViewModel {
    private let networkService: NetworkService
    
    // Constructor Injection
    init(networkService: NetworkService) {
        self.networkService = networkService
    }
    
    func loadUsers() {
        networkService.fetchData { result in
            // Ergebnis behandeln
        }
    }
}

// Verwendung
let viewModel = UserViewModel(networkService: URLSessionNetworkService())

// Testen
let testViewModel = UserViewModel(networkService: MockNetworkService())

Seltenheit: Häufig Schwierigkeit: Mittel


10. Was ist das Repository-Muster?

Antwort: Das Repository-Muster abstrahiert die Datenzugriffslogik und bietet eine saubere API für Datenoperationen.

  • Vorteile: Zentralisierte Datenlogik, einfaches Umschalten von Datenquellen (API, Datenbank, Cache), testbar
  • Implementierung: Das Repository koordiniert zwischen mehreren Datenquellen
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func saveUser(_ user: User) async throws
}

class UserRepositoryImpl: UserRepository {
    private let apiService: APIService
    private let cacheService: CacheService
    
    init(apiService: APIService, cacheService: CacheService) {
        self.apiService = apiService
        self.cacheService = cacheService
    }
    
    func getUser(id: Int) async throws -> User {
        // Zuerst Cache prüfen
        if let cachedUser = try? await cacheService.getUser(id: id) {
            return cachedUser
        }
        
        // Von API abrufen
        let user = try await apiService.fetchUser(id: id)
        
        // Cache aktualisieren
        try? await cacheService.saveUser(user)
        
        return user
    }
    
    func saveUser(_ user: User) async throws {
        try await apiService.updateUser(user)
        try await cacheService.saveUser(user)
    }
}

Seltenheit: Mittel Schwierigkeit: Mittel


11. Erklären Sie die Unterschiede zwischen MVC, MVP und MVVM.

Antwort:

  • MVC (Model-View-Controller):
    • Apples Standardmuster
    • Der Controller vermittelt zwischen Model und View
    • Problem: Massive View Controller
  • MVP (Model-View-Presenter):
    • Der Presenter verwaltet die gesamte UI-Logik
    • Die View ist passiv (zeigt nur Daten an)
    • Bessere Testbarkeit als MVC
  • MVVM (Model-View-ViewModel):
    • Das ViewModel stellt Datenströme bereit
    • Die View bindet an das ViewModel
    • Am besten für reaktive Programmierung (Combine, RxSwift)

Seltenheit: Häufig Schwierigkeit: Schwer


Leistung & Optimierung (5 Fragen)

12. Wie optimieren Sie die Leistung von Table Views und Collection Views?

Antwort: Mehrere Strategien verbessern die Scroll-Leistung:

  1. Cell Reuse: Verwenden Sie dequeueReusableCell richtig
  2. Vermeiden Sie aufwendige Operationen: Führen Sie keine teuren Berechnungen in cellForRowAt durch
  3. Bildoptimierung:
    • Ändern Sie die Größe von Bildern auf die Anzeigegröße
    • Verwenden Sie Hintergrundthreads für die Bildverarbeitung
    • Cachen Sie dekodierte Bilder
  4. Prefetching: Implementieren Sie UITableViewDataSourcePrefetching
  5. Height Caching: Cachen Sie berechnete Zellhöhen
  6. Vermeiden Sie Transparenz: Opake Views rendern schneller
class ImageTableViewCell: UITableViewCell {
    static let identifier = "ImageCell"
    private let customImageView = UIImageView()
    
    override func prepareForReuse() {
        super.prepareForReuse()
        customImageView.image = nil  // Altes Bild löschen
    }
}

// Im View Controller
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.identifier, for: indexPath) as! ImageTableViewCell
    
    // Bild asynchron laden
    DispatchQueue.global(qos: .userInitiated).async {
        let image = self.loadAndResizeImage(at: indexPath)
        DispatchQueue.main.async {
            cell.imageView?.image = image
        }
    }
    
    return cell
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


13. Erklären Sie Instruments und wie Sie es zur Leistungsprofilierung verwenden.

Antwort: Instruments ist das Tool zur Leistungsanalyse von Xcode.

  • Häufig verwendete Instruments:
    • Time Profiler: Identifiziert CPU-intensive Code
    • Allocations: Verfolgt Speicherzuweisungen und Lecks
    • Leaks: Erkennt Speicherlecks
    • Network: Überwacht die Netzwerkaktivität
    • Energy Log: Analysiert den Batterieverbrauch
  • Workflow:
    1. App profilieren (Cmd+I)
    2. Instrument auswählen
    3. Aufzeichnen und mit der App interagieren
    4. Aufrufbaum und Zeitachse analysieren
    5. Engpässe identifizieren

Seltenheit: Häufig Schwierigkeit: Mittel


14. Wie erkennen und beheben Sie Speicherlecks?

Antwort: Speicherlecks treten auf, wenn Objekte nicht freigegeben werden, wenn sie nicht mehr benötigt werden.

  • Häufige Ursachen:
    • Retain Cycles (starke Referenzzyklen)
    • Closures, die self stark erfassen
    • Delegates, die nicht als weak gekennzeichnet sind
  • Erkennung:
    • Instruments Leaks Tool
    • Memory Graph Debugger in Xcode
    • Beobachten Sie den steigenden Speicherverbrauch
  • Behebungen:
    • Verwenden Sie weak oder unowned für Delegates
    • Verwenden Sie [weak self] oder [unowned self] in Closures
    • Durchbrechen Sie Retain Cycles
class ViewController: UIViewController {
    var closure: (() -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // SCHLECHT - Retain Cycle
        closure = {
            self.view.backgroundColor = .red
        }
        
        // GUT - Weak Self
        closure = { [weak self] in
            self?.view.backgroundColor = .red
        }
        
        // GUT - Unowned Self (wenn Self immer existiert)
        closure = { [unowned self] in
            self.view.backgroundColor = .red
        }
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


15. Welche Techniken verwenden Sie zur Optimierung des App-Starts?

Antwort: Ein schnellerer App-Start verbessert die Benutzererfahrung:

  1. Lazy Loading: Initialisieren Sie Objekte nur bei Bedarf
  2. Reduzieren Sie das Laden von Dylibs: Minimieren Sie dynamische Bibliotheken
  3. Optimieren Sie application:didFinishLaunching:
    • Verschieben Sie nicht-kritische Arbeiten in den Hintergrund
    • Verschieben Sie die aufwendige Initialisierung
  4. Binärgröße: Kleinere Binärdateien werden schneller geladen
  5. Vermeiden Sie aufwendige Operationen: Blockieren Sie nicht den Main Thread
  6. Messen: Verwenden Sie die App Launch-Vorlage von Instruments
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // Kritisch: Window und Root View Controller einrichten
    setupWindow()
    
    // Nicht-kritische Arbeiten verschieben
    DispatchQueue.main.async {
        self.setupAnalytics()
        self.setupCrashReporting()
    }
    
    // Hintergrundarbeit
    DispatchQueue.global(qos: .background).async {
        self.preloadData()
    }
    
    return true
}

Seltenheit: Häufig Schwierigkeit: Mittel


16. Wie handhaben Sie das Caching und Laden von Bildern?

Antwort: Eine effiziente Bildverarbeitung ist entscheidend für die Leistung:

  • Strategien:
    • Memory Cache: Schneller Zugriff, begrenzte Größe
    • Disk Cache: Persistent, größere Kapazität
    • Download: Abrufen aus dem Netzwerk
  • Bibliotheken: SDWebImage, Kingfisher (verwalten das Caching automatisch)
  • Benutzerdefinierte Implementierung:
class ImageCache {
    static let shared = ImageCache()
    
    private let memoryCache = NSCache<NSString, UIImage>()
    private let fileManager = FileManager.default
    private let cacheDirectory: URL
    
    private init() {
        cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("ImageCache")
        try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
    }
    
    func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
        let key = url.absoluteString as NSString
        
        // Memory Cache prüfen
        if let cachedImage = memoryCache.object(forKey: key) {
            completion(cachedImage)
            return
        }
        
        // Disk Cache prüfen
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        if let diskImage = UIImage(contentsOfFile: fileURL.path) {
            memoryCache.setObject(diskImage, forKey: key)
            completion(diskImage)
            return
        }
        
        // Download
        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data, let image = UIImage(data: data) else {
                completion(nil)
                return
            }
            
            // In Caches speichern
            self.memoryCache.setObject(image, forKey: key)
            try? data.write(to: fileURL)
            
            DispatchQueue.main.async {
                completion(image)
            }
        }.resume()
    }
}

Seltenheit: Häufig Schwierigkeit: Schwer


Concurrency & Asynchrone Programmierung (4 Fragen)

17. Erklären Sie Async/Await in Swift.

Antwort: Swifts modernes Concurrency-Modell, das in Swift 5.5 eingeführt wurde.

  • Vorteile: Sauberere Syntax als Completion Handler, einfachere Fehlerbehandlung, vom Compiler erzwungene Thread-Sicherheit
  • Schlüsselwörter:
    • async: Markiert eine Funktion, die ausgesetzt werden kann
    • await: Markiert einen Aussetzungspunkt
    • Task: Erstellt einen neuen asynchronen Kontext
    • actor: Thread-sicherer Referenztyp
// Alter Weg mit Completion Handlern
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // Ergebnis behandeln
    }.resume()
}

// Neuer Weg mit Async/Await
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

// Verwendung
Task {
    do {
        let user = try await fetchUser(id: 1)
        print(user.name)
    } catch {
        print("Error: \(error)")
    }
}

// Parallele Ausführung
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
let users = try await [user1, user2]

Seltenheit: Sehr häufig Schwierigkeit: Schwer


18. Was sind Actors in Swift?

Antwort: Actors sind Referenztypen, die ihren veränderlichen Zustand vor Data Races schützen.

  • Thread-Sicherheit: Nur eine Task kann gleichzeitig auf den veränderlichen Zustand des Actors zugreifen
  • Automatische Synchronisierung: Der Compiler erzwingt sicheren Zugriff
  • Main Actor: Spezieller Actor für UI-Updates
actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// Verwendung
let account = BankAccount()

Task {
    await account.deposit(amount: 100)
    let balance = await account.getBalance()
    print("Balance: \(balance)")
}

// Main Actor für UI-Updates
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [String] = []
    
    func loadData() async {
        let newData = await fetchDataFromAPI()
        // Automatisch im Main Thread
        self.data = newData
    }
}

Seltenheit: Mittel Schwierigkeit: Schwer


19. Erklären Sie das Combine Framework.

Antwort: Combine ist Apples Framework für reaktive Programmierung.

  • Kernkonzepte:
    • Publisher: Sendet Werte im Laufe der Zeit
    • Subscriber: Empfängt Werte
    • Operator: Transformiert Werte
  • Vorteile: Deklarativ, zusammensetzbar, integrierte Operatoren
  • Anwendungsfälle: Netzwerk, Benutzer-Input-Verarbeitung, Datenbindung
import Combine

class UserViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var users: [User] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { query in
                self.searchUsers(query: query)
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] users in
                self?.users = users
            }
            .store(in: &cancellables)
    }
    
    func searchUsers(query: String) -> AnyPublisher<[User], Never> {
        // Publisher zurückgeben
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [User].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .eraseToAnyPublisher()
    }
}

Seltenheit: Häufig Schwierigkeit: Schwer


20. Was ist der Unterschied zwischen Serial und Concurrent Queues?

Antwort: Dispatch Queues führen Tasks entweder seriell oder gleichzeitig aus:

  • Serielle Queue: Führt jeweils eine Task in FIFO-Reihenfolge aus. Tasks warten, bis die vorherige Task abgeschlossen ist.
  • Gleichzeitige Queue: Führt mehrere Tasks gleichzeitig aus. Tasks beginnen in FIFO-Reihenfolge, können aber in beliebiger Reihenfolge abgeschlossen werden.
  • Main Queue: Spezielle serielle Queue für UI-Updates
// Serielle Queue
let serialQueue = DispatchQueue(label: "com.app.serial")
serialQueue.async { print("Task 1") }
serialQueue.async { print("Task 2") }
// Ausgabe: Task 1, Task 2 (immer in Reihenfolge)

// Gleichzeitige Queue
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)
concurrentQueue.async { print("Task A") }
concurrentQueue.async { print("Task B") }
// Ausgabe: Task A, Task B ODER Task B, Task A (Reihenfolge nicht garantiert)

// Barrier für Schreiboperationen
concurrentQueue.async(flags: .barrier) {
    // Exklusiver Zugriff - keine anderen Tasks werden gleichzeitig ausgeführt
    print("Schreiboperation")
}

Seltenheit: Häufig Schwierigkeit: Mittel


Core Data & Persistenz (3 Fragen)

21. Erklären Sie die Core Data-Architektur und ihre Hauptkomponenten.

Antwort: Core Data ist Apples Framework für Objektgraphen und Persistenz.

Loading diagram...
  • NSManagedObjectModel: Schemadefinition (Entitäten, Attribute, Beziehungen)
  • NSPersistentStoreCoordinator: Koordiniert zwischen Kontext und Store
  • NSManagedObjectContext: Arbeitsspeicher für Objekte (wie ein Notizblock)
  • NSPersistentStore: Tatsächlicher Speicher (SQLite, binär, im Speicher)

Seltenheit: Häufig Schwierigkeit: Mittel


22. Wie handhaben Sie Concurrency in Core Data?

Antwort: Core Data-Kontexte sind nicht Thread-sicher. Verwenden Sie die richtigen Concurrency-Muster:

  • Kontexttypen:
    • Main Queue Context: Für UI-Operationen
    • Private Queue Context: Für Hintergrundoperationen
  • Best Practices:
    • Übergeben Sie niemals Managed Objects zwischen Threads
    • Verwenden Sie perform oder performAndWait für Kontextoperationen
    • Übergeben Sie Objekt-IDs zwischen Kontexten
class CoreDataManager {
    let persistentContainer: NSPersistentContainer
    
    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
        persistentContainer.performBackgroundTask { context in
            block(context)
            
            if context.hasChanges {
                try? context.save()
            }
        }
    }
}

// Verwendung
coreDataManager.performBackgroundTask { context in
    let user = User(context: context)
    user.name = "John"
    // Kontext speichert automatisch
}

Seltenheit: Mittel Schwierigkeit: Schwer


23. Was ist NSFetchedResultsController und wann verwenden Sie es?

Antwort: NSFetchedResultsController verwaltet Core Data-Ergebnisse effizient für Table/Collection Views.

  • Vorteile:
    • Automatische Änderungsverfolgung
    • Speichereffizient (Batching)
    • Abschnittsverwaltung
    • Automatische UI-Updates
  • Anwendungsfall: Anzeigen von Core Data-Objekten in Table/Collection Views
class UsersViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    var fetchedResultsController: NSFetchedResultsController<User>!
    
    func setupFetchedResultsController() {
        let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        
        fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil
        )
        
        fetchedResultsController.delegate = self
        try? fetchedResultsController.performFetch()
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController.sections?[section].numberOfObjects ?? 0
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.reloadData()
    }
}

Seltenheit: Mittel Schwierigkeit: Mittel


Systemdesign & Best Practices (2 Fragen)

24. Wie würden Sie eine Offline-First Mobile App entwerfen?

Antwort: Offline-First Apps funktionieren ohne Internet und werden synchronisiert, wenn eine Verbindung besteht.

  • Architektur:
    • Lokale Datenbank als Source of Truth (Core Data, Realm)
    • Synchronisierungsschicht zur Abstimmung mit dem Server
    • Strategie zur Konfliktlösung
  • Strategien:
    • Optimistische UI: Zeigen Sie Änderungen sofort an, synchronisieren Sie im Hintergrund
    • Queue Operations: Speichern Sie fehlgeschlagene Anfragen, versuchen Sie es erneut, wenn Sie online sind
    • Timestamp/Version: Verfolgen Sie die Datenaktualität
  • Herausforderungen:
    • Konfliktlösung (Last-Write-Wins, Merge, Benutzerauswahl)
    • Datenkonsistenz
    • Speicherbegrenzungen

Seltenheit: Mittel Schwierigkeit: Schwer


25. Welche Strategien haben Sie für die App-Sicherheit in iOS?

Antwort: Mehrere Ebenen schützen die App und Benutzerdaten:

  1. Datenschutz:
    • Keychain für sensible Daten (Passwörter, Token)
    • Verschlüsseln Sie Daten im Ruhezustand
    • Verwenden Sie die Data Protection API
  2. Netzwerksicherheit:
    • Nur HTTPS (App Transport Security)
    • Certificate Pinning für kritische APIs
    • Validieren Sie SSL-Zertifikate
  3. Codesicherheit:
    • Obfuskierung für sensible Logik
    • Jailbreak-Erkennung
    • Keine fest codierten Geheimnisse
  4. Authentifizierung:
    • Biometrische Authentifizierung (Face ID, Touch ID)
    • Sichere Token-Speicherung
    • Token-Aktualisierungsmechanismus
  5. Input-Validierung:
Newsletter subscription

Wöchentliche Karrieretipps, die wirklich funktionieren

Erhalten Sie die neuesten Einblicke direkt in Ihr Postfach

Decorative doodle

Ihr nächstes Vorstellungsgespräch ist nur einen Lebenslauf entfernt

Erstellen Sie in wenigen Minuten einen professionellen, optimierten Lebenslauf. Keine Designkenntnisse erforderlich—nur bewährte Ergebnisse.

Meinen Lebenslauf erstellen

Diesen Beitrag teilen

Nutzen Sie Ihre 6 Sekunden Optimal

Recruiter scannen Lebensläufe durchschnittlich nur 6 bis 7 Sekunden. Unsere bewährten Vorlagen sind darauf ausgelegt, sofort Aufmerksamkeit zu erregen und sie weiterlesen zu lassen.