Preparazione al colloquio iOS

Domande di colloquio su iOS

20 domande frequenti di colloquio su iOS. Lo sviluppo iOS è un ambito molto richiesto incentrato su app Swift, SwiftUI e UIKit, con problemi di lifecycle, concorrenza, rete, storage, performance, test e pubblicazione su App Store. Le domande coprono livelli diversi e puoi esercitarti a rispondere a voce nel nostro simulatore di colloquio.

Inizia un colloquio AI su iOSNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande per principianti

1Quali responsabilità dovrebbe avere un UIViewController in una tipica architettura MVC (Model-View-Controller) per iOS?

Nel classico pattern MVC (Model-View-Controller) di iOS, `UIViewController` funge da Controller facendo da mediatore tra i componenti della View e i dati del Model. Le sue responsabilità principali includono la gestione del ciclo di vita della view (ad esempio `viewDidLoad`, `viewWillAppear`), la configurazione e l'aggiornamento degli elementi dell'interfaccia utente, la gestione delle interazioni dirette dell'utente (come tap sui pulsanti, delegati e target-action) e l'orchestrazione delle transizioni o presentazioni tra schermate. Le responsabilità non legate alla UI — come le chiamate di rete grezze, la persistenza e la logica di business complessa — dovrebbero essere delegate a oggetti di servizio o di modello dedicati, evitando così che il view controller si trasformi in 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
    }
}
Prova a rispondere a questa domanda con un coach IA

2Qual è il ruolo di un provisioning profile nel rilascio di un'applicazione iOS?

Nel rilascio su iOS, un provisioning profile funge da pacchetto firmato crittograficamente che raccorda i requisiti di sicurezza di Apple con il binario dell'app e i dispositivi di destinazione. Il suo ruolo principale è indicare al sistema operativo iOS e ad App Store Connect che l'applicazione è autorizzata a essere eseguita secondo specifiche regole di distribuzione. Un provisioning profile raggruppa diversi elementi critici: l'App ID (Bundle Identifier), i certificati di firma autorizzati che confermano chi ha compilato l'app, gli entitlement e le funzionalità (capabilities) concesse all'app (come le notifiche push, Sign in with Apple o iCloud) e — nei profili non destinati ad App Store come Development o Ad-Hoc — un elenco di UDID (Unique Device Identifier) dei dispositivi consentiti. Per i profili di rilascio di App Store, gli UDID dei singoli dispositivi vengono omessi perché Apple consente la distribuzione a qualsiasi dispositivo consumer tramite 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)
Prova a rispondere a questa domanda con un coach IA

3Cosa significa per un'app iOS eseguire operazioni in background e quali limiti fondamentali impone il sistema a tali operazioni?

Eseguire operazioni in background su iOS significa eseguire codice quando l'app non è attualmente attiva sullo schermo o visibile all'utente. Quando un utente esce da un'app, questa passa dallo stato attivo in primo piano (foreground) al background e, poco dopo, a uno stato di sospensione in cui l'esecuzione viene messa in pausa, sebbene la sua memoria rimanga nella RAM. Per preservare l'autonomia della batteria, la reattività del sistema e le risorse del dispositivo, iOS controlla rigorosamente l'esecuzione in background. Il sistema limita il tempo in cui un'app può eseguire codice una volta passata in background (in genere una breve finestra temporale finita di circa 30 secondi, a meno che non si utilizzino modalità di background specifiche o attività pianificate), riduce la priorità di CPU e rete e può terminare le app sospese se il dispositivo riscontra una pressione sulla memoria.

import UIKit

NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    print("App entered background. Code execution will pause soon unless extended.")
}
Prova a rispondere a questa domanda con un coach IA

4Che cos'è un publisher in Combine e in cosa differisce da un subscriber in un tipico flusso di dati iOS?

In Combine, un Publisher emette un flusso di valori nel tempo e può terminare con un evento di completamento (completando con successo o fallendo con un errore). Dichiara due tipi associati: `Output` (il tipo di dato che produce) e `Failure` (il tipo di errore che può emettere). Un Subscriber riceve valori ed eventi del ciclo di vita da un publisher. In un tipico flusso di dati iOS, i publisher fungono da sorgente o produttore di eventi asincroni (come risposte di rete, notifiche o input dell'utente), mentre i subscriber fungono da consumer che reagiscono ai valori emessi, gestiscono gli errori e rispondono al completamento (come l'aggiornamento dello stato della UI o la scrittura in un database). Il subscriber si collega a un publisher, riceve un token di sottoscrizione e richiede la domanda (demand) di elementi.

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)")
    }
)
Prova a rispondere a questa domanda con un coach IA

5Cosa significa async/await in Swift e come lo utilizzeresti per chiamare una API (Application Programming Interface) asincrona da un'app iOS?

`async/await` è la sintassi integrata di Swift per scrivere codice asincrono in modo lineare, leggibile e sequenziale, anziché utilizzare closure annidate e completion handler. Contrassegnare una funzione con `async` indica al compilatore che la funzione può sospendere la propria esecuzione in attesa del completamento di un'operazione prolungata (come richieste di rete o operazioni di I/O su file). La parola chiave `await` indica un punto di sospensione: l'esecuzione della funzione corrente si interrompe temporaneamente, liberando il thread sottostante per svolgere altro lavoro, e riprende non appena il risultato atteso o l'eventuale errore è pronto. Per chiamare un'API asincrona in un'app iOS, si invoca il metodo `async` preceduto da `await` (e da `try await` se la funzione può generare errori/eccezioni) all'interno di un contesto asincrono, come un'altra funzione `async` o un blocco `Task`.

func fetchUserData(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    
    return try JSONDecoder().decode(User.self, from: data)
}
Prova a rispondere a questa domanda con un coach IA

6Quando è opportuno utilizzare UserDefaults in un'app iOS e quali tipi di dati non dovrebbero esservi memorizzati?

UserDefaults è progettato per memorizzare preferenze utente, impostazioni e flag di piccole dimensioni e leggeri tra i lanci dell'app (ad esempio, preferenze di tema, livello del volume o un flag booleano `hasSeenOnboarding`). Supporta i tipi di property list come String, Int, Double, Bool, Date, Data, Array e Dictionary. NON si dovrebbero memorizzare: 1. Informazioni sensibili (come password utente, chiavi private o token di autenticazione), poiché UserDefaults salva i dati non cifrati in un file plist in testo non codificato. 2. Dataset di grandi dimensioni, documenti o file multimediali (come immagini, audio o payload JSON corposi), poiché l'intero file plist viene caricato in memoria all'accesso, causando un elevato consumo di memoria e rallentando l'avvio dell'applicazione.

// 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"
Prova a rispondere a questa domanda con un coach IA

7Che cos'è ARC (Automatic Reference Counting) in Swift e come gestisce il ciclo di vita delle istanze di classe in un'app iOS?

ARC (Automatic Reference Counting) è il meccanismo di gestione della memoria a tempo di compilazione di Swift per tracciare e gestire il ciclo di vita delle istanze di classe (tipi per riferimento). Ogni volta che viene creata una nuova istanza di una classe e assegnata a un riferimento forte (strong reference), ARC tiene traccia di un conteggio interno dei riferimenti per quell'istanza. Finché esiste almeno un riferimento forte verso un'istanza, ARC la mantiene in memoria. Quando tutti i riferimenti forti vengono rimossi e il conteggio dei riferimenti scende a zero, ARC dealloca immediatamente l'istanza per liberare memoria, invocando automaticamente il metodo `deinit` dell'istanza subito prima della deallocazione. A differenza della garbage collection a runtime in ambienti come Java o .NET, ARC non esegue cicli periodici di pulizia (sweep); il compilatore Swift inserisce le opportune chiamate di retain e release direttamente nel binario durante la compilazione.

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
Prova a rispondere a questa domanda con un coach IA

8Come si esegue una semplice richiesta HTTP (Hypertext Transfer Protocol) GET con URLSession in un'app iOS?

Per eseguire una semplice richiesta HTTP GET in un'app iOS, si crea un oggetto URL e lo si passa a un'istanza di URLSession, come URLSession.shared. È possibile eseguire la richiesta utilizzando una completion handler di data task (URLSession.shared.dataTask(with: url) { data, response, error in ... }) oppure tramite la moderna concorrenza di Swift (let (data, response) = try await URLSession.shared.data(from: url)). Quando si usa l'approccio tradizionale basato su data task, è necessario chiamare .resume() sul task per avviarlo, gestire eventuali errori di trasporto, ispezionare il codice di stato HTTPURLResponse e assicurarsi che tutti gli aggiornamenti della UI siano inviati al thread principale.

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()
Prova a rispondere a questa domanda con un coach IA

9Che cos'è Instruments in Xcode e come lo utilizzeresti per analizzare un semplice problema di prestazioni in un'app iOS?

Instruments è lo strumento di analisi e profilazione delle prestazioni di Apple integrato in Xcode. Consente agli sviluppatori di monitorare il comportamento a runtime, l'utilizzo della CPU, le allocazioni di memoria, i memory leak e le prestazioni di rendering della UI. Per esaminare un semplice problema di prestazioni (come scatti dell'interfaccia o un elevato utilizzo della CPU), si avvia Instruments da Xcode (tramite Product -> Profile), si sceglie un template appropriato (come Time Profiler per colli di bottiglia della CPU o Allocations/Leaks per problemi di memoria) e si registra una traccia riproducendo il flusso utente problematico su un dispositivo. Dopo la registrazione, si analizzano il call tree e le stack trace più pesanti per individuare con precisione i metodi che causano rallentamenti o un consumo eccessivo di risorse, consentendo di misurare e localizzare il collo di bottiglia prima di modificare il codice.

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.
Prova a rispondere a questa domanda con un coach IA

10Che cos'è APNs (Apple Push Notification service) e quale ruolo svolge nel recapito delle notifiche push a un'app iOS?

APNs (Apple Push Notification service) è il servizio intermediario basato su cloud di Apple che instrada in modo sicuro le notifiche push dai server di backend (server provider) ai dispositivi Apple. Poiché i dispositivi mobili non possono mantenere connessioni socket aperte e continue con il backend di ogni singola app senza scaricare la batteria e consumare troppa larghezza di banda di rete, Apple mantiene un'unica connessione persistente a basso consumo energetico tra ciascun dispositivo e APNs. Quando l'app si registra per le notifiche remote, APNs genera un device token univoco e opaco che identifica quella specifica installazione dell'app su quello specifico dispositivo. L'app invia questo token al server provider di backend. Quando il provider desidera notificare l'utente, costruisce un payload e lo invia insieme al device token ad APNs tramite HTTP/2. APNs individua quindi la connessione attiva per quel dispositivo e recapita il payload della notifica push direttamente a iOS, che mostra l'avviso o attiva l'app in base alla configurazione.

[Provider Server] ---> (HTTP/2 Request + Device Token + Payload) ---> [APNs]
                                                                           |
                                                               (Persistent APNs Connection)
                                                                           v
                                                                    [iOS Device / App]
Prova a rispondere a questa domanda con un coach IA

Domande intermedie

11Come effettueresti il refactoring di un UIViewController di grandi dimensioni che gestisce aggiornamenti della UI (User Interface), validazione, networking e navigazione?

Il refactoring di un Massive View Controller (MVC) deve essere eseguito in modo incrementale per ridurre i rischi, separando le diverse responsabilità in livelli dedicati: 1. Estrarre networking e dati: spostare le chiamate API, la persistenza dei dati e il parsing JSON fuori dal view controller verso classi di tipo Service o Repository dedicate, mediante astrazioni basate su protocolli. 2. Estrarre lo stato di presentazione e la validazione: introdurre un ViewModel (o Presenter). Spostare la validazione dell'input, la formattazione delle stringhe e la gestione dello stato della UI nel ViewModel, rendendo questa logica testabile con unit test in isolamento. 3. Estrarre la navigazione: adottare il pattern Coordinator (oppure Router / Flow Controller) per rimuovere le chiamate di tipo push/present e la logica del flusso di navigazione dal view controller. 4. Mantenere il View Controller snello: conservare solo il layout della UI, la configurazione delle subview, gli hook del ciclo di vita e il binding dei controlli UI al ViewModel. 5. Esecuzione incrementale con test: aggiungere unit test per i servizi e i ViewModel appena estratti durante il refactoring, per garantire che il comportamento esistente rimanga inalterato.

// 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() }
}
Prova a rispondere a questa domanda con un coach IA

12Come risolveresti un errore di caricamento di un archivio iOS causato da problemi di firma (signing) o provisioning?

La risoluzione dei problemi relativi al fallimento del caricamento di un archivio iOS dovuto a errori di signing o provisioning richiede la verifica di tre aree principali: certificati di distribuzione, profili di provisioning ed entitlement del target. 1. Ispezionare i log diagnostici: esaminare i log di distribuzione dell'Organizer di Xcode, gli errori dettagliati di validazione del caricamento o i log della console di CI (`altool`/`notarytool`) per identificare l'esatta causa del rifiuto. 2. Verifica del certificato e dell'identità: verificare che nel Keychain sia presente un certificato Apple Distribution valido insieme alla relativa chiave privata corrispondente, e che non sia scaduto, revocato o privo del certificato intermedio Apple WWDR. 3. Corrispondenza del profilo di provisioning: assicurarsi che il profilo utilizzato per l'esportazione sia un profilo App Store Distribution corrispondente esattamente al Bundle Identifier. Se si utilizzano capability come Push Notifications o Associated Domains, verificare che sia configurato un App ID esplicito anziché un wildcard non compatibile. 4. Allineamento degli entitlement: verificare eventuali discrepanze tra il file `.entitlements` del target e le capability abilitate per l'App ID nell'Apple Developer Portal. Se gli entitlement locali richiedono permessi non registrati nel profilo sul portale, il caricamento fallirà. 5. Configurazione della firma: se si utilizza il signing automatico, verificare il Team selezionato e le credenziali dell'Apple ID nelle impostazioni di Xcode. Se si utilizza il signing manuale (o opzioni di esportazione CI), verificare che `ExportOptions.plist` associ ciascun bundle ID al certificato di distribuzione e al profilo corretti.

# 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
Prova a rispondere a questa domanda con un coach IA

13Come sceglieresti tra BGAppRefreshTask, BGProcessingTask e URLSession in background per una funzionalità di sincronizzazione in background in ambiente di produzione?

La scelta dell'API di background corretta dipende dalla durata dell'operazione, dalla necessità di specifici vincoli di alimentazione/rete e dalla natura del payload di rete: 1. **BGAppRefreshTask**: Ideale per aggiornamenti di contenuto brevi e leggeri (come il refresh di feed di notizie, timeline social o dashboard utente) che richiedono circa 15–30 secondi. Il sistema li pianifica in base ai pattern di utilizzo dell'utente, in modo che i contenuti aggiornati siano pronti poco prima che l'utente apra abitualmente l'app. 2. **BGProcessingTask**: Progettato per operazioni pesanti, non urgenti e di lunga durata come l'indicizzazione dei dati, la pulizia/migrazione del database, l'addestramento di modelli di ML o sincronizzazioni massive di dati. Può essere eseguito per diversi minuti e può richiedere che il dispositivo sia in carica (`requiresExternalPower = true`) e connesso a una rete/Wi-Fi (`requiresNetworkConnectivity = true`), venendo generalmente eseguito durante la notte. 3. **Background URLSession (`URLSessionConfiguration.background`)**: La scelta corretta per il trasferimento di file di grandi dimensioni (caricamento di foto/video o download di bundle di risorse consistenti) che devono proseguire fuori processo anche se l'app viene sospesa o terminata dal sistema operativo. Delega il trasferimento di rete al demone `nsurlsessiond` anziché mantenere attivo in memoria il codice dell'applicazione.

import BackgroundTasks

func scheduleDatabaseMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.db_cleanup")
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true // Run overnight on charger
    request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 3600)
    
    try? BGTaskScheduler.shared.submit(request)
}

func scheduleTimelineRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.feed_refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    try? BGTaskScheduler.shared.submit(request)
}
Prova a rispondere a questa domanda con un coach IA

14Come realizzeresti una pipeline Combine per un campo di testo di ricerca che esegua il debounce dell'input, ignori i duplicati, effettui una richiesta di rete e aggiorni l'UI (User Interface) in modo sicuro?

Per creare una pipeline di ricerca sicura in Combine: 1. **Debounce e deduplicazione:** Prendi il publisher della query di ricerca (ad es. `$queryText`), applica `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` per attendere le pause nella digitazione e `.removeDuplicates()` per ignorare le stringhe di ricerca invariate. 2. **Richiesta di rete e cancellazione:** Mappa ogni query su un publisher di richiesta di rete e appiattisci il flusso usando `.switchToLatest()`. In questo modo si annullano automaticamente le richieste in corso quando arriva una nuova query di ricerca, evitando race condition e risultati obsoleti. 3. **Isolamento degli errori:** Intercetta gli errori di rete all'interno del publisher interno (ad es. `.catch { _ in Just([]) }`), così che eventuali fallimenti di rete non terminino lo stream principale del testo di ricerca. 4. **Distribuzione sul thread principale:** Applica `.receive(on: DispatchQueue.main)` prima di aggiornare lo stato o assegnare i valori alle proprietà dell'interfaccia utente.

import Combine
import Foundation

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var searchResults: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .map { query -> AnyPublisher<[String], Never> in
                guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.performSearch(query)
                    .catch { _ in Just([]) }
                    .eraseToAnyPublisher()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$searchResults)
    }
    
    private func performSearch(_ term: String) -> AnyPublisher<[String], Error> {
        let url = URL(string: "https://api.example.com/search?q=\(term)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
Prova a rispondere a questa domanda con un coach IA

15Come utilizzeresti async/await e la cancellazione dei Task in Swift per implementare una schermata che carica dati dalla rete evitando di aggiornare la UI (User Interface) dopo che l'utente si è allontanato dalla vista?

Definirei il caricamento di rete come una funzione `async` su un servizio o un view model e la eseguirei da un `Task` il cui ciclo di vita sia associato a quello della schermata. In UIKit, ciò comporta in genere la memorizzazione di un riferimento come `var loadTask: Task<Void, Never>?` nel view controller o nel view model, avviandolo quando la schermata deve caricare i dati e cancellandolo in `viewWillDisappear`, `deinit` o prima di avviare una nuova richiesta a seconda del ciclo di vita desiderato. In SwiftUI, preferirei usare il modificatore `.task` o `.task(id:)` quando possibile, poiché SwiftUI ne gestisce automaticamente l'annullamento quando la vista scompare o l'identificatore cambia. All'interno del task, richiamerei l'API di rete asincrona con `try await`, gestirei l'annullamento separatamente dai veri errori e verificherei lo stato di cancellazione prima di applicare i risultati in caso di molteplici `await` o passaggi di elaborazione. Le modifiche allo stato della UI devono avvenire sul main actor, rendendo il view model `@MainActor` o utilizzando `await MainActor.run`. Per evitare aggiornamenti di UI obsoleti, cancellerei i task precedenti, eviterei task detached o globali per operazioni legate alla schermata e, facoltativamente, confronterei l'identificatore della richiesta o l'ID dell'elemento corrente prima di applicare il risultato.

@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)
            }
        }
    }
}
Prova a rispondere a questa domanda con un coach IA

16Come sceglieresti tra UserDefaults, Keychain, file system, SQLite e Core Data in base alle diverse tipologie di dati dell'app?

La scelta del meccanismo di archiviazione appropriato su iOS dipende dalla sensibilità dei dati, dalle dimensioni, dalla complessità relazionale, dai requisiti di query e dal ciclo di vita dei backup: 1. **Keychain**: Per dati sensibili (token di autenticazione, password, chiavi di cifratura, credenziali biometriche). Offre cifratura basata su hardware, persiste dopo la reinstallazione dell'app e rimane sicuro anche in ambienti sandbox compromessi. 2. **UserDefaults**: Per preferenze e flag leggeri e non sensibili (ad esempio `hasCompletedOnboarding`, tema selezionato). Poiché viene caricato interamente in memoria come property list, non deve essere utilizzato per dataset di grandi dimensioni, contenuti multimediali o token sensibili. 3. **File system (Documents / Caches / Application Support)**: Per file distinti e file binari di grandi dimensioni (immagini scaricate, audio, PDF). `Documents` è accessibile all'utente ed è incluso nei backup di iCloud; `Caches` è destinato a contenuti eliminabili e riscaricabili; `Application Support` serve per file persistenti non visibili all'utente. 4. **SQLite**: Per dati strutturati e tabulari che richiedono query indicizzate veloci, operazioni di massa o condivisione del database multipiattaforma senza l'overhead di un grafo a oggetti. 5. **Core Data / SwiftData**: Per grafi di oggetti complessi con relazioni, tracciamento delle modifiche, caricamento su richiesta (faulting), gestione dell'annullamento (undo management) e integrazione diretta con UIKit (`NSFetchedResultsController`) o SwiftUI (`@Query`).

- Auth tokens / API keys          -> Keychain (Hardware-backed encrypted store)
- App settings / UI flags         -> UserDefaults (Lightweight key-value, non-sensitive)
- Downloaded videos / PDFs        -> File System (Documents / Caches directory)
- 50k items with complex queries   -> SQLite / Core Data / SwiftData (Indexed, relationship-aware)
Prova a rispondere a questa domanda con un coach IA

17In una schermata UIKit di produzione con callback provenienti da un view model, come decideresti se catturare self come weak, unowned o strong nei gestori delle closure?

Nell'architettura UIKit in cui un view controller interagisce con un view model tramite callback, la strategia di cattura è determinata dal concetto di ownership e dal ciclo di vita: 1. `[weak self]`: L'approccio standard e più sicuro per closure memorizzate o escaping (come le callback di eventi del view model o gli handler di completamento asincrono di rete/dati). Poiché il view controller possiede il view model, catturare `self` in modo forte (strong) in una callback salvata dal view model crea un ciclo di riferimenti forti (`VC -> VM -> closure -> VC`). `[weak self]` converte `self` in un opzionale (`UIViewController?`), consentendone la corretta deallocazione ed evitando memory leak. 2. `[unowned self]`: Assume che `self` non sarà mai nil nel momento in cui la closure viene eseguita. Va utilizzato con estrema cautela nelle schermate UI. Se un'attività asincrona o una callback si completa dopo che il view controller è stato rimosso e deallocato, accedere a `unowned self` genera un crash irreversibile a runtime. Per questa ragione, `[weak self]` è generalmente preferito a `unowned` nelle callback di UI in UIKit di produzione. 3. **Cattura forte (Strong capture, predefinita, senza capture list):** Adeguata quando la closure è non-escaping (ad esempio operazioni standard su collezioni come `map`/`filter`) oppure quando la closure ha un ciclo di vita breve e non crea un ciclo di ownership verso `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 */ }
}
Prova a rispondere a questa domanda con un coach IA

Domande per esperti

18Ti unisci allo sviluppo di un'app iOS matura con centinaia di schermate, molti massive view controller e cicli di rilascio lenti. Come pianificheresti una strategia di miglioramento architetturale incrementale senza interrompere il rilascio di nuove funzionalità?

Una strategia efficace di migrazione incrementale evita riscritture complete e abbina il refactoring direttamente alla consegna continua delle funzionalità (seguendo il pattern Strangler Fig). Si inizia definendo un modello architetturale di destinazione condiviso (come MVVM o VIPER con dependency injection e coordinator modulari) e stabilendo una baseline di test (test di caratterizzazione, snapshot e unit test) attorno al codice legacy prima di modificarlo. L'ordine di priorità dovrebbe essere guidato dal tasso di modifica (churn) e dal rischio: è preferibile refattorizzare schermate su cui i team lavorano attivamente o aree con alto tasso di bug, piuttosto che codice legacy stabile. Nel refactoring dei Massive View Controller, occorre separare la logica di business e di presentazione in ViewModel o Presenter dedicati, introducendo adapter o coordinator basati su protocolli per isolare la UI legacy dal nuovo codice. Infine, la velocità e la qualità vanno sostenute tramite governance architetturale: fornendo implementazioni di riferimento (golden sample), imponendo limiti mediante linter in CI (Continuous Integration) o linee guida per le PR (Pull Request) e allocando una quota costante per il debito tecnico (ad esempio il 15–20% della capacità o abbinando i refactor al lavoro sulle feature correlate), monitorando costantemente metriche come tempi di build, tassi di crash e copertura dei test.

protocol ProfileDisplaying: AnyObject {
    func updateProfile(name: String, avatarUrl: URL?)
}

extension LegacyProfileViewController: ProfileDisplaying {
    func updateProfile(name: String, avatarUrl: URL?) {
        self.nameLabel.text = name
    }
}

final class ProfilePresenter {
    private weak var view: ProfileDisplaying?
    private let userUseCase: FetchUserUseCaseProtocol
    
    init(view: ProfileDisplaying, userUseCase: FetchUserUseCaseProtocol) {
        self.view = view
        self.userUseCase = userUseCase
    }
    
    func onViewLoaded() async {
        guard let user = try? await userUseCase.execute() else { return }
        await MainActor.run {
            view?.updateProfile(name: user.name, avatarUrl: user.avatarURL)
        }
    }
}
Prova a rispondere a questa domanda con un coach IA

19Devi rilasciare un'applicazione iOS ad alto traffico con una migrazione del database e una modifica alle API (Application Programming Interface) di backend; come progetteresti il piano di rollout per ridurre al minimo l'impatto sugli utenti?

Progettare un piano di rollout per un'app iOS ad alto traffico con migrazioni del database e modifiche alle API di backend richiede il disaccoppiamento tra il deployment e l'attivazione delle funzionalità, visti i vincoli del client iOS (aggiornamenti asincroni da parte degli utenti e impossibilità di forzare rollback dei binari lato client). 1. Compatibilità del backend: implementare una strategia expand-and-contract. Distribuire le API v2 parallelamente alle API v1, in modo che sia le versioni legacy sia quelle aggiornate del client funzionino contemporaneamente senza modifiche che causino rotture (breaking changes). 2. Migrazione resiliente del database: garantire che le migrazioni dello schema locale (ad es. SQLite, Core Data, SwiftData) siano idempotenti, non distruttive e gestiscano in sicurezza i salti di più versioni (ad es. aggiornamento da N-3 a N) senza bloccare il thread principale né innescare cicli di crash all'avvio. 3. Rilascio controllato e dinamico (Dynamic Gating): rilasciare le nuove funzionalità client in modalità "dark" protette da feature flag o configurazioni remote gestite dal server. Mantenere il feature flag disabilitato durante la distribuzione iniziale. 4. Rollout graduale e monitoraggio: distribuire l'app tramite il Phased Release di App Store (un rollout progressivo in 7 giorni). Monitorare costantemente la telemetria, il tasso di crash, le metriche di successo delle migrazioni del database e i tassi di errore delle API. In presenza di anomalie, sospendere il rilascio graduale in App Store Connect e disattivare i feature flag senza dover ricorrere a un rollback di emergenza del binario.

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.
Prova a rispondere a questa domanda con un coach IA

20Stai progettando la sincronizzazione offline-first per un'app di note in cui le modifiche devono convergere tra dispositivi diversi, ma iOS potrebbe posticipare o interrompere i task in background. Quale architettura di esecuzione in background sceglieresti?

Un'architettura di sincronizzazione offline-first considera il database locale (come SQLite, Core Data o SwiftData) come l'unica fonte di verità immediata per lo stato della UI, mentre le mutazioni vengono accodate in una coda outbox persistente su disco, in modo che le modifiche non inviate sopravvivano alla terminazione del processo. Per gestire l'esecuzione in background opportunistica e non deterministica di iOS, il lavoro in background viene articolato su più livelli: 1. Svuotamento dell'outbox al passaggio in background: avviato tramite `UIApplication.shared.beginBackgroundTask(expirationHandler:)` per completare mutazioni in corso o rapide. 2. Sincronizzazione periodica programmata: registrata con `BGTaskScheduler` tramite `BGAppRefreshTask` per sincronizzazioni leggere di metadati/delta e `BGProcessingTask` per operazioni di sincronizzazione più pesanti. 3. Sincronizzazione di asset di grandi dimensioni: gestita fuori processo tramite una `URLSessionConfiguration` in background usando upload e download di file. 4. Sincronizzazione attivata dal server: risvegli opportunistici tramite notifiche push silenziose (`content-available: 1`). Poiché i task in background possono essere interrotti o rischedulati in qualsiasi momento, le operazioni devono utilizzare chiavi di idempotenza generate dal client (ad es. UUID) per consentire retry sicuri senza duplicare dati. Se viene richiamato l'handler `expirationHandler` di un task, il lavoro in corso deve essere annullato immediatamente, le operazioni non confermate devono essere reimpostate su stato in sospeso e deve essere chiamato `setTaskCompleted(success: false)`. La consistenza eventuale viene garantita tramite meccanismi di risoluzione dei conflitti quali CRDT (Conflict-free Replicated Data Types), vettori di versione o last-write-wins con tombstone di cancellazione, assicurando che i delta remoti non sovrascrivano mutazioni locali dell'outbox non ancora confermate.

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()
        }
    }
}
Prova a rispondere a questa domanda con un coach IA