20 ofte stillede iOS-interviewspørgsmål. iOS-udvikling er et efterspurgt felt med fokus på Swift-, SwiftUI- og UIKit-apps samt lifecycle-, concurrency-, netværks-, storage-, performance-, test- og App Store-releaseopgaver. Spørgsmålene dækker flere niveauer, og du kan øve dig i at svare mundtligt i vores interviewtræner.
1Hvilke ansvarsområder bør en UIViewController have i en typisk iOS MVC-arkitektur (Model-View-Controller)?
I klassisk iOS MVC (Model-View-Controller) fungerer en UIViewController som controlleren, der formidler mellem View-komponenter og Model-data. Dens primære ansvarsområder omfatter styring af visningens livscyklus (f.eks. viewDidLoad og viewWillAppear), opsætning og opdatering af brugergrænsefladeelementer, håndtering af direkte brugerinteraktioner (såsom knaptryk, delegates og target-actions) samt orkestrering af overgange eller præsentationer af nye skærmbilleder. Ansvarsområder, der ikke vedrører brugergrænsefladen — såsom rå netværkskald, persistens og tung forretningslogik — bør delegeres til dedikerede service- eller modelobjekter for at forhindre, at controlleren udvikler sig til en 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
}
}
2Hvilken rolle spiller en provisioning profile i forbindelse med udgivelse af en iOS-app?
I en iOS-udgivelse fungerer en provisioning profile som en kryptografisk signeret pakke, der forbinder Apples sikkerhedskrav med app-binærfilen og målenhederne. Dens primære rolle er at fortælle iOS-operativsystemet og App Store Connect, at applikationen er autoriseret til at køre under specifikke distributionsregler. En provisioning profile samler flere kritiske elementer: App ID (Bundle Identifier), de autoriserede signeringscertifikater, som bekræfter, hvem der har bygget appen, de entitlements og rettigheder, der er tildelt appen (såsom Push Notifications, Sign in with Apple eller iCloud), og — i profiler, der ikke er til App Store, såsom Development eller Ad-Hoc — en liste over tilladte enheders UDID'er (Unique Device Identifier). For App Store-udgivelsesprofiler udelades individuelle enheds-UDID'er, fordi Apple tillader distribution til enhver forbrugerenhed via 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)
3Hvad indebærer det for en iOS-app at udføre arbejde i baggrunden, og hvilke grundlæggende begrænsninger sætter systemet for dette arbejde?
At udføre arbejde i baggrunden på iOS betyder at eksekvere kode, når appen ikke er aktiv på skærmen eller synlig for brugeren. Når en bruger forlader en app, overgår den fra den aktive forgrundstilstand til baggrunden og kort efter til en suspenderet tilstand, hvor dens eksekvering pauses, selvom dens hukommelse forbliver i RAM. For at spare på batteriet, bevare systemets responstid og beskytte enhedens ressourcer styrer iOS baggrundseksekvering stramt. Systemet begrænser, hvor længe en app må eksekvere kode, når den er i baggrunden (typisk begrænset til et kort tidsvindue på cirka 30 sekunder, medmindre der anvendes specifikke baggrundstilstande eller planlagte opgaver), drosler CPU- og netværksprioritet og kan terminere suspenderede apps, hvis enheden oplever hukommelsesmangel.
4Hvad er en Combine-publisher, og hvordan adskiller den sig fra en subscriber i et typisk iOS-dataflow?
I Combine udsender en Publisher en strøm af værdier over tid og kan afsluttes med en fuldførelsesbegivenhed (enten ved at fuldføre fejlfrit eller ved at fejle med en fejl). Den erklærer to tilknyttede typer: `Output` (typen af data, den producerer) og `Failure` (typen af fejl, den kan udsende). En Subscriber modtager værdier og livscyklusbegivenheder fra en publisher. I et typisk iOS-dataflow fungerer publishers som kilden eller producenten af asynkrone hændelser (såsom netværkssvar, notifikationer eller brugerhandlinger), mens subscribers fungerer som forbrugere, der reagerer på udsendte værdier, håndterer fejl og reagerer på fuldførelse (såsom opdatering af UI-tilstand eller skrivning til en database). En subscriber knytter sig til en publisher, modtager et abonnementstoken og anmoder om elementer efter behov.
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)")
}
)
5Hvad betyder async/await i Swift, og hvordan vil du bruge det til at kalde et asynkront API (Application Programming Interface) fra en iOS-app?
`async/await` er Swifts indbyggede syntaks til at skrive asynkron kode på en lineær, letlæselig og sekventiel måde frem for at anvende indlejrede closures og completion handlers. Når man markerer en funktion med `async`, fortæller det compileren, at funktionen kan suspendere sin udførelse, mens den venter på langvarigt arbejde (såsom netværksanmodninger eller fil-I/O) på at blive færdigt. Nøgleordet `await` angiver et suspensionspunkt: Udførelsen af den aktuelle funktion pauses, hvilket frigør den underliggende tråd til at udføre andet arbejde, og genoptages, når det afventede resultat eller en fejl er klar. For at kalde et asynkront API i en iOS-app kalder man `async`-metoden med foranstillet `await` (og `try await`, hvis funktionen kan kaste fejl) inde fra en asynkron kontekst, f.eks. i en anden `async`-funktion eller i en `Task`-blok.
6Hvornår vil du bruge UserDefaults i en iOS-app, og hvilke typer data bør ikke gemmes der?
UserDefaults er designet til at gemme små, lette brugerpræferencer, indstillinger og flag på tværs af app-starter (for eksempel temapræferencer, lydstyrke eller et `hasSeenOnboarding` boolsk flag). Det understøtter property list-typer såsom String, Int, Double, Bool, Date, Data, Array og Dictionary. Du bør IKKE gemme: 1. Følsomme oplysninger (såsom brugeradgangskoder, private nøgler eller godkendelsestokens), fordi UserDefaults gemmer data ukrypteret i en almindelig plist-tekstfil. 2. Store datasæt, dokumenter eller mediefiler (såsom billeder, lyd eller store JSON-payloads), fordi hele plist-filen indlæses i hukommelsen, når der tilgås data, hvilket medfører et højt hukommelsesforbrug og gør app-opstart langsommere.
// 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"
7Hvad er ARC (Automatic Reference Counting) i Swift, og hvordan styrer det levetiden for klasseinstanser i en iOS-app?
ARC (Automatic Reference Counting) er Swifts hukommelsesstyringsmekanisme på kompileringstidspunktet til at spore og håndtere levetiden for klasseinstanser (referencetyper). Hver gang en ny instans af en klasse oprettes og tildeles en stærk reference (strong reference), sporer ARC et internt referencetal for den pågældende instans. Så længe der findes mindst én stærk reference til en instans, holder ARC instansen i hukommelsen. Når alle stærke referencer fjernes, og referencetallet falder til nul, frigiver ARC straks instansen for at frigøre hukommelse, og instansens `deinit`-metode kaldes automatisk lige før frigivelsen. I modsætning til runtime garbage collection i miljøer som Java eller .NET kører ARC ikke periodiske sweep-cyklusser; Swift-kompileren indsætter passende retain- og release-kald direkte i den binære kode på kompileringstidspunktet.
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
8Hvordan udfører du en simpel HTTP (Hypertext Transfer Protocol) GET-forespørgsel med URLSession i en iOS-app?
For at udføre en simpel HTTP GET-forespørgsel i en iOS-app opretter du et URL-objekt og sender det til en URLSession-instans, såsom URLSession.shared. Du kan udføre forespørgslen ved hjælp af en completion handler på en data task (URLSession.shared.dataTask(with: url) { data, response, error in ... }) eller ved brug af moderne Swift concurrency (let (data, response) = try await URLSession.shared.data(from: url)). Når du anvender den traditionelle data task-tilgang, skal du kalde .resume() på opgaven for at starte den, håndtere eventuelle transportfejl, undersøge statuskoden i HTTPURLResponse og sikre, at eventuelle UI-opdateringer dirigeres til hovedtråden.
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()
9Hvad er Instruments i Xcode, og hvordan vil du bruge det til at undersøge et simpelt ydeevneproblem i en iOS-app?
Instruments er Apples værktøj til profilering og analyse af ydeevne, som følger med Xcode. Det giver udviklere mulighed for at overvåge runtime-adfærd, CPU-forbrug, hukommelsesallokeringer, hukommelseslækager og UI-rendering. For at undersøge et simpelt ydeevneproblem (såsom hakken i brugergrænsefladen eller højt CPU-forbrug) starter du Instruments fra Xcode (via Product -> Profile), vælger en passende skabelon (såsom Time Profiler til CPU-flaskehalse eller Allocations/Leaks til hukommelsesproblemer) og optager et trace, mens du genskaber det problematiske brugerflow på en enhed. Efter optagelsen analyserer du kaldstræet (call tree) og de tungeste stakspor for at lokalisere de præcise metoder, der forårsager forsinkelser eller overdrevent ressourceforbrug, så du kan måle og identificere flaskehalsen, før du ændrer koden.
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.
10Hvad er APNs (Apple Push Notification service), og hvilken rolle spiller det ved levering af push-notifikationer til en iOS-app?
APNs (Apple Push Notification service) er Apples cloud-baserede mellemliggende tjeneste, som sikkert videredirigerer push-notifikationer fra dine backend-servere (provider servers) til Apple-enheder. Da mobile enheder ikke kan opretholde kontinuerlige, åbne socket-forbindelser til hver enkelt app-backend uden at dræne batteriet og forbruge unødig netværksbåndbredde, opretholder Apple en enkelt vedvarende lavenergiforbindelse mellem hver enhed og APNs. Når din app registrerer sig til fjernnotifikationer, genererer APNs et unikt, uigennemsigtigt (opaque) enhedstoken, der identificerer den specifikke appinstallation på den pågældende enhed. Appen sender dette token til din backend-provider-server. Når udbyderen vil notificere brugeren, opbygger den en payload og sender den sammen med enhedstokenet til APNs over HTTP/2. APNs finder derefter den aktive forbindelse for den pågældende enhed og leverer push-notifikations-payloadet direkte til iOS, som viser advarslen eller vækker appen som konfigureret.
11Hvordan vil du refakturere en stor UIViewController, der håndterer UI-opdateringer (User Interface), validering, netværkskald og navigation?
Refakturering af en Massive View Controller i MVC-arkitekturen (Model-View-Controller) bør udføres trinvist for at reducere risici, samtidig med at særskilte ansvarsområder adskilles i dedikerede lag: 1. Udtræk netværk og data: Flyt API-kald (Application Programming Interface), datapersistering og JSON-parsing (JavaScript Object Notation) ud af view controlleren til dedikerede Service- eller Repository-klasser med protokolabstraktioner. 2. Udtræk præsentationstilstand og validering: Introducér en ViewModel (eller Presenter). Flyt inputvalidering, strengformatering og styring af UI-tilstand over i ViewModel, så denne logik kan enhedstestes isoleret. 3. Udtræk navigation: Tag Coordinator-mønstret (eller Router / Flow Controller) i brug for at fjerne push/present-kald og navigationsflow-logik fra view controlleren. 4. Hold view controlleren enkel og slank: Bevar kun UI-layout, underordnede view-konfigurationer, livscykluskroge og binding af UI-kontroller til ViewModel. 5. Trinvis udførelse med tests: Tilføj enhedstests for nyudtrukne services og ViewModels under refaktureringen for at sikre, at den eksisterende adfærd forbliver uændret.
// 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() }
}
12Hvordan vil du fejlfinde en mislykket upload af et iOS-arkiv, der skyldes signerings- eller provisioning-fejl?
Fejlfinding af en mislykket upload af et iOS-arkiv forårsaget af signerings- eller provisioning-problemer indebærer gennemgang af tre hovedområder: distributionscertifikater, provisioning-profiler og target entitlements. 1. Inspicér diagnostiske logfiler: Gennemgå distributionslogfiler i Xcode Organizer, detaljerede valideringsfejl ved upload eller CI-konsollogfiler (Continuous Integration) (`altool`/`notarytool`) for at identificere den præcise årsag til afvisningen. 2. Verifikation af certifikat og identitet: Bekræft, at et gyldigt Apple Distribution-certifikat findes i Keychain sammen med dets tilhørende private nøgle, og at det ikke er udløbet, tilbagekaldt eller mangler Apple WWDR-mellemcertifikatet (Worldwide Developer Relations). 3. Match af provisioning-profil: Sørg for, at profilen, der anvendes til eksport, er en App Store Distribution-profil, som matcher det nøjagtige Bundle Identifier. Hvis funktioner som Push Notifications eller Associated Domains benyttes, skal du sikre, at der er konfigureret et eksplicit App ID i stedet for et inkompatibelt wildcard. 4. Overensstemmelse af entitlements: Tjek for uoverensstemmelser mellem målets `.entitlements`-fil og de funktioner, der er aktiveret for dit App ID i Apple Developer Portal. Hvis lokale rettigheder kræver tilladelser, der ikke er registreret på portalprofilen, vil uploaden fejle. 5. Signeringskonfiguration: Hvis automatisk signering anvendes, skal du bekræfte det valgte Team og Apple ID-brugeroplysninger i Xcode Settings. Hvis manuel signering (eller CI-eksportindstillinger) benyttes, skal du sikre, at `ExportOptions.plist` mapper hvert bundle-ID til det korrekte distributionscertifikat og den rette profil.
# 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
13Hvordan vælger du mellem BGAppRefreshTask, BGProcessingTask og URLSession i baggrunden til en produktionsklar baggrundssynkroniseringsfunktion?
Valget af den rette baggrunds-API afhænger af opgavens varighed, om der stilles krav til strøm/netværk, og typen af netværksdata: 1. **BGAppRefreshTask**: Bedst til korte, lette indholdsopdateringer (såsom opdatering af nyhedsfeeds, sociale tidslinjer eller brugerdashboards), der tager omtrent 15–30 sekunder. Systemet planlægger disse baseret på brugerens anvendelsesmønstre, så nyt indhold er klar, lige før brugeren typisk åbner appen. 2. **BGProcessingTask**: Designet til langvarige, ikke-hastende, tunge operationer såsom dataindeksering, oprydning/migreringer i databasen, træning af ML-modeller (Machine Learning) eller store datasynkroniseringer. Den kan køre i flere minutter og kan kræve, at enheden oplader (`requiresExternalPower = true`) og er forbundet til Wi-Fi/netværk (`requiresNetworkConnectivity = true`), og den udføres typisk natten over. 3. **Background URLSession (`URLSessionConfiguration.background`)**: Det rigtige valg ved overførsel af store filer (upload af fotos/videoer eller download af store asset-pakker), som skal fortsætte uden for app-processen, selvom appen suspenderes eller afsluttes af styresystemet. Den uddelegerer netværksoverførslen til `nsurlsessiond`-dæmonen i stedet for at holde app-kode aktiv i hukommelsen.
14Hvordan opbygger du en Combine-pipeline til et søgetekstfelt, der debouncer input, ignorerer dubletter, udfører en netværksforespørgsel og opdaterer UI (User Interface) sikkert?
For at opbygge en sikker søge-pipeline i Combine: 1. **Debounce og deduplikering:** Tag søgeforespørgslens publisher (f.eks. `$queryText`), anvend `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` for at afvente pauser i indtastningen, og `.removeDuplicates()` for at ignorere uændrede søgestrenge. 2. **Netværksforespørgsel og annullering:** Knyt (map) hver forespørgsel til en publisher for netværksforespørgsler, og fladgør med `.switchToLatest()`. Dette annullerer automatisk igangværende forespørgsler, når der modtages en ny søgeforespørgsel, hvilket forhindrer race conditions og forældede resultater. 3. **Isoler fejl:** Fang netværksfejl i den indre publisher (f.eks. `.catch { _ in Just([]) }`), så netværksfejl ikke afbryder den ydre søgetekststrøm. 4. **Levering på hovedtråden:** Anvend `.receive(on: DispatchQueue.main)` inden opdatering af tilstand eller tildeling til UI-egenskaber.
15Hvordan ville du bruge Swift async/await og annullering af `Task` til at implementere en skærm, der indlæser data fra netværket og undgår at opdatere UI (User Interface), efter brugeren har navigeret væk?
Jeg ville gøre netværksindlæsningen til en `async`-funktion på en service eller view model og køre den fra en `Task`, hvis levetid er bundet til skærmen. I UIKit betyder det typisk, at man gemmer noget i retning af `var loadTask: Task<Void, Never>?` på view controlleren eller view modellen, starter den, når skærmen skal indlæses, og annullerer den i `viewWillDisappear`, `deinit` eller før start af en nyere forespørgsel afhængigt af den ønskede levetid. I SwiftUI ville jeg foretrække `.task` eller `.task(id:)`, når det er muligt, fordi SwiftUI automatisk annullerer den, når visningen forsvinder, eller id'et ændres. Inde i opgaven ville jeg kalde det asynkrone netværks-API med `try await`, håndtere annullering adskilt fra reelle fejl og kontrollere for annullering før anvendelse af resultaterne, hvis der er flere `await`-kald eller behandlingstrin. Ændringer i UI-tilstand skal ske på main actor, enten ved at markere view modellen med `@MainActor` eller ved at bruge `await MainActor.run`. For at undgå forældet UI ville jeg annullere tidligere opgaver, undgå detached/globale opgaver til skærmbundet arbejde og eventuelt sammenligne et forespørgsels-id eller det aktuelle element-id, før resultatet anvendes.
@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)
}
}
}
}
16Hvordan ville du vælge mellem UserDefaults, Keychain, filer, SQLite og Core Data til forskellige typer app-data?
Valget af den rette lagringsmekanisme i iOS afhænger af datafølsomhed, størrelse, relationel kompleksitet, forespørgselskrav og backup-livscyklus: 1. **Keychain**: Til følsomme data (godkendelsestokens, adgangskoder, krypteringsnøgler, biometriske hemmeligheder). Den tilbyder hardware-understøttet kryptering, bevares på tværs af geninstallationer af appen og forbliver sikker selv i kompromitterede sandbox-miljøer. 2. **UserDefaults**: Til letvægts-, ikke-følsomme præferencer og flag (f.eks. `hasCompletedOnboarding`, temapræference). Da den indlæses fuldstændigt i hukommelsen som en property list, bør den ikke bruges til store datasæt, medier eller følsomme tokens. 3. **Filsystem (Documents / Caches / Application Support)**: Til separate filer og store binære dataobjekter (downloadede billeder, lyd, PDF'er). `Documents` er brugerrettet og sikkerhedskopieres til iCloud; `Caches` er til sletbart indhold, der kan downloades igen; `Application Support` er til permanente filer, der ikke er direkte brugerrettede. 4. **SQLite**: Til strukturerede tabeldata, der kræver hurtige indekserede forespørgsler, massekørsler eller databasedeling på tværs af platforme uden overhead fra en objektgraf. 5. **Core Data / SwiftData**: Til komplekse objektgrafer med relationer, ændringssporing, faulting, fortrydelseshåndtering og direkte integration med UIKit (`NSFetchedResultsController`) eller SwiftUI (`@Query`).
17I en UIKit-produktionsskærm med callbacks fra en view model, hvordan beslutter du så, om self skal captures som weak, unowned eller strong i closure-handlers?
I en UIKit-arkitektur, hvor en view controller interagerer med en view model via callbacks, bestemmes capture-strategien af ejerskab og levetid: 1. `[weak self]`: Den gængse og sikreste tilgang til gemte eller escaping closures (såsom hændelses-callbacks fra view models eller asynkrone netværks-/data-completion-handlers). Da view controlleren ejer view modellen, vil en stærk reference til `self` i et callback, der gemmes af view modellen, skabe en stærk referencecyklus (`VC -> VM -> closure -> VC`). `[weak self]` omdanner `self` til en optional (`UIViewController?`), hvilket tillader `self` at blive deallokeret korrekt og forhindrer hukommelseslækager. 2. `[unowned self]`: Forudsætter, at `self` aldrig er nil, når closuren afvikles. Det bør anvendes med stor forsigtighed i UI-skærme. Hvis en asynkron opgave eller et callback fuldføres, efter at view controlleren er blevet lukket og deallokeret, vil tilgang til `unowned self` forårsage et fatalt runtime-nedbrud (crash). Af denne grund foretrækkes `[weak self]` generelt frem for `unowned` i callbacks i UIKit-produktionskode. 3. Stærk capture (standard, ingen capture list): Passende, når closuren er non-escaping (f.eks. standard samlingsoperationer som `map`/`filter`), eller når closuren er kortlivet og ikke skaber en referencecyklus tilbage til `self`.
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 */ }
}
18Du starter på en moden iOS-app med hundredvis af skærme, mange massive view controllers og langsomme udgivelsescyklusser. Hvordan ville du planlægge en strategi for trinvise arkitekturforbedringer uden at standse leveringen af nye funktioner?
En effektiv trinvis migreringsstrategi undgår totale omskrivninger og kobler refaktorisering direkte sammen med den løbende levering af funktioner (efter Strangler Fig-mønsteret). Du starter med at definere en aftalt målarkitektur (såsom MVVM eller VIPER med dependency injection og modulære coordinators) og etablere en baseline for tests (karakteriseringstests, snapshot-tests og enhedstests) omkring den eksisterende kode (legacy-kode), før den ændres. Prioriteringen bør drives af ændringshyppighed (churn) og risiko: refaktorisér skærme, som teams arbejder aktivt på, eller områder med en høj fejlrate, frem for stabil legacy-kode. Når Massive View Controllers refaktoriseres, afkobles forretnings- og præsentationslogik til dedikerede ViewModels/Presenters, og der introduceres protokolbaserede adaptere eller coordinators til at isolere det ældre UI fra den nye kode. Slutteligt opretholdes udviklingshastighed og kvalitet via arkitektonisk styring: sørg for referenceimplementeringer (golden samples), håndhæv grænser via linters i CI (Continuous Integration) eller retningslinjer for PR (Pull Requests), og allokér løbende kapacitet til teknisk gæld (f.eks. 15–20 % kapacitet eller ved at kombinere refaktorisering med relateret funktionsudvikling), samtidig med at metrikker som byggetider, crash-rater og testdækning følges nøje.
19Du skal udgive en iOS-app med høj trafik, som indebærer en databasemigrering og en ændring i backendens API (Application Programming Interface); hvordan vil du designe udrulningsplanen for at minimere påvirkningen af brugerne?
At designe en udrulningsplan for en iOS-app med høj trafik, der involverer databasemigreringer og backend-API-ændringer, kræver, at deployering adskilles fra funktionsaktivering på grund af begrænsninger på iOS-klienten (asynkrone brugeropdateringer og manglende mulighed for at gennemtvinge tilbagerulning af klientbinærer). 1. Backend-kompatibilitet: Implementer en expand-and-contract-strategi. Deployer API v2 sideløbende med API v1, så både ældre og opdaterede klientversioner fungerer samtidigt uden 'breaking changes'. 2. Robust databasemigrering: Sørg for, at lokale skemamigreringer (f.eks. SQLite, Core Data, SwiftData) er idempotente, ikke-destruktive og sikkert håndterer spring over flere versioner (f.eks. opgradering fra N-3 til N) uden at blokere hovedtråden eller forårsage nedbrudsløkker ved opstart. 3. Dynamisk styring: Udgiv de nye klientfunktioner skjult bag serverstyrede feature flags/fjernkonfiguration. Hold feature flaget deaktiveret under den indledende distribution. 4. Trinvis udrulning og overvågning: Distribuer appen via App Store Phased Release (en 7-dages trinvis udrulning). Overvåg løbende telemetri, nedbrudsfrekvenser, succesrater for databasemigrering og API-fejlfrekvenser. Hvis der opstår uregelmæssigheder, sættes den trinvise udrulning på pause i App Store Connect, og feature flags slås fra uden behov for en akut tilbagerulning af binæren.
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.
20Du designer offline-first-synkronisering til en note-app, hvor redigeringer i sidste ende skal synkroniseres på tværs af enheder, men hvor iOS kan udskyde eller annullere baggrundsarbejde. Hvilken arkitektur til baggrundseksekvering vil du vælge?
En offline-first-synkroniseringsarkitektur behandler den lokale database (såsom SQLite, Core Data eller SwiftData) som den umiddelbare autoritative sandhedskilde for UI-tilstanden, mens ændringer (mutations) tilføjes til en persistent udbakke-kø (outbox) på disken, så usendte redigeringer overlever processens afslutning. For at håndtere iOS' opportunistiske og ikke-deterministiske baggrundseksekvering struktureres baggrundsarbejdet i flere niveauer: 1. Udbakke-tømning ved overgang til baggrunden: Startes via `UIApplication.shared.beginBackgroundTask(expirationHandler:)` for at færdiggøre igangværende eller hurtige afventende ændringer. 2. Planlagt periodisk synkronisering: Registreres hos `BGTaskScheduler` ved hjælp af `BGAppRefreshTask` til letvægts synkronisering af metadata/ændringer og `BGProcessingTask` til tungere synkroniseringsoperationer. 3. Synkronisering af store aktiver: Håndteres uden for processen via en baggrunds-`URLSessionConfiguration` ved hjælp af fil-uploads/downloads. 4. Serverudløst synkronisering: Opportunistiske opvågninger via 'silent push'-notifikationer (`content-available: 1`). Da baggrundsopgaver kan afbrydes eller omplanlægges når som helst, skal operationer bruge klientgenererede idempotens-nøgler (f.eks. UUID'er) for at tillade sikre genforsøg uden datadublering. Hvis en opgaves `expirationHandler` kaldes, skal igangværende arbejde straks annulleres, ikke-bekræftede handlinger markeres som afventende igen, og `setTaskCompleted(success: false)` skal kaldes. Eventuel konsistens (eventual consistency) opretholdes ved hjælp af konfliktløsningsmekanismer såsom CRDT'er, versionsvektorer eller last-write-wins med sletnings-tombstones, hvilket sikrer, at eksterne ændringer ikke overskriver ikke-synkroniserede lokale ændringer fra udbakken.
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()
}
}
}