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 des tâches en arrière-plan pour une application iOS, et quelles limites fondamentales le système impose-t-il à cette exécution ?
Exécuter des tâches en arrière-plan sous iOS signifie exécuter du code lorsque l'application n'est pas active à l'écran ou visible pour l'utilisateur. Lorsqu'un utilisateur quitte une application, celle-ci passe de l'état actif au premier plan à l'arrière-plan, puis peu après à un état suspendu où son exécution est mise en pause, bien que sa mémoire reste conservée en mémoire vive (RAM, Random Access Memory). Afin de préserver l'autonomie de la batterie, la réactivité du système et les ressources de l'appareil, iOS contrôle strictement l'exécution en arrière-plan. Le système limite la durée pendant laquelle une application peut exécuter du code une fois passée en arrière-plan (généralement limitée à une courte fenêtre d'environ 30 secondes, sauf si des modes d'arrière-plan spécifiques ou des tâches planifiées sont utilisés), bride la priorité du processeur (CPU, Central Processing Unit) et du réseau, et peut terminer les applications suspendues si l'appareil subit une 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 un échec de téléversement d'archive iOS causé par des erreurs de signature de code ou de profil de provisionnement ?
Le diagnostic d'un échec de téléversement d'archive iOS lié à la signature ou au provisionnement repose sur la vérification de trois éléments clés : les certificats de distribution, les profils de provisionnement et les droits d'accès (*entitlements*) de la cible.
1. **Examiner les journaux de diagnostic :** Consultez les journaux de distribution de l'organiseur Xcode, les erreurs détaillées de validation lors du téléversement ou les logs de console CI (Continuous Integration) (`altool`/`notarytool`) pour identifier le motif exact du rejet.
2. **Vérification des certificats et identités :** Vérifiez qu'un certificat Apple Distribution valide est présent dans le trousseau d'accès (Keychain) avec sa clé privée associée, qu'il n'est ni expiré ni révoqué, et que le certificat intermédiaire Apple WWDR (Worldwide Developer Relations) est bien installé.
3. **Correspondance du profil de provisionnement :** Assurez-vous que le profil utilisé pour l'exportation est un profil App Store Distribution correspondant exactement au Bundle Identifier. Si des fonctionnalités comme les notifications push ou les domaines associés sont activées, vérifiez qu'un App ID explicite est utilisé plutôt qu'un identifiant générique (*wildcard*) incompatible.
4. **Alignement des *entitlements* :** Contrôlez la cohérence entre le fichier `.entitlements` de la cible et les fonctionnalités déclarées pour l'App ID sur le portail Apple Developer. Si les droits locaux revendiquent des autorisations non déclarées sur le portail, le téléversement échouera.
5. **Configuration de signature :** En signature automatique, vérifiez l'équipe sélectionnée et les identifiants Apple ID dans les réglages de Xcode. En signature manuelle (ou via options d'export en CI), assurez-vous que `ExportOptions.plist` associe chaque bundle ID au bon certificat et au profil de distribution adéquat.
# 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 fonctionnalité de synchronisation en production ?
Le choix de la bonne API d'arrière-plan dépend de la durée de la tâche, des contraintes d'alimentation et de réseau requises, ainsi que de la nature des transferts :
1. **BGAppRefreshTask** : Idéal pour les mises à jour de contenu courtes et légères (comme le rafraîchissement d'un fil d'actualité, d'une timeline ou d'un tableau de bord) durant environ 15 à 30 secondes. Le système planifie ces tâches selon les habitudes d'utilisation de l'utilisateur afin que les données soient prêtes juste avant l'ouverture habituelle de l'application.
2. **BGProcessingTask** : Conçu pour les opérations lourdes, longues et non urgentes, telles que l'indexation de données, le nettoyage ou la migration de base de données, l'entraînement de modèles de ML ou les synchronisations massives. Cette tâche peut s'exécuter pendant plusieurs minutes et exiger que l'appareil soit en charge (`requiresExternalPower = true`) et connecté au réseau (`requiresNetworkConnectivity = true`), généralement la nuit.
3. **Background URLSession (`URLSessionConfiguration.background`)** : Le choix incontournable pour le transfert de fichiers volumineux (téléversement de photos/vidéos ou téléchargement de ressources volumineuses) qui doit se poursuivre hors processus même si l'application est suspendue ou arrêtée par le système d'exploitation. Le transfert réseau est délégué au démon `nsurlsessiond` au lieu de 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, les fichiers, SQLite et Core Data pour différents types de données applicatives ?
Le choix du mécanisme de stockage iOS approprié dépend de la sensibilité des données, de leur taille, de la complexité relationnelle, des besoins de requêtage et du cycle de vie des sauvegardes : 1. **Keychain** : Pour les données sensibles (jetons d'authentification, mots de passe, clés de chiffrement, secrets biométriques). Il offre un chiffrement matériel, persiste après la réinstallation de l'application et reste sécurisé même dans des environnements sandbox compromis. 2. **UserDefaults** : Pour les préférences et indicateurs légers et non sensibles (par exemple, `hasCompletedOnboarding`, préférence de thème). Comme il est entièrement chargé en mémoire sous forme de liste de propriétés (property list), il ne doit pas être utilisé pour de grands ensembles de données, des médias ou des jetons sensibles. 3. **Système de fichiers (Documents / Caches / Application Support)** : Pour les fichiers discrets et les volumineux objets binaires (images téléchargées, audio, PDF). `Documents` est accessible à l'utilisateur et sauvegardé sur iCloud ; `Caches` sert au contenu purgeable et re-téléchargeable ; `Application Support` est destiné aux fichiers persistants non visibles par l'utilisateur. 4. **SQLite** : Pour les données structurées et tabulaires nécessitant des requêtes indexées rapides, des opérations en masse ou un partage de base de données multiplateforme sans la surcharge d'un graphe d'objets. 5. **Core Data / SwiftData** : Pour les graphes d'objets complexes avec relations, suivi des modifications, chargement paresseux (faulting), gestion de l'annulation (undo management) et intégration directe avec UIKit (`NSFetchedResultsController`) ou SwiftUI (`@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 une synchronisation hors ligne prioritaire (« offline-first ») pour une application de prise de notes où les modifications doivent à terme être synchronisées entre les appareils, mais où iOS peut reporter ou annuler les tâches en arrière-plan. Quelle architecture d'exécution en arrière-plan choisiriez-vous ?
Une architecture de synchronisation hors ligne prioritaire (« offline-first ») traite la base de données locale (telle que SQLite, Core Data ou SwiftData) comme la source unique de vérité immédiate pour l'état de l'UI, tandis que les mutations sont ajoutées à une file d'attente persistante (boîte d'envoi / outbox) sur le disque afin que les modifications non envoyées survivent à l'arrêt du processus. Pour gérer l'exécution en arrière-plan opportuniste et non déterministe d'iOS, le travail en arrière-plan est structuré en plusieurs niveaux :
1. Vidage de la boîte d'envoi lors du passage en arrière-plan : Initié via `UIApplication.shared.beginBackgroundTask(expirationHandler:)` pour terminer les mutations en cours ou rapides.
2. Synchronisation périodique planifiée : Enregistrée auprès de `BGTaskScheduler` à l'aide de `BGAppRefreshTask` pour la synchronisation légère de métadonnées/deltas et de `BGProcessingTask` pour les opérations de synchronisation plus lourdes.
3. Synchronisation de fichiers volumineux : Gérée hors processus via une `URLSessionConfiguration` d'arrière-plan à l'aide de téléversements/téléchargements de fichiers.
4. Synchronisation déclenchée par le serveur : Réveils opportunistes via des notifications push silencieuses (`content-available: 1`).
Étant donné que les tâches en arrière-plan peuvent être interrompues ou replanifiées à tout moment, les opérations doivent utiliser des clés d'idempotence générées par le client (par exemple des UUID) pour permettre des tentatives sûres sans dupliquer les données. Si le `expirationHandler` d'une tâche est appelé, le travail en cours doit être annulé immédiatement, les opérations non validées doivent être remises en attente, et `setTaskCompleted(success: false)` doit être appelé. La cohérence éventuelle est maintenue à l'aide de mécanismes de résolution de conflits tels que les CRDT (Conflict-free Replicated Data Types), les vecteurs de version ou la stratégie de dernière écriture gagnante (last-write-wins) avec des marqueurs de suppression (tombstones), garantissant que les deltas distants n'écrasent pas 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()
}
}
}