20 questions fréquemment posées en entretien iOS. Le développement iOS est un domaine très recherché axé sur les apps Swift, SwiftUI et UIKit, avec des sujets de lifecycle, concurrence, réseau, stockage, performance, tests et publication sur l'App Store. Les questions couvrent plusieurs niveaux, et vous pouvez vous entraîner à y répondre à voix haute dans notre simulateur d'entretien.
1Quelles responsabilités un UIViewController doit-il assumer dans une architecture MVC (Model-View-Controller) typique sous iOS ?
Dans le modèle MVC (Model-View-Controller) classique d'iOS, le `UIViewController` joue le rôle de contrôleur faisant le lien entre les composants de la vue (`View`) et les données du modèle (`Model`). Ses responsabilités principales incluent la gestion du cycle de vie de la vue (par exemple `viewDidLoad`, `viewWillAppear`), la configuration et la mise à jour des éléments d'interface utilisateur, la gestion des interactions directes de l'utilisateur (comme les appuis sur des boutons, les délégués et le mécanisme cible-action), ainsi que l'orchestration des transitions et des présentations d'écrans. Les responsabilités non liées à l'interface utilisateur — telles que les appels réseau bruts, la persistance des données et la logique métier lourde — doivent être déléguées à des services dédiés ou à des objets métier afin d'éviter que le contrôleur ne devienne un « 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
}
}
2Quel est le rôle d'un provisioning profile lors de la publication d'une application iOS ?
Lors de la publication d'une application iOS, un provisioning profile fait office de package signé cryptographiquement qui fait le lien entre les exigences de sécurité d'Apple, le binaire de l'application et les appareils cibles. Son rôle principal est d'indiquer au système d'exploitation iOS et à App Store Connect que l'application est autorisée à s'exécuter selon des règles de distribution précises. Un provisioning profile regroupe plusieurs éléments essentiels : l'App ID (Bundle Identifier), le ou les certificats de signature autorisés confirmant l'identité de l'émetteur du build, les droits et fonctionnalités accordés à l'application (comme les notifications push, Sign in with Apple ou iCloud), et — dans le cas de profils hors App Store tels que Development ou Ad-Hoc — une liste d'identifiants d'appareils autorisés (UDID). Pour les profils de distribution destinés à l'App Store, les UDID individuels des appareils sont omis car Apple autorise la distribution vers l'ensemble des appareils grand public via l'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)
3Que signifie exécuter du travail en arrière-plan dans une application iOS, et quelles limites de base le système impose-t-il ?
Il s’agit d’exécuter du code lorsque l’application n’est plus active à l’écran ou visible. Lorsqu’un utilisateur quitte l’application, elle passe du premier plan actif à l’arrière-plan, puis rapidement à l’état suspendu : son exécution est interrompue, mais sa mémoire reste en mémoire vive (RAM). Pour préserver batterie, réactivité et ressources, iOS contrôle strictement cette exécution. Par défaut, seule une courte période déterminée par le système est disponible après le passage en arrière-plan. Une demande de temps d’exécution fini ou une interface de programmation (API) d’arrière-plan appropriée peut accorder du temps supplémentaire, sans durée fixe garantie. Le système réduit les priorités processeur et réseau et peut terminer les applications suspendues en cas de pression mémoire.
4Qu'est-ce qu'un publisher Combine, et en quoi diffère-t-il d'un subscriber dans un flux de données iOS typique ?
Dans Combine, un Publisher émet un flux de valeurs au fil du temps et peut se terminer par un événement de complétion (soit en se terminant avec succès, soit en échouant avec une erreur). Il déclare deux types associés : `Output` (le type de données qu'il produit) et `Failure` (le type d'erreur qu'il peut émettre). Un Subscriber reçoit les valeurs et les événements de cycle de vie de la part d'un publisher. Dans un flux de données iOS typique, les publishers jouent le rôle de source ou de producteur d'événements asynchrones (comme des réponses réseau, des notifications ou des saisies utilisateur), tandis que les subscribers agissent comme des consommateurs qui réagissent aux valeurs émises, gèrent les erreurs et traitent la complétion (comme la mise à jour de l'interface utilisateur ou l'écriture dans une base de données). Le subscriber s'attache à un publisher, reçoit une souscription (subscription) et émet une demande (demand) pour recevoir des éléments.
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)")
}
)
5Que signifie async/await en Swift, et comment l'utiliseriez-vous pour appeler une API (Application Programming Interface) asynchrone depuis une application iOS ?
`async/await` est la syntaxe intégrée de Swift permettant d'écrire du code asynchrone de manière linéaire, lisible et séquentielle, plutôt qu'en utilisant des fermetures imbriquées et des gestionnaires de complétion (completion handlers). Marquer une fonction avec `async` indique au compilateur que la fonction peut suspendre son exécution en attendant la fin d'un travail de longue durée (comme des requêtes réseau ou des entrées/sorties de fichiers). Le mot-clé `await` signale un point de suspension : l'exécution de la fonction en cours est mise en pause, libérant le thread sous-jacent pour d'autres tâches, et reprend dès que le résultat attendu ou l'erreur est disponible. Pour appeler une API asynchrone dans une application iOS, vous appelez la méthode `async` précédée de `await` (ou `try await` si la fonction peut lever une erreur) depuis un contexte asynchrone, par exemple à l'intérieur d'une autre fonction `async` ou d'un bloc `Task`.
6Dans quel cas utiliseriez-vous UserDefaults dans une application iOS, et quels types de données ne devraient pas y être stockés ?
`UserDefaults` est conçu pour stocker des préférences utilisateur légères, des paramètres et des indicateurs (flags) d'un lancement d'application à l'autre (par exemple, les préférences de thème, le niveau de volume ou un indicateur booléen `hasSeenOnboarding`). Il prend en charge les types de liste de propriétés (property list) tels que `String`, `Int`, `Double`, `Bool`, `Date`, `Data`, `Array` et `Dictionary`. Vous ne devez PAS y stocker : 1. Des informations sensibles (comme les mots de passe utilisateur, les clés privées ou les jetons d'authentification), car `UserDefaults` stocke les données non chiffrées dans un fichier plist en texte brut. 2. De grands ensembles de données, des documents ou des fichiers multimédias (comme des images, des fichiers audio ou de gros payloads JSON), car l'intégralité du fichier plist est chargée en mémoire lors de son accès, ce qui entraîne une surcharge importante de mémoire et ralentit le lancement de l'application.
// 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"
7Qu'est-ce que l'ARC (Automatic Reference Counting) en Swift, et comment gère-t-il la durée de vie des instances de classe dans une application iOS ?
L'ARC (Automatic Reference Counting) est le mécanisme de gestion de mémoire à la compilation de Swift permettant de suivre et gérer la durée de vie des instances de classe (types par référence). Chaque fois qu'une nouvelle instance de classe est créée et assignée à une référence forte, l'ARC suit un compteur de références interne pour cette instance. Tant qu'au moins une référence forte vers une instance existe, l'ARC maintient l'instance en mémoire. Lorsque toutes les références fortes sont supprimées et que le compteur de références tombe à zéro, l'ARC désalloue immédiatement l'instance pour libérer la mémoire, en appelant automatiquement la méthode `deinit` de l'instance juste avant la désallocation. Contrairement au ramasse-miettes (Garbage Collector) à l'exécution dans des environnements comme Java ou .NET, l'ARC n'exécute pas de cycles de balayage périodiques ; le compilateur Swift insère les appels retain et release appropriés dans le binaire au moment de la compilation.
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
8Comment effectuez-vous une simple requête HTTP (Hypertext Transfer Protocol) GET avec URLSession dans une application iOS ?
Pour effectuer une simple requête HTTP GET dans une application iOS, vous créez un objet URL et le transmettez à une instance de URLSession, comme `URLSession.shared`. Vous pouvez exécuter la requête à l'aide d'une fonction de rappel de fin de tâche de données (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) ou en utilisant la concurrence moderne de Swift (`let (data, response) = try await URLSession.shared.data(from: url)`). Lorsque vous utilisez l'approche traditionnelle avec data task, vous devez appeler `.resume()` sur la tâche pour la démarrer, gérer les éventuelles erreurs de transport, inspecter le code de statut de la réponse `HTTPURLResponse` et veiller à ce que toutes les mises à jour de l'interface utilisateur soient exécutées sur le thread principal.
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()
9Qu'est-ce qu'Instruments dans Xcode, et comment l'utiliseriez-vous pour analyser un problème simple de performances dans une application iOS ?
Instruments est l'outil de profilage et d'analyse des performances d'Apple intégré à Xcode. Il permet aux développeurs de surveiller le comportement à l'exécution, l'utilisation du CPU, les allocations mémoire, les fuites de mémoire et les performances de rendu de l'interface utilisateur. Pour analyser un problème simple de performances (comme des saccades de l'interface ou une utilisation élevée du CPU), vous lancez Instruments depuis Xcode (via Product -> Profile), choisissez un modèle approprié (comme Time Profiler pour les goulots d'étranglement du CPU ou Allocations/Leaks pour les problèmes de mémoire), puis enregistrez une trace tout en reproduisant le parcours utilisateur problématique sur un appareil. Après l'enregistrement, vous analysez l'arbre d'appels et les traces de pile les plus consommatrices afin d'identifier avec précision les méthodes à l'origine des ralentissements ou d'une consommation excessive de ressources, ce qui vous permet de mesurer et de localiser le goulot d'étranglement avant de modifier le code.
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.
10Qu'est-ce que l'APNs (Apple Push Notification service) et quel rôle joue-t-il dans la distribution des notifications push vers une application iOS ?
L'APNs (Apple Push Notification service) est le service intermédiaire cloud d'Apple qui achemine de manière sécurisée les notifications push depuis vos serveurs backend (serveurs fournisseurs) vers les appareils Apple. Comme les appareils mobiles ne peuvent pas maintenir des connexions socket ouvertes et continues vers chaque serveur backend sans épuiser la batterie et consommer une bande passante réseau excessive, Apple maintient une unique connexion persistante et basse consommation entre chaque appareil et l'APNs.
Lorsque votre application s'enregistre pour recevoir des notifications distantes, l'APNs génère un jeton d'appareil (device token) unique et opaque identifiant cette installation spécifique sur cet appareil précis. L'application transmet ce jeton à votre serveur backend fournisseur. Lorsque ce dernier souhaite notifier l'utilisateur, il construit un payload et l'envoie accompagné du jeton d'appareil à l'APNs via HTTP/2. L'APNs localise ensuite la connexion active de l'appareil et transmet directement le payload de la notification push à iOS, qui affiche l'alerte ou réveille l'application selon la configuration définie.
11Comment refactoriseriez-vous un UIViewController volumineux qui gère à la fois les mises à jour de l'UI (User Interface), la validation, les appels réseau et la navigation ?
La refactorisation d'un Massive View Controller (MVC) doit être effectuée de manière progressive pour limiter les risques, en séparant les responsabilités dans des couches dédiées :
1. **Extraire le réseau et les données :** Déplacez les appels d'API (Application Programming Interface), la persistance des données et l'analyse JSON hors du contrôleur de vue vers des classes de Service ou de Repository dédiées, en vous appuyant sur des protocoles.
2. **Extraire l'état de présentation et la validation :** Introduisez un ViewModel (ou un Presenter). Déplacez la validation des saisies, le formatage des chaînes et la gestion de l'état d'affichage dans le ViewModel afin de pouvoir tester cette logique unitairement et de façon isolée.
3. **Extraire la navigation :** Adoptez le patron Coordinator (ou Router / Flow Controller) pour découpler les appels de présentation/empilement d'écrans (*push/present*) et le flux de navigation du contrôleur de vue.
4. **Conserver un contrôleur de vue allégé :** Ne gardez dans le contrôleur que la mise en page de l'interface, la configuration des sous-vues, les méthodes du cycle de vie et la liaison (*binding*) des contrôles UI avec le ViewModel.
5. **Progression par étapes avec tests :** Ajoutez des tests unitaires pour chaque nouveau composant (services et ViewModels) au fil de la refactorisation afin de garantir l'absence de régression fonctionnelle.
// 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() }
}
12Comment diagnostiqueriez-vous l’échec d’envoi d’une archive iOS dû à la signature ou au provisionnement ?
Vérifiez les certificats de distribution, profils de provisionnement et droits des cibles. Consultez les journaux de distribution de Xcode Organizer, les erreurs détaillées de validation ou les journaux d’intégration continue (CI) de l’outil d’envoi App Store pour connaître le motif exact ; notarytool concerne la notarisation macOS. Assurez-vous qu’un certificat Apple Distribution valide et sa clé privée correspondante sont présents dans le trousseau, que le certificat n’est ni expiré ni révoqué et que le certificat intermédiaire Apple WWDR est disponible. Le profil utilisé à l’export doit être un profil App Store Distribution correspondant exactement à l’identifiant de bundle. Pour les notifications push ou Associated Domains, vérifiez qu’un App ID explicite est configuré plutôt qu’un identifiant générique incompatible. Comparez le fichier .entitlements de la cible aux capacités activées pour l’App ID dans le portail : des droits locaux absents du profil font échouer l’envoi. Avec la signature automatique, vérifiez l’équipe sélectionnée et les identifiants Apple dans les réglages Xcode. Avec la signature manuelle ou les options d’export CI, ExportOptions.plist doit associer chaque identifiant de bundle au bon profil et sélectionner séparément l’équipe et le certificat appropriés.
# 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
13Comment choisiriez-vous entre BGAppRefreshTask, BGProcessingTask et une URLSession en arrière-plan pour une synchronisation de production ?
Le choix dépend de la durée, des besoins d’alimentation et de réseau, et de la nature des données transférées. BGAppRefreshTask convient aux mises à jour légères et courtes, comme un fil d’actualités, une chronologie sociale ou un tableau de bord, durant environ 15–30 secondes. Le système les programme d’après les habitudes pour préparer du contenu frais avant l’ouverture habituelle. BGProcessingTask vise des opérations longues, lourdes et non urgentes : indexation, nettoyage ou migration de base, entraînement de modèle ou synchronisation importante. Elle peut durer plusieurs minutes et exiger une alimentation externe avec requiresExternalPower = true ou une connexion réseau avec requiresNetworkConnectivity = true, cette dernière n’exigeant pas uniquement le Wi-Fi. Le système choisit le lancement ; ni une exécution nocturne ni une durée fixe ne sont garanties. Une URLSession configurée avec URLSessionConfiguration.background convient aux gros transferts, comme l’envoi de photos ou vidéos et le téléchargement de ressources, devant continuer hors processus si l’application est suspendue ou terminée par le système. Le démon nsurlsessiond prend en charge le transfert sans maintenir le code de l’application actif en mémoire.
14Comment construiriez-vous un pipeline Combine pour un champ de recherche textuel qui temporise les saisies (debounce), ignore les doublons, effectue une requête réseau et met à jour l'UI (User Interface) en toute sécurité ?
Pour construire un pipeline de recherche sécurisé avec Combine :
1. **Temporisation et déduplication** : Récupérez l'émetteur de la requête de recherche (par exemple `$queryText`), appliquez `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` pour attendre une pause dans la frappe, puis `.removeDuplicates()` pour ignorer les chaînes inchangées.
2. **Requête réseau et annulation** : Transformez chaque terme de recherche en émetteur de requête réseau et aplatissez avec `.switchToLatest()`. Cet opérateur annule automatiquement les requêtes en cours dès qu'une nouvelle saisie arrive, évitant ainsi les situations de concurrence et les résultats obsolètes.
3. **Isolation des erreurs** : Interceptez les erreurs réseau à l'intérieur de l'émetteur interne (par exemple `.catch { _ in Just([]) }`) afin qu'un échec réseau n'interrompe pas le flux principal de recherche textuelle.
4. **Réception sur le thread principal** : Appliquez `.receive(on: DispatchQueue.main)` avant de mettre à jour l'état ou d'assigner les valeurs aux éléments de l'interface utilisateur.
15Comment utiliseriez-vous async/await et l'annulation de `Task` en Swift pour implémenter un écran qui charge des données depuis le réseau et évite de mettre à jour l'interface utilisateur (UI, User Interface) après que l'utilisateur a quitté l'écran ?
Je définirais le chargement réseau comme une fonction `async` sur un service ou un view model et l'exécuterais depuis une `Task` dont la durée de vie est liée à l'écran. Dans UIKit, cela implique généralement de stocker une variable comme `var loadTask: Task<Void, Never>?` sur le view controller ou le view model, de la démarrer lorsque l'écran doit charger des données et de l'annuler dans `viewWillDisappear`, `deinit`, ou avant de lancer une nouvelle requête selon la durée de vie souhaitée. Dans SwiftUI, je privilégierais `.task` ou `.task(id:)` lorsque possible, car SwiftUI annule automatiquement la tâche lorsque la vue disparaît ou que l'identifiant change. Au sein de la tâche, j'appellerais l'API réseau asynchrone avec `try await`, je gérerais l'annulation séparément des véritables erreurs et je vérifierais l'état d'annulation avant d'appliquer les résultats s'il y a plusieurs étapes d'attente (`await`) ou de traitement. Les modifications d'état de l'UI doivent impérativement s'effectuer sur le thread principal via le main actor, soit en annotant le view model avec `@MainActor`, soit en utilisant `await MainActor.run`. Pour éviter d'afficher des données obsolètes dans l'UI, j'annulerais les tâches précédentes, éviterais les tâches détachées ou globales (`Task.detached`) pour les opérations liées à l'écran, et comparerais éventuellement un identifiant de requête ou d'élément en cours avant d'appliquer le résultat.
@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)
}
}
}
}
16Comment choisiriez-vous entre UserDefaults, Keychain, fichiers, SQLite et Core Data pour différents types de données applicatives ?
Le choix dépend de la sensibilité, du volume, des relations, des requêtes et du cycle de sauvegarde. Keychain convient aux jetons, mots de passe et clés : il chiffre le stockage et applique des contrôles d’accès. Les modèles biométriques restent gérés par le système et ne sont pas exposés aux applications. Keychain ne protège pas contre du code disposant des accès autorisés de l’application ; la persistance après réinstallation ne constitue pas un contrat de stockage. UserDefaults convient aux petites préférences et indicateurs non sensibles, comme hasCompletedOnboarding ou le thème, pas aux gros jeux de données, médias ou jetons sensibles. Le système de fichiers convient aux fichiers distincts et gros objets binaires, comme images, audio et PDF. Documents contient les fichiers destinés à l’utilisateur et sauvegardés dans iCloud ; Caches les contenus supprimables et retéléchargeables ; Application Support les fichiers persistants internes. SQLite convient aux données structurées tabulaires nécessitant requêtes indexées rapides, opérations en masse ou partage multiplateforme sans le coût d’un graphe d’objets. Core Data ou SwiftData convient aux graphes complexes avec relations, suivi des changements, chargement différé, annulation et intégration UIKit avec NSFetchedResultsController ou SwiftUI avec @Query.
17Sur un écran UIKit en production utilisant des rappels provenant d'un ViewModel, comment décidez-vous de capturer self en tant que weak, unowned ou strong dans les fermetures (closures) ?
Dans une architecture UIKit où un view controller interagit avec un view model via des fonctions de rappel, la stratégie de capture est déterminée par la propriété (ownership) et le cycle de vie :
1. `[weak self]` : L'approche standard et la plus sûre pour les fermetures stockées ou échappantes (escaping), telles que les rappels d'événements du view model ou les gestionnaires de complétion asynchrones réseau/données. Le view controller détenant le view model, capturer `self` fortement dans un rappel stocké par le view model crée un cycle de références fortes (`VC -> VM -> closure -> VC`). `[weak self]` transforme `self` en optionnel (`UIViewController?`), permettant à `self` d'être libéré proprement et d'éviter les fuites de mémoire.
2. `[unowned self]` : Suppose que `self` ne sera jamais `nil` lors de l'exécution de la fermeture. Cela doit être utilisé avec une extrême prudence dans les écrans d'interface. Si une tâche ou un rappel asynchrone se termine après la fermeture et la désallocation du view controller, accéder à `unowned self` déclenche un crash fatal à l'exécution. C'est pourquoi `[weak self]` est généralement préféré à `unowned` dans les rappels d'interface UIKit en production.
3. Capture forte (par défaut, sans capture list) : Appropriée lorsque la fermeture est non-échappante (ex. opérations classiques sur les collections comme `map`/`filter`) ou lorsqu'elle a une courte durée de vie et ne crée aucun cycle de rétention vers `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 */ }
}
18Vous rejoignez une application iOS mature comprenant des centaines d'écrans, de nombreux view controllers volumineux (Massive View Controllers) et des cycles de livraison lents. Comment planifieriez-vous une stratégie d'amélioration incrémentale de l'architecture sans interrompre la livraison de fonctionnalités ?
Une stratégie de migration incrémentale efficace évite les réécritures complètes et associe le refactorisage directement à la livraison continue de fonctionnalités (en suivant le patron Strangler Fig). Vous commencez par définir une architecture cible concertée (telle que MVVM ou VIPER avec injection de dépendances et coordinateurs modulaires) et par mettre en place des bases de tests (tests de caractérisation, de snapshot et unitaires) autour du code existant avant de le modifier.
La priorisation doit s'appuyer sur la fréquence de modification (churn) et le risque : refactorisez les écrans que les équipes modifient activement ou les zones présentant un taux élevé de bugs, plutôt que le code patrimonial stable. Lors du refactorisage des Massive View Controllers, découplez la logique métier et de présentation dans des ViewModels/Présenteurs dédiés et introduisez des adaptateurs ou coordinateurs basés sur des protocoles pour isoler l'ancienne interface du nouveau code.
Enfin, maintenez la vélocité et la qualité grâce à une gouvernance architecturale : fournissez des implémentations de référence (« golden samples »), faites respecter les frontières architecturales via des linters en CI ou des directives de revue de code, et allouez une capacité continue au traitement de la dette technique (ex. 15 à 20 % de la capacité ou association du refactorisage aux fonctionnalités connexes) tout en suivant des métriques telles que les temps de compilation, les taux de plantage et la couverture de code.
19Vous déployez une application iOS à fort trafic impliquant une migration de base de données et une modification d'API (Application Programming Interface) backend ; comment concevriez-vous le plan de déploiement progressif pour minimiser l'impact sur les utilisateurs ?
La conception d'un plan de déploiement pour une application iOS à fort trafic impliquant des migrations de base de données et des modifications d'API backend nécessite de découpler le déploiement de l'activation des fonctionnalités, en raison des contraintes inhérentes aux clients iOS (mises à jour asynchrones des utilisateurs et impossibilité d'imposer un rollback du binaire côté client).
1. Compatibilité backend : Mettre en œuvre une stratégie « expand-and-contract ». Déployer l'API v2 en parallèle de l'API v1 afin que les versions de clients existantes et mises à jour fonctionnent simultanément sans rupture de compatibilité.
2. Migration de base de données résiliente : S'assurer que les migrations de schéma local (par exemple SQLite, Core Data, SwiftData) sont idempotentes, non destructives et gèrent de manière sûre les sauts de plusieurs versions (par exemple une mise à niveau de N-3 vers N) sans bloquer le thread principal ni provoquer de boucles de crash au lancement.
3. Activation dynamique : Livrer les nouvelles fonctionnalités clientes masquées derrière des feature flags ou de la configuration distante pilotée par le serveur. Garder le feature flag désactivé lors de la distribution initiale.
4. Déploiement progressif et surveillance : Distribuer l'application via le déploiement par phases de l'App Store (App Store Phased Release sur 7 jours). Surveiller en continu la télémétrie, les taux de crash, les métriques de succès de migration de la base de données et les taux d'erreur de l'API. En cas d'anomalie, suspendre le déploiement progressif dans App Store Connect et désactiver les feature flags sans nécessiter de rollback d'urgence du binaire.
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.
20Vous concevez la synchronisation d’une application de notes privilégiant le hors ligne : les modifications doivent converger entre appareils, mais iOS peut différer ou annuler le travail en arrière-plan. Quelle architecture d’exécution choisissez-vous ?
La base locale, SQLite, Core Data ou SwiftData, est la source de vérité immédiate de l’interface. Les mutations sont ajoutées à une boîte d’envoi persistante sur disque pour que les modifications non envoyées survivent à la mort du processus. L’exécution opportuniste et non déterministe d’iOS exige plusieurs niveaux. Au passage en arrière-plan, UIApplication.shared.beginBackgroundTask(expirationHandler:) permet de terminer des mutations en cours ou rapides. BGTaskScheduler programme BGAppRefreshTask pour les métadonnées ou deltas légers et BGProcessingTask pour des opérations plus lourdes. Les ressources volumineuses sont transférées hors processus par une URLSessionConfiguration d’arrière-plan, avec envois et téléchargements de fichiers. Les notifications silencieuses content-available: 1 offrent des réveils opportunistes déclenchés par le serveur. Toute opération doit porter une clé d’idempotence générée côté client, par exemple un identifiant universellement unique (UUID), car une tâche peut être interrompue ou reprogrammée à tout moment. Si expirationHandler est appelé, annulez immédiatement le travail en cours, remettez les opérations non validées en attente et appelez setTaskCompleted(success: false). Synchronisez aussi au premier plan et après les modifications locales, sous la responsabilité d’un coordinateur unique. Aucun lancement en arrière-plan n’est garanti : ne promettez pas de délai fixe de convergence. Poursuivez la cohérence à terme avec des types de données répliqués sans conflit (CRDT), des vecteurs de versions associés à une politique de fusion ou une stratégie dernière écriture gagnante avec marqueurs de suppression, en empêchant les deltas distants d’écraser les mutations locales non validées de la boîte d’envoi.
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()
}
}
}