20 häufig gestellte iOS-Interviewfragen. iOS-Entwicklung ist ein gefragtes Feld für Apps mit Swift, SwiftUI und UIKit sowie Lifecycle-, Concurrency-, Netzwerk-, Storage-, Performance-, Test- und App-Store-Release-Aufgaben. Die Fragen decken verschiedene Niveaus ab, und du kannst mündliche Antworten in unserem Interview-Trainer üben.
1Welche Aufgaben und Verantwortlichkeiten sollte ein UIViewController in einer typischen iOS-MVC-Architektur (Model-View-Controller) haben?
Im klassischen iOS-MVC (Model-View-Controller) agiert der UIViewController als Controller, der zwischen View-Komponenten und Model-Daten vermittelt. Zu seinen Hauptaufgaben gehören die Verwaltung des View-Lebenszyklus (z. B. viewDidLoad, viewWillAppear), das Einrichten und Aktualisieren von UI-Elementen, das Behandeln direkter Benutzerinteraktionen (wie Button-Taps, Delegates und Target-Actions) sowie das Orchestrieren von Bildschirmübergängen und -präsentationen. Aufgaben, die nicht die Benutzeroberfläche betreffen – wie direkte Netzwerkaufrufe, Persistenz und komplexe Geschäftslogik –, sollten an dedizierte Service- oder Model-Objekte delegiert werden, um zu verhindern, dass der View-Controller zu einem Massive View Controller anwächst.
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
}
}
2Welche Rolle spielt ein Provisioning Profile beim Release einer iOS-App?
Bei einem iOS-Release fungiert ein Provisioning Profile als kryptografisch signiertes Paket, das die Sicherheitsanforderungen von Apple mit dem App-Binary und den Zielgeräten verbindet. Seine primäre Aufgabe besteht darin, dem iOS-Betriebssystem und App Store Connect mitzuteilen, dass die Anwendung unter bestimmten Distributionsregeln ausgeführt werden darf. Ein Provisioning Profile bündelt mehrere wesentliche Komponenten: die App-ID (Bundle Identifier), das oder die autorisierten Signaturzertifikate, die bestätigen, wer die App erstellt hat, die der App gewährten Entitlements und Capabilities (wie Push Notifications, Sign in with Apple oder iCloud) sowie – bei Profilen außerhalb des App Stores wie Development oder Ad-Hoc – eine Liste erlaubter Geräte-UDIDs. Bei Release-Profilen für den App Store werden individuelle Geräte-UDIDs weggelassen, da Apple die Verteilung an jedes Endgerät über den App Store gestattet.
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)
3Was bedeutet es für eine iOS-App, Aufgaben im Hintergrund auszuführen, und welche grundlegenden Einschränkungen legt das System für diese Arbeit fest?
Die Ausführung von Aufgaben im Hintergrund bedeutet unter iOS, dass Code ausgeführt wird, während die App gerade nicht aktiv auf dem Bildschirm oder für den Benutzer sichtbar ist. Wenn ein Benutzer eine App verlässt, wechselt sie vom aktiven Vordergrundzustand in den Hintergrund und kurz darauf in einen angehaltenen Zustand (Suspended State), in dem ihre Ausführung pausiert ist, ihr Speicher jedoch im RAM verbleibt. Um die Akkulaufzeit, die Reaktionsfähigkeit des Systems und die Geräteressourcen zu schonen, kontrolliert iOS die Hintergrundausführung streng. Das System begrenzt die Dauer, für die eine App nach dem Wechsel in den Hintergrund Code ausführen darf (typischerweise beschränkt auf ein kurzes Zeitfenster von etwa 30 Sekunden, sofern keine speziellen Hintergrundmodi oder geplanten Aufgaben genutzt werden), drosselt die CPU- und Netzwerkpriorität und kann angehaltene Apps beenden, wenn das Gerät unter Speicherdruck gerät.
4Was ist ein Publisher in Combine, und wie unterscheidet er sich von einem Subscriber in einem typischen iOS-Datenfluss?
In Combine emittiert ein Publisher einen Datenstrom von Werten über die Zeit hinweg und kann mit einem Abschlussereignis (entweder erfolgreich abgeschlossen oder mit einem Fehler fehlgeschlagen) enden. Er deklariert zwei assoziierte Typen: `Output` (der Typ der Daten, die er erzeugt) und `Failure` (der Typ des Fehlers, den er ausgeben kann). Ein Subscriber empfängt Werte und Lebenszyklusereignisse von einem Publisher. In einem typischen iOS-Datenfluss fungieren Publisher als Quelle oder Produzent asynchroner Ereignisse (wie Netzwerkantworten, Benachrichtigungen oder Benutzereingaben), während Subscriber als Konsumenten agieren, die auf ausgegebene Werte reagieren, Fehler behandeln und auf den Abschluss reagieren (wie etwa das Aktualisieren des UI-Zustands oder das Schreiben in eine Datenbank). Der Subscriber verbindet sich mit einem Publisher, erhält ein Subscription-Token und fordert Elemente nach Bedarf (Demand) an.
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)")
}
)
5Was bedeutet async/await in Swift, und wie nutzt man es, um eine asynchrone API (Application Programming Interface) aus einer iOS-App aufzurufen?
`async/await` ist die integrierte Syntax in Swift, um asynchronen Code linear, lesbar und sequenziell zu schreiben, anstatt verschachtelte Closures und Completion-Handler zu verwenden. Das Kennzeichnen einer Funktion mit `async` teilt dem Compiler mit, dass die Funktion ihre Ausführung unterbrechen (suspendieren) kann, während sie auf den Abschluss länger dauernder Aufgaben (wie Netzwerkanfragen oder Datei-I/O) wartet. Das Schlüsselwort `await` markiert einen Unterbrechungspunkt: Die Ausführung der aktuellen Funktion pausiert, wodurch der zugrunde liegende Thread für andere Aufgaben freigegeben wird, und wird fortgesetzt, sobald das erwartete Ergebnis oder ein Fehler bereitsteht. Um eine asynchrone API in einer iOS-App aufzurufen, wird die `async`-Methode mit vorangestelltem `await` (bzw. `try await`, falls die Funktion Fehler wirft) innerhalb eines asynchronen Kontexts aufgerufen, beispielsweise innerhalb einer anderen `async`-Funktion oder eines `Task`-Blocks.
6Wann sollte man UserDefaults in einer iOS-App verwenden, und welche Arten von Daten sollten dort nicht gespeichert werden?
UserDefaults ist für das Speichern kleiner, leichtgewichtiger Benutzereinstellungen, Konfigurationen und Flags über App-Starts hinweg konzipiert (beispielsweise Theme-Einstellungen, Lautstärkepegel oder ein `hasSeenOnboarding`-Boolean-Flag). Es unterstützt Property-List-Typen wie String, Int, Double, Bool, Date, Data, Array und Dictionary. Folgendes sollte NICHT gespeichert werden: 1. Sensible Informationen (wie Benutzerpasswörter, private Schlüssel oder Authentifizierungs-Token), da UserDefaults Daten unverschlüsselt in einer Plist-Klartextdatei ablegt. 2. Große Datensätze, Dokumente oder Mediendateien (wie Bilder, Audioaufnahmen oder große JSON-Payloads), da beim Zugriff die gesamte Plist in den Speicher geladen wird, was zu hohem Speicherbedarf führt und den App-Start verlangsamt.
// 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"
7Was ist ARC (Automatic Reference Counting) in Swift und wie verwaltet es die Lebensdauer von Klasseninstanzen in einer iOS-App?
ARC (Automatic Reference Counting) ist Swifts Mechanismus zur Speicherverwaltung zur Kompilierzeit, um die Lebensdauer von Klasseninstanzen (Referenztypen) zu verfolgen und zu verwalten. Jedes Mal, wenn eine neue Instanz einer Klasse erstellt und einer starken Referenz zugewiesen wird, verfolgt ARC einen internen Referenzzähler für diese Instanz. Solange mindestens eine starke Referenz auf eine Instanz existiert, behält ARC die Instanz im Speicher. Wenn alle starken Referenzen entfernt werden und der Referenzzähler auf null fällt, gibt ARC die Instanz sofort frei, um Speicher freizugeben, und ruft unmittelbar vor der Freigabe automatisch die `deinit`-Methode der Instanz auf. Im Gegensatz zur Garbage Collection zur Laufzeit in Umgebungen wie Java oder .NET führt ARC keine periodischen Bereinigungszyklen durch; der Swift-Compiler fügt zur Kompilierzeit die entsprechenden Retain- und Release-Aufrufe direkt in die Binärdatei ein.
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
8Wie führt man eine einfache HTTP (Hypertext Transfer Protocol)-GET-Anfrage mit URLSession in einer iOS-App aus?
Um eine einfache HTTP-GET-Anfrage in einer iOS-App durchzuführen, erstellt man ein `URL`-Objekt und übergibt es an eine `URLSession`-Instanz, beispielsweise `URLSession.shared`. Die Anfrage kann entweder über einen Data-Task mit Completion-Handler (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) oder über moderne Swift-Concurrency (`let (data, response) = try await URLSession.shared.data(from: url)`) ausgeführt werden. Bei der herkömmlichen Data-Task-Methode muss `.resume()` auf dem Task aufgerufen werden, um ihn zu starten. Zudem müssen Übertragungsfehler behandelt, der HTTP-Statuscode in `HTTPURLResponse` geprüft und alle UI-Aktualisierungen explizit auf den Main-Thread weitergeleitet werden.
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()
9Was ist Instruments in Xcode und wie verwendet man es, um ein einfaches Performance-Problem in einer iOS-App zu untersuchen?
Instruments ist Apples Profiling- und Analysewerkzeug zur Leistungsmessung, das in Xcode integriert ist. Es ermöglicht Entwicklern, das Laufzeitverhalten, die CPU-Auslastung, Speicherallokationen, Speicherlecks (Memory Leaks) und die Rendering-Performance der Benutzeroberfläche zu überwachen. Um ein einfaches Performance-Problem (wie UI-Ruckler oder hohe CPU-Last) zu untersuchen, startet man Instruments aus Xcode heraus (über Product -> Profile), wählt eine passende Vorlage (wie Time Profiler für CPU-Engpässe oder Allocations/Leaks für Speicherprobleme) und zeichnet einen Trace auf, während man den problematischen Benutzerablauf auf einem Gerät reproduziert. Nach der Aufzeichnung analysiert man den Call-Tree und die schwersten Stack-Traces, um genau die Methoden zu identifizieren, die Verzögerungen oder übermäßigen Ressourcenverbrauch verursachen, sodass der Engpass vor Codeänderungen gemessen und lokalisiert werden kann.
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.
10Was ist der APNs (Apple Push Notification service) und welche Rolle spielt er bei der Zustellung von Push-Benachrichtigungen an eine iOS-App?
Der APNs (Apple Push Notification service) ist ein cloudbasierter Vermittlungsdienst von Apple, der Push-Benachrichtigungen sicher von Ihren Backend-Servern (Provider-Servern) an Apple-Geräte weiterleitet. Da Mobilgeräte nicht zu jedem einzelnen App-Backend eine dauerhaft offene Socket-Verbindung aufrechterhalten können, ohne den Akku zu belasten und übermäßig Bandbreite zu verbrauchen, unterhält Apple eine einzige persistente, energiesparende Verbindung zwischen jedem Gerät und dem APNs. Wenn sich Ihre App für Remote-Benachrichtigungen registriert, generiert der APNs ein eindeutiges, opakes Device-Token, das diese spezifische App-Installation auf diesem konkreten Gerät identifiziert. Die App sendet dieses Token an Ihren Backend-Provider-Server. Möchte der Provider den Benutzer benachrichtigen, erstellt er ein Payload und sendet dieses zusammen mit dem Device-Token über HTTP/2 an den APNs. Der APNs ermittelt daraufhin die aktive Verbindung für das betreffende Gerät und stellt das Push-Benachrichtigungs-Payload direkt an iOS zu, welches den Hinweis anzeigt oder die App entsprechend der Konfiguration aufweckt.
11Wie würden Sie einen umfangreichen UIViewController refaktorieren, der UI-Aktualisierungen, Validierung, Netzwerkkommunikation und Navigation verarbeitet?
Das Refactoring eines Massive View Controllers (MVC) sollte schrittweise erfolgen, um Risiken zu minimieren und unterschiedliche Verantwortlichkeiten in dedizierte Schichten aufzuteilen:
1. **Netzwerk & Daten auslagern:** Verschieben Sie API-Aufrufe, Datenpersistenz und JSON-Parsing aus dem View Controller in dedizierte Service- oder Repository-Klassen mit Protokollabstraktionen.
2. **Präsentationszustand & Validierung auslagern:** Führen Sie ein ViewModel (oder einen Presenter) ein. Verlagern Sie Eingabevalidierung, String-Formatierung und das UI-Zustandsmanagement in das ViewModel, wodurch diese Logik isoliert per Unit-Test überprüfbar wird.
3. **Navigation auslagern:** Nutzen Sie das Coordinator-Muster (oder Router / Flow Controller), um `push`- und `present`-Aufrufe sowie die Navigationsfluss-Logik aus dem View Controller zu entfernen.
4. **Den View Controller schlank halten:** Behalten Sie nur UI-Layout, Subview-Konfiguration, Lifecycle-Hooks und die Bindung von UI-Steuerelementen an das ViewModel im View Controller.
5. **Schrittweise Umsetzung mit Tests:** Erstellen Sie während des Refactorings Unit-Tests für neu extrahierte Services und ViewModels, um sicherzustellen, dass das bestehende Verhalten unverändert bleibt.
// 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() }
}
12Wie würden Sie einen Fehler beim Hochladen eines iOS-Archivs beheben, der durch Signierungs- oder Provisioning-Fehler verursacht wurde?
Die Fehlerbehebung bei einem Upload-Fehler eines iOS-Archivs aufgrund von Signierungs- oder Provisioning-Problemen umfasst die Überprüfung dreier Hauptbereiche: Distributionszertifikate, Provisioning-Profile und Target-Entitlements.
1. **Diagnoseprotokolle prüfen:** Überprüfen Sie die Distributionsprotokolle des Xcode Organizers, detaillierte Validierungsfehler beim Upload oder CI-Konsolenprotokolle (`altool`/`notarytool`), um den genauen Ablehnungsgrund zu ermitteln.
2. **Zertifikats- & Identitätsüberprüfung:** Stellen Sie sicher, dass ein gültiges Apple-Distributionszertifikat zusammen mit dem passenden privaten Schlüssel im Schlüsselbund (Keychain) vorhanden ist und dass es weder abgelaufen noch widerrufen ist und das Apple WWDR-Zwischenzertifikat nicht fehlt.
3. **Abgleich des Provisioning-Profils:** Vergewissern Sie sich, dass das für den Export verwendete Profil ein App Store Distribution Profile ist, das exakt mit dem Bundle Identifier übereinstimmt. Falls Capabilities wie Push Notifications oder Associated Domains genutzt werden, stellen Sie sicher, dass eine explizite App-ID statt einer inkompatiblen Wildcard-ID konfiguriert ist.
4. **Abgleich der Entitlements:** Prüfen Sie auf Diskrepanzen zwischen der `.entitlements`-Datei des Targets und den für die App-ID im Apple Developer Portal aktivierten Capabilities. Wenn lokale Entitlements Berechtigungen anfordern, die im Portal-Profil nicht hinterlegt sind, schlägt der Upload fehl.
5. **Signierungskonfiguration:** Wenn Sie automatische Signierung nutzen, überprüfen Sie das ausgewählte Team und die Apple-ID-Zugangsdaten in den Xcode-Einstellungen. Bei manueller Signierung (oder CI-Exportoptionen) stellen Sie sicher, dass `ExportOptions.plist` jede Bundle-ID dem korrekten Distributionszertifikat und Profil zuordnet.
# 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
13Wie würden Sie bei einem Hintergrundsynchronisations-Feature für die Produktion zwischen BGAppRefreshTask, BGProcessingTask und einer Hintergrund-URLSession (background URLSession) wählen?
Die Wahl der richtigen Hintergrund-API hängt von der Aufgabendauer, erforderlichen Strom-/Netzwerkbedingungen und der Art der Nutzdaten ab: 1. **BGAppRefreshTask**: Optimal für kurze, leichtgewichtige Inhaltsaktualisierungen (wie das Aktualisieren von News-Feeds, Social-Media-Timelines oder Dashboards), die etwa 15–30 Sekunden dauern. Das System plant diese Aufgaben basierend auf den Nutzungsgewohnheiten des Nutzers ein, damit frische Daten bereitstehen, kurz bevor die App üblicherweise geöffnet wird. 2. **BGProcessingTask**: Ausgelegt für langlebige, unkritische und ressourcenintensive Vorgänge wie Datenindizierung, Datenbankbereinigung/-migrationen, Training von ML-Modellen oder umfangreiche Datensynchronisationen. Diese Aufgaben können mehrere Minuten laufen und erfordern optional eine Stromverbindung (`requiresExternalPower = true`) sowie Netzwerkzugriff (`requiresNetworkConnectivity = true`), was typischerweise über Nacht ausgeführt wird. 3. **Hintergrund-URLSession (`URLSessionConfiguration.background`)**: Die richtige Wahl für die Übertragung großer Dateien (Upload von Fotos/Videos oder Download großer Asset-Pakete), die out-of-process fortgesetzt werden müssen, selbst wenn die App durch das Betriebssystem suspendiert oder beendet wird. Der Netzwerktransfer wird an den Daemon `nsurlsessiond` delegiert, anstatt App-Code im Speicher aktiv zu halten.
14Wie würden Sie eine Combine-Pipeline für ein Suchtextfeld aufbauen, die Eingaben debounced (verzögert entprellt), Duplikate ignoriert, eine Netzwerkanfrage ausführt und die UI (User Interface) sicher aktualisiert?
Um eine sichere Such-Pipeline in Combine aufzubauen: 1. **Debounce & Deduplizierung:** Den Publisher für die Suchanfrage (z. B. `$queryText`) nehmen, `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` anwenden, um Schreibpausen abzuwarten, und `.removeDuplicates()` nutzen, um unveränderte Suchbegriffe zu ignorieren. 2. **Netzwerkanfrage & Stornierung:** Jede Suchanfrage auf einen Publisher für Netzwerkaufrufe mappen und mit `.switchToLatest()` vereinfachen. Dadurch werden laufende Anfragen automatisch abgebrochen, sobald eine neue Suchanfrage eintrifft, was Race Conditions und veraltete Ergebnisse verhindert. 3. **Fehler isolieren:** Netzwerkfehler innerhalb des inneren Publishers abfangen (z. B. `.catch { _ in Just([]) }`), damit Netzwerkfehler nicht den übergeordneten Stream des Suchtextes beenden. 4. **Auslieferung auf dem Main Thread:** `.receive(on: DispatchQueue.main)` anwenden, bevor der State aktualisiert oder UI-Eigenschaften Werte zugewiesen werden.
15Wie würden Sie `async`/`await` und `Task`-Abbrüche in Swift verwenden, um einen Screen zu implementieren, der Daten über das Netzwerk lädt und verhindert, dass die UI (User Interface) aktualisiert wird, nachdem die Ansicht verlassen wurde?
Ich würde den Netzwerkladevorgang als `async`-Funktion in einem Service oder ViewModel implementieren und ihn über einen `Task` starten, dessen Lebensdauer an den Screen gebunden ist. In UIKit bedeutet das in der Regel, eine Referenz wie `var loadTask: Task<Void, Never>?` im ViewController oder ViewModel zu halten, diesen Task beim Laden des Screens zu starten und ihn in `viewWillDisappear`, `deinit` oder vor dem Start einer neueren Anfrage abzubrechen. In SwiftUI empfiehlt sich `.task` oder `.task(id:)`, da SwiftUI den Task automatisch abbricht, sobald die View verschwindet oder sich die ID ändert.
Innerhalb des Tasks wird die asynchrone Netzwerk-API mit `try await` aufgerufen. Abbrüche sollten getrennt von echten Fehlern behandelt und bei mehreren `await`-Aufrufen oder Verarbeitungsschritten vor dem Anwenden der Ergebnisse geprüft werden. Änderungen am UI-Zustand müssen auf dem Main Actor erfolgen – entweder indem das ViewModel mit `@MainActor` annotiert ist oder über `await MainActor.run`.
Um veraltete UI-Zustände zu vermeiden, sollten vorherige Tasks abgebrochen, von der View losgelöste globale Tasks für viewspezifische Arbeit vermieden und optional vor dem Zuweisen des Ergebnisses eine Request-ID oder die aktuelle Element-ID abgeglichen werden.
@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)
}
}
}
}
16Wie würden Sie bei unterschiedlichen Arten von App-Daten zwischen UserDefaults, Keychain, Dateien, SQLite und Core Data wählen?
Die Wahl des passenden iOS-Speichermechanismus hängt von Datensensibilität, Datenmenge, relationaler Komplexität, Abfrageanforderungen und dem Backup-Lebenszyklus ab: 1. **Keychain**: Für sensible Daten (Authentifizierungs-Tokens, Passwörter, Verschlüsselungsschlüssel, biometrische Secrets). Bietet hardwaregestützte Verschlüsselung, bleibt über Neuinstallationen der App hinweg erhalten und bleibt selbst in kompromittierten Sandbox-Umgebungen geschützt. 2. **UserDefaults**: Für einfache, unkritische Einstellungen und Flags (z. B. `hasCompletedOnboarding`, Theme-Auswahl). Da Daten vollständig als Property List in den Arbeitsspeicher geladen werden, sollte es nicht für große Datensätze, Mediendateien oder sensible Tokens verwendet werden. 3. **Dateisystem (Documents / Caches / Application Support)**: Für eigenständige Dateien und große Binärdaten (heruntergeladene Bilder, Audio, PDFs). `Documents` ist nutzerorientiert und wird in iCloud gesichert; `Caches` dient für löschbare, wiederherunterladbare Inhalte; `Application Support` ist für persistente Dateien gedacht, die nicht direkt für den Nutzer sichtbar sind. 4. **SQLite**: Für strukturierte, tabellarische Daten mit Bedarf an schnellen indizierten Abfragen, Massenoperationen oder plattformübergreifender Datenbanknutzung ohne den Overhead eines Objektgraphen. 5. **Core Data / SwiftData**: Für komplexe Objektgraphen mit Relationen, Änderungsverfolgung, Faulting, Undo-Management und nativer Anbindung an UIKit (`NSFetchedResultsController`) oder SwiftUI (`@Query`).
17Wie entscheiden Sie in einem produktiven UIKit-Screen mit Callbacks aus einem ViewModel, ob self in Closure-Handlern als weak, unowned oder strong erfasst werden soll?
In einer UIKit-Architektur, in der ein View Controller über Callbacks mit einem ViewModel interagiert, richtet sich die Erfassungsstrategie nach Eigentümerschaft und Lebensdauer:
1. `[weak self]`: Der Standard und sicherste Ansatz für gespeicherte oder escapende Closures (wie ViewModel-Event-Callbacks oder asynchrone Netzwerk-/Daten-Completion-Handler). Da der View Controller das ViewModel besitzt, erzeugt eine starke Erfassung von `self` in einem vom ViewModel gespeicherten Callback einen starken Referenzzyklus (`VC -> VM -> Closure -> VC`). `[weak self]` macht `self` zu einem Optional (`UIViewController?`), sodass `self` ordnungsgemäß deallokiert werden kann und Speicherlecks verhindert werden.
2. `[unowned self]`: Setzt voraus, dass `self` zum Zeitpunkt der Ausführung der Closure niemals `nil` ist. Dies sollte in UI-Screens mit äußerster Vorsicht verwendet werden. Wenn ein asynchroner Task oder Callback abgeschlossen wird, nachdem der View Controller bereits geschlossen und freigegeben wurde, führt der Zugriff auf `unowned self` zu einem fatalen Laufzeitabsturz. Aus diesem Grund ist `[weak self]` in produktiven UIKit-UI-Callbacks gegenüber `unowned` im Regelfall vorzuziehen.
3. **Starke Erfassung (Standard, keine Capture List):** Geeignet, wenn die Closure nicht-escapend ist (z. B. bei Standard-Collection-Operationen wie `map`/`filter`) oder wenn die Closure kurzlebig ist und keinen Eigentumszyklus zurück zu `self` erzeugt.
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 */ }
}
18Sie übernehmen die Verantwortung für eine gewachsene iOS-App mit hunderten Screens, vielen Massive View Controllern und langsamen Release-Zyklen. Wie würden Sie eine Strategie zur schrittweisen Architekturverbesserung planen, ohne die Bereitstellung neuer Features zu stoppen?
Eine effektive inkrementelle Migrationsstrategie vermeidet vollständige Neuentwicklungen und koppelt das Refactoring direkt an die laufende Bereitstellung von Features (nach dem Strangler-Fig-Muster).
Zunächst wird eine abgestimmte Zielarchitektur definiert (wie MVVM oder VIPER mit Dependency Injection und modularen Coordinators) und vor Änderungen am Altsystem eine Testbasis (Charakterisierungs-, Snapshot- und Unit-Tests) etabliert. Die Priorisierung sollte sich an Feature-Änderungsfrequenz (Churn) und Risiko orientieren: Es werden primär Screens refaktoriert, an denen Teams aktiv arbeiten oder die hohe Fehlerraten aufweisen, anstatt stabiler Legacy-Code.
Beim Refactoring von Massive View Controllern werden Geschäfts- und Präsentationslogik in dedizierte ViewModels bzw. Presenter ausgelagert sowie protokollbasierte Adapter oder Coordinators eingeführt, um bestehende UI-Komponenten vom neuen Code zu isolieren.
Schließlich sichert architektonische Governance Entwicklungsgeschwindigkeit und Qualität: durch Referenzimplementierungen (Golden Samples), Durchsetzung von Modulgrenzen über CI-Linter oder Pull-Request-Richtlinien sowie die kontinuierliche Bereitstellung fester Kapazitäten für technische Schulden (z. B. 15–20 % der Kapazität oder Kopplung an zugehörige Feature-Tickets), begleitet vom Monitoring relevanter Metriken wie Build-Zeiten, Crash-Raten und Testabdeckung.
19Sie veröffentlichen eine stark frequentierte iOS-App mit einer Datenbankmigration und einer Änderung der Backend-API (Application Programming Interface). Wie würden Sie den Rollout-Plan gestalten, um Beeinträchtigungen für die Nutzer zu minimieren?
Die Gestaltung eines Rollout-Plans für eine stark frequentierte iOS-App mit Datenbankmigrationen und Backend-API-Änderungen erfordert die Entkopplung von Deployment und Feature-Aktivierung. Grund dafür sind iOS-spezifische Client-Einschränkungen wie asynchrone Nutzer-Updates und die Unmöglichkeit erzwungener clientseitiger Binär-Rollbacks.
1. Backend-Kompatibilität: Implementieren Sie eine Expand-and-Contract-Strategie. Stellen Sie API v2 parallel zu API v1 bereit, sodass sowohl alte als auch aktualisierte Client-Versionen gleichzeitig ohne Breaking Changes funktionieren.
2. Ausfallsichere Datenbankmigration: Stellen Sie sicher, dass lokale Schemamigrationen (z. B. mit SQLite, Core Data oder SwiftData) idempotent und zerstörungsfrei sind sowie Versionssprünge (z. B. ein Upgrade von N-3 auf N) sicher verarbeiten, ohne den Main-Thread zu blockieren oder Absturzschleifen beim App-Start zu verursachen.
3. Dynamisches Gating: Liefern Sie neue Client-Funktionen zunächst inaktiv („dark“) hinter serverseitigen Feature-Flags bzw. Remote-Konfigurationen aus. Lassen Sie das Feature-Flag während der initialen Verteilung deaktiviert.
4. Phasenweiser Rollout und Monitoring: Verteilen Sie die App über den phasenweisen Release im App Store (gestaffelter 7-Tage-Rollout). Überwachen Sie kontinuierlich Telemetriedaten, Absturzraten, Erfolgsmetriken der Datenbankmigration und API-Fehlerraten. Treten Auffälligkeiten auf, pausieren Sie den phasenweisen Rollout in App Store Connect und deaktivieren Sie die Feature-Flags, ohne dass ein Notfall-Binär-Rollback erforderlich ist.
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.
20Sie entwerfen eine Offline-First-Synchronisation für eine Notizen-App, bei der Bearbeitungen letztendlich über mehrere Geräte hinweg synchronisiert werden müssen, iOS Hintergrundaufgaben jedoch verzögern oder abbrechen kann. Welche Architektur für die Hintergrundausführung würden Sie wählen?
Eine Offline-First-Synchronisationsarchitektur betrachtet die lokale Datenbank (wie SQLite, Core Data oder SwiftData) als unmittelbare Single Source of Truth für den UI-Zustand, während Mutationen an eine persistente Outbox-Queue auf der Festplatte angehängt werden, damit ungesendete Bearbeitungen Prozessabbrüche überstehen. Um die opportunistische und nicht-deterministische Hintergrundausführung von iOS zu handhaben, wird die Hintergrundarbeit mehrstufig strukturiert:
1. Outbox-Leerung beim Wechsel in den Hintergrund: Initiiert über `UIApplication.shared.beginBackgroundTask(expirationHandler:)`, um laufende oder kurze anstehende Mutationen abzuschließen.
2. Geplante periodische Synchronisation: Registriert beim `BGTaskScheduler` unter Verwendung von `BGAppRefreshTask` für leichtgewichtige Metadaten-/Delta-Synchronisationen und `BGProcessingTask` für aufwendigere Synchronisationsvorgänge.
3. Synchronisation großer Dateien: Auslagerung aus dem Hauptprozess über eine Hintergrund-`URLSessionConfiguration` für Datei-Uploads und -Downloads.
4. Server-getriggerte Synchronisation: Opportunistisches Aufwecken über Silent Push Notifications (`content-available: 1`).
Da Hintergrundaufgaben jederzeit unterbrochen oder neu terminiert werden können, müssen Operationen clientseitig erzeugte Idempotenzschlüssel (z. B. UUIDs) verwenden, um sichere Wiederholungsversuche ohne Datenverdopplung zu ermöglichen. Wird der `expirationHandler` einer Aufgabe aufgerufen, muss die laufende Arbeit sofort abgebrochen werden, unbestätigte Operationen müssen wieder als ausstehend markiert werden und `setTaskCompleted(success: false)` muss aufgerufen werden. Eventual Consistency wird durch Konfliktlösungsmechanismen wie CRDTs, Versionsvektoren oder Last-Write-Wins mit Deletion-Tombstones gewährleistet, sodass Remote-Deltas unbestätigte lokale Outbox-Mutationen nicht überschreiben.
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()
}
}
}