# Colloquio iOS Senior 2026: Domande su Architettura e Design Pattern > Preparazione ai colloqui iOS senior con domande chiave su MVVM, VIPER, Clean Architecture e design pattern. Guida completa con esempi di codice Swift. - Published: 2026-03-02 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, swift, architecture, mvvm, viper, interview, design-patterns - Reading time: 12 min --- I colloqui iOS senior pongono grande enfasi su architettura e design pattern. Oltre alla sintassi Swift, gli intervistatori valutano la capacità di progettare applicazioni manutenibili, testabili e scalabili. Questa guida copre le domande più frequenti su MVVM, VIPER, Clean Architecture e i pattern essenziali, con risposte dettagliate ed esempi di codice pronti per il colloquio. > **Cosa valutano davvero gli intervistatori** > > Nei colloqui senior, la risposta tecnica conta meno del ragionamento. Bisogna sempre spiegare **perché** un'architettura si adatta a un determinato contesto, non solo **come** implementarla. ## Comprendere le architetture iOS: una panoramica Prima di affrontare le domande specifiche, è essenziale capire il panorama architetturale iOS. Ogni pattern risolve problemi diversi e si adatta a contesti diversi. ### MVC: il pattern storico di Apple MVC (Model-View-Controller) resta il pattern di default di Apple, ma soffre del problema dei "Massive View Controllers" nelle applicazioni complesse. ```swift // UserViewController.swift // Typical MVC example showing its limitations class UserViewController: UIViewController { // The ViewController accumulates too many responsibilities private var users: [User] = [] override func viewDidLoad() { super.viewDidLoad() // View logic setupUI() // Business logic fetchUsers() // Navigation logic setupNavigationBar() } private func fetchUsers() { // Networking in the VC: anti-pattern URLSession.shared.dataTask(with: URL(string: "api/users")!) { data, _, _ in // JSON parsing here too... }.resume() } } ``` Questo pattern diventa problematico quando i ViewController superano le 500 righe, rendendo i test unitari quasi impossibili. ## Domanda 1: Spiega MVVM e la sua implementazione in Swift MVVM (Model-View-ViewModel) separa la logica di presentazione in un ViewModel, facilitando i test e riducendo la dimensione del ViewController. > **Punto chiave del colloquio** > > MVVM brilla con SwiftUI grazie a `@Observable` e al data binding nativo. Con UIKit serve un meccanismo di binding (Combine, closure). ### Implementazione MVVM con Combine ```swift // UserViewModel.swift // ViewModel separated from any UIKit dependency import Combine @MainActor final class UserViewModel: ObservableObject { // Published states for binding @Published private(set) var users: [User] = [] @Published private(set) var isLoading = false @Published private(set) var errorMessage: String? // Injected dependency for testability private let userRepository: UserRepositoryProtocol private var cancellables = Set() init(userRepository: UserRepositoryProtocol = UserRepository()) { self.userRepository = userRepository } func loadUsers() { isLoading = true errorMessage = nil userRepository.fetchUsers() .receive(on: DispatchQueue.main) .sink { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } } receiveValue: { [weak self] users in self?.users = users } .store(in: &cancellables) } } ``` ### La View con binding Combine ```swift // UserListView.swift // SwiftUI View consuming the ViewModel import SwiftUI struct UserListView: View { // StateObject for lifecycle management @StateObject private var viewModel = UserViewModel() var body: some View { Group { if viewModel.isLoading { ProgressView("Loading...") } else if let error = viewModel.errorMessage { ErrorView(message: error, retry: viewModel.loadUsers) } else { List(viewModel.users) { user in UserRowView(user: user) } } } .onAppear { viewModel.loadUsers() } } } ``` Il ViewModel non conosce né UIKit né SwiftUI, il che lo rende interamente testabile a livello unitario. ## Domanda 2: Quando scegliere VIPER al posto di MVVM? VIPER (View-Interactor-Presenter-Entity-Router) si adatta ad applicazioni complesse che richiedono una rigida separazione delle responsabilità e una navigazione avanzata. ### Struttura completa di VIPER ```swift // UserListProtocols.swift // Contract definitions between VIPER components protocol UserListViewProtocol: AnyObject { var presenter: UserListPresenterProtocol? { get set } func showUsers(_ users: [UserViewModel]) func showError(_ message: String) func showLoading() } protocol UserListPresenterProtocol: AnyObject { var view: UserListViewProtocol? { get set } var interactor: UserListInteractorInputProtocol? { get set } var router: UserListRouterProtocol? { get set } func viewDidLoad() func didSelectUser(_ user: UserViewModel) } protocol UserListInteractorInputProtocol: AnyObject { var presenter: UserListInteractorOutputProtocol? { get set } func fetchUsers() } protocol UserListInteractorOutputProtocol: AnyObject { func didFetchUsers(_ users: [User]) func didFailWithError(_ error: Error) } protocol UserListRouterProtocol: AnyObject { func navigateToUserDetail(with userId: String) } ``` ### Il Presenter orchestra la logica ```swift // UserListPresenter.swift // The Presenter bridges View and Interactor final class UserListPresenter: UserListPresenterProtocol { weak var view: UserListViewProtocol? var interactor: UserListInteractorInputProtocol? var router: UserListRouterProtocol? func viewDidLoad() { view?.showLoading() interactor?.fetchUsers() } func didSelectUser(_ user: UserViewModel) { router?.navigateToUserDetail(with: user.id) } } // Extension for Interactor callbacks extension UserListPresenter: UserListInteractorOutputProtocol { func didFetchUsers(_ users: [User]) { // Model -> ViewModel transformation let viewModels = users.map { UserViewModel(from: $0) } view?.showUsers(viewModels) } func didFailWithError(_ error: Error) { view?.showError(error.localizedDescription) } } ``` > **Avvertenza durante il colloquio** > > VIPER introduce un boilerplate significativo. L'utilizzo va giustificato con la dimensione del team (più sviluppatori sullo stesso modulo) o la complessità del dominio di business. ## Domanda 3: Come si implementa Clean Architecture su iOS? Clean Architecture organizza il codice in cerchi concentrici, con le regole di business al centro, indipendenti dai framework. ### Struttura a livelli ```swift // Domain/Entities/User.swift // Pure business entity, no framework dependencies struct User: Identifiable, Equatable { let id: String let email: String let fullName: String let subscriptionLevel: SubscriptionLevel enum SubscriptionLevel: String { case free, premium, enterprise } } // Domain/UseCases/GetUsersUseCase.swift // Use Case encapsulating a business rule protocol GetUsersUseCaseProtocol { func execute() async throws -> [User] } final class GetUsersUseCase: GetUsersUseCaseProtocol { // Dependency on abstraction, not implementation private let repository: UserRepositoryProtocol init(repository: UserRepositoryProtocol) { self.repository = repository } func execute() async throws -> [User] { let users = try await repository.fetchAll() // Business rule: sort by subscription level return users.sorted { $0.subscriptionLevel.rawValue > $1.subscriptionLevel.rawValue } } } ``` ### Il livello dati con il pattern Repository ```swift // Data/Repositories/UserRepository.swift // Concrete repository implementation final class UserRepository: UserRepositoryProtocol { private let remoteDataSource: UserRemoteDataSourceProtocol private let localDataSource: UserLocalDataSourceProtocol init( remoteDataSource: UserRemoteDataSourceProtocol = UserRemoteDataSource(), localDataSource: UserLocalDataSourceProtocol = UserLocalDataSource() ) { self.remoteDataSource = remoteDataSource self.localDataSource = localDataSource } func fetchAll() async throws -> [User] { do { // Cache-first strategy with fallback let remoteUsers = try await remoteDataSource.fetchUsers() await localDataSource.save(remoteUsers) return remoteUsers } catch { // Fallback to local cache return try await localDataSource.fetchUsers() } } } ``` Questa organizzazione consente di testare ogni livello in modo indipendente e di cambiare implementazioni (per esempio migrare da CoreData a SwiftData) senza toccare il dominio. ## Domanda 4: Quali design pattern usi quotidianamente? Gli intervistatori si aspettano padronanza pratica dei pattern, non recitazione teorica. Ecco i più frequenti su iOS. ### Dependency Injection con Property Wrappers ```swift // DependencyInjection/Container.swift // Simple and effective injection container final class DIContainer { static let shared = DIContainer() private var factories: [String: () -> Any] = [:] func register(_ type: T.Type, factory: @escaping () -> T) { let key = String(describing: type) factories[key] = factory } func resolve(_ type: T.Type) -> T { let key = String(describing: type) guard let factory = factories[key], let instance = factory() as? T else { fatalError("No registration for \(key)") } return instance } } // Property Wrapper for elegant injection @propertyWrapper struct Injected { private var value: T init() { self.value = DIContainer.shared.resolve(T.self) } var wrappedValue: T { get { value } mutating set { value = newValue } } } // Usage in a ViewModel final class PaymentViewModel { @Injected private var paymentService: PaymentServiceProtocol @Injected private var analyticsService: AnalyticsServiceProtocol func processPayment(_ amount: Decimal) async throws { analyticsService.track(.paymentInitiated(amount: amount)) try await paymentService.charge(amount) } } ``` ### Pattern Coordinator per la navigazione ```swift // Coordinators/AppCoordinator.swift // Coordinator managing navigation flow protocol Coordinator: AnyObject { var childCoordinators: [Coordinator] { get set } var navigationController: UINavigationController { get } func start() } final class AppCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] let navigationController: UINavigationController private let window: UIWindow init(window: UIWindow) { self.window = window self.navigationController = UINavigationController() } func start() { window.rootViewController = navigationController window.makeKeyAndVisible() // Flow decision based on state if AuthManager.shared.isAuthenticated { showMainFlow() } else { showAuthFlow() } } private func showAuthFlow() { let authCoordinator = AuthCoordinator(navigationController: navigationController) authCoordinator.delegate = self childCoordinators.append(authCoordinator) authCoordinator.start() } private func showMainFlow() { let mainCoordinator = MainCoordinator(navigationController: navigationController) childCoordinators.append(mainCoordinator) mainCoordinator.start() } } ``` Questo pattern sposta la logica di navigazione fuori dai ViewController, rendendoli più leggeri e riutilizzabili. ## Domanda 5: Come si gestisce la comunicazione tra moduli? La comunicazione tra moduli è cruciale nelle applicazioni di grandi dimensioni. Esistono diversi approcci a seconda dell'accoppiamento desiderato. ### Comunicazione basata su protocolli ```swift // Modules/Shared/ModuleProtocols.swift // Public contracts exposed by each module protocol PaymentModuleProtocol { func startPaymentFlow(for productId: String, completion: @escaping (Result) -> Void) } protocol UserModuleProtocol { func getCurrentUser() -> User? func updateProfile(_ profile: ProfileUpdate) async throws } // Modules/Payment/PaymentModule.swift // Internal module implementation final class PaymentModule: PaymentModuleProtocol { static let shared: PaymentModuleProtocol = PaymentModule() private let paymentService: PaymentService private init() { self.paymentService = PaymentService() } func startPaymentFlow(for productId: String, completion: @escaping (Result) -> Void) { // Module-internal logic paymentService.process(productId: productId, completion: completion) } } ``` ### Comunicazione orientata agli eventi con Combine ```swift // EventBus/AppEventBus.swift // Decoupled event bus for async communication enum AppEvent { case userDidLogin(User) case userDidLogout case purchaseCompleted(Receipt) case subscriptionChanged(SubscriptionLevel) } final class AppEventBus { static let shared = AppEventBus() // Private Subject, public Publisher private let eventSubject = PassthroughSubject() var events: AnyPublisher { eventSubject.eraseToAnyPublisher() } func send(_ event: AppEvent) { eventSubject.send(event) } } // Listening in any module final class AnalyticsModule { private var cancellables = Set() init() { AppEventBus.shared.events .sink { [weak self] event in self?.handleEvent(event) } .store(in: &cancellables) } private func handleEvent(_ event: AppEvent) { switch event { case .purchaseCompleted(let receipt): trackPurchase(receipt) case .userDidLogin(let user): identifyUser(user) default: break } } } ``` > **Suggerimento per il colloquio** > > Menzionare che la scelta tra accoppiamento forte (protocolli) e accoppiamento debole (eventi) dipende dal contesto: gli eventi si adattano alle notifiche globali, i protocolli alle interazioni dirette. ## Domanda 6: Come si strutturano i test in un'architettura modulare? La testabilità è un criterio fondamentale per le posizioni senior. Una buona architettura facilita i test a tutti i livelli. ### Test unitario del ViewModel ```swift // Tests/UserViewModelTests.swift // Unit tests with injected mocks import XCTest @testable import MyApp final class UserViewModelTests: XCTestCase { private var sut: UserViewModel! private var mockRepository: MockUserRepository! override func setUp() { super.setUp() mockRepository = MockUserRepository() sut = UserViewModel(userRepository: mockRepository) } override func tearDown() { sut = nil mockRepository = nil super.tearDown() } func test_loadUsers_success_updatesUsersArray() async { // Given let expectedUsers = [User.mock(), User.mock()] mockRepository.stubbedUsers = expectedUsers // When await sut.loadUsers() // Then XCTAssertEqual(sut.users.count, 2) XCTAssertFalse(sut.isLoading) XCTAssertNil(sut.errorMessage) } func test_loadUsers_failure_setsErrorMessage() async { // Given mockRepository.stubbedError = NetworkError.noConnection // When await sut.loadUsers() // Then XCTAssertTrue(sut.users.isEmpty) XCTAssertNotNil(sut.errorMessage) } } // Mocks/MockUserRepository.swift final class MockUserRepository: UserRepositoryProtocol { var stubbedUsers: [User] = [] var stubbedError: Error? var fetchUsersCalled = false func fetchUsers() -> AnyPublisher<[User], Error> { fetchUsersCalled = true if let error = stubbedError { return Fail(error: error).eraseToAnyPublisher() } return Just(stubbedUsers) .setFailureType(to: Error.self) .eraseToAnyPublisher() } } ``` ### Test di integrazione del Use Case ```swift // Tests/GetUsersUseCaseTests.swift // Integration test verifying business logic final class GetUsersUseCaseTests: XCTestCase { func test_execute_sortsUsersBySubscriptionLevel() async throws { // Given let freeUser = User(id: "1", email: "free@test.com", fullName: "Free", subscriptionLevel: .free) let premiumUser = User(id: "2", email: "premium@test.com", fullName: "Premium", subscriptionLevel: .premium) let mockRepo = MockUserRepository() mockRepo.stubbedUsers = [freeUser, premiumUser] let sut = GetUsersUseCase(repository: mockRepo) // When let result = try await sut.execute() // Then - Premium should be first XCTAssertEqual(result.first?.subscriptionLevel, .premium) XCTAssertEqual(result.last?.subscriptionLevel, .free) } } ``` ## Domanda 7: Come gestire stati di UI complessi? La gestione dello stato è critica nelle applicazioni senior. Un approccio strutturato evita bug e semplifica il debug. ### Macchina a stati con Enum ```swift // ViewModels/CheckoutViewModel.swift // State machine for payment flow enum CheckoutState: Equatable { case idle case loadingCart case cartLoaded(CartSummary) case processingPayment case paymentSucceeded(Receipt) case paymentFailed(PaymentError) var isLoading: Bool { switch self { case .loadingCart, .processingPayment: return true default: return false } } } @MainActor final class CheckoutViewModel: ObservableObject { @Published private(set) var state: CheckoutState = .idle private let cartService: CartServiceProtocol private let paymentService: PaymentServiceProtocol init(cartService: CartServiceProtocol, paymentService: PaymentServiceProtocol) { self.cartService = cartService self.paymentService = paymentService } func loadCart() async { state = .loadingCart do { let summary = try await cartService.getSummary() state = .cartLoaded(summary) } catch { state = .paymentFailed(.cartLoadFailed) } } func confirmPayment() async { guard case .cartLoaded(let summary) = state else { return } state = .processingPayment do { let receipt = try await paymentService.charge(summary.total) state = .paymentSucceeded(receipt) } catch let error as PaymentError { state = .paymentFailed(error) } catch { state = .paymentFailed(.unknown) } } } ``` Questo approccio rende impossibili gli stati incoerenti (per esempio `isLoading = true` con un errore visualizzato). ## Conclusione I colloqui iOS senior valutano la capacità di scegliere e giustificare un'architettura adatta al contesto. Punti chiave: **Checklist architettura iOS Senior:** ✅ MVVM per app di medie dimensioni con SwiftUI o UIKit + Combine ✅ VIPER per team grandi e domini di business complessi ✅ Clean Architecture per l'indipendenza dal framework ✅ Dependency Injection sistematica per la testabilità ✅ Pattern Coordinator per disaccoppiare la navigazione ✅ Macchine a stati per i flussi complessi ✅ Test a ogni livello: unitari, integrazione, UI **Nei colloqui occorre dimostrare:** - Comprensione dei trade-off (MVVM semplice vs VIPER strutturato) - Esperienza pratica con esempi di progetti reali - Capacità di adattare l'architettura al contesto (dimensione del team, complessità) - Padronanza dei test come criterio di qualità architetturale --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/ios/ios-senior-interview-architecture-design-patterns