Entrevista iOS Senior 2026: Preguntas de Arquitectura y Patrones de Diseño

Prepárate para entrevistas iOS senior con preguntas clave sobre MVVM, VIPER, Clean Architecture y patrones de diseño. Guía completa con ejemplos de código Swift.

Diagrama de arquitectura iOS con patrones MVVM y VIPER para entrevista técnica senior

Las entrevistas iOS senior ponen un fuerte énfasis en la arquitectura y los patrones de diseño. Más allá de la sintaxis Swift, los entrevistadores evalúan la capacidad de diseñar aplicaciones mantenibles, testeables y escalables.

Esta guía cubre las preguntas más frecuentes sobre MVVM, VIPER, Clean Architecture y patrones esenciales, con respuestas detalladas y ejemplos de código listos para entrevistas.

Lo que realmente evalúan los entrevistadores

En las entrevistas senior, la respuesta técnica importa menos que el razonamiento. Siempre hay que explicar por qué una arquitectura encaja en un contexto dado, no solo cómo implementarla.

Comprender las arquitecturas iOS: una visión general

Antes de profundizar en preguntas específicas, es esencial entender el panorama arquitectónico de iOS. Cada patrón resuelve diferentes problemas y se adapta a distintos contextos.

MVC: el patrón histórico de Apple

MVC (Model-View-Controller) sigue siendo el patrón por defecto de Apple, pero sufre el problema de los "Massive View Controllers" en aplicaciones complejas.

UserViewController.swiftswift
// 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()
    }
}

Este patrón se vuelve problemático cuando los ViewControllers superan las 500 líneas, haciendo que las pruebas unitarias sean prácticamente imposibles.

Pregunta 1: Explica MVVM y su implementación en Swift

MVVM (Model-View-ViewModel) separa la lógica de presentación en un ViewModel, facilitando las pruebas y reduciendo el tamaño del ViewController.

Punto clave en entrevistas

MVVM brilla con SwiftUI gracias a @Observable y al data binding nativo. Con UIKit, se requiere un mecanismo de binding (Combine, closures).

Implementación de MVVM con Combine

UserViewModel.swiftswift
// 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<AnyCancellable>()

    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

UserListView.swiftswift
// 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() }
    }
}

El ViewModel no conoce ni UIKit ni SwiftUI, lo que lo hace completamente testeable de forma unitaria.

Pregunta 2: ¿Cuándo elegir VIPER en lugar de MVVM?

VIPER (View-Interactor-Presenter-Entity-Router) se adapta a aplicaciones complejas que requieren una separación estricta de responsabilidades y una navegación avanzada.

Estructura completa de VIPER

UserListProtocols.swiftswift
// 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)
}

El Presenter orquesta la lógica

UserListPresenter.swiftswift
// 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)
    }
}
Advertencia en entrevistas

VIPER introduce un boilerplate considerable. Su uso debe justificarse por el tamaño del equipo (varios desarrolladores en un mismo módulo) o por la complejidad del dominio de negocio.

Pregunta 3: ¿Cómo se implementa Clean Architecture en iOS?

Clean Architecture organiza el código en círculos concéntricos, con las reglas de negocio en el centro, independientes de los frameworks.

Estructura por capas

Domain/Entities/User.swiftswift
// 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 }
    }
}

La capa de datos con el patrón Repository

Data/Repositories/UserRepository.swiftswift
// 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()
        }
    }
}

Esta organización permite testear cada capa de forma independiente y cambiar implementaciones (por ejemplo, migrar de CoreData a SwiftData) sin tocar el dominio.

¿Listo para aprobar tus entrevistas de iOS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Pregunta 4: ¿Qué patrones de diseño usas a diario?

Los entrevistadores esperan dominio práctico de los patrones, no recitación teórica. Estos son los más frecuentes en iOS.

Inyección de dependencias con Property Wrappers

DependencyInjection/Container.swiftswift
// Simple and effective injection container
final class DIContainer {
    static let shared = DIContainer()

    private var factories: [String: () -> Any] = [:]

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        let key = String(describing: type)
        factories[key] = factory
    }

    func resolve<T>(_ 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<T> {
    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)
    }
}

Patrón Coordinator para la navegación

Coordinators/AppCoordinator.swiftswift
// 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()
    }
}

Este patrón saca la lógica de navegación de los ViewControllers, haciéndolos más ligeros y reutilizables.

Pregunta 5: ¿Cómo se maneja la comunicación entre módulos?

La comunicación entre módulos es crucial en aplicaciones grandes. Existen varios enfoques según el acoplamiento deseado.

Comunicación basada en protocolos

Modules/Shared/ModuleProtocols.swiftswift
// Public contracts exposed by each module
protocol PaymentModuleProtocol {
    func startPaymentFlow(for productId: String, completion: @escaping (Result<Receipt, PaymentError>) -> 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<Receipt, PaymentError>) -> Void) {
        // Module-internal logic
        paymentService.process(productId: productId, completion: completion)
    }
}

Comunicación orientada a eventos con Combine

EventBus/AppEventBus.swiftswift
// 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<AppEvent, Never>()
    var events: AnyPublisher<AppEvent, Never> {
        eventSubject.eraseToAnyPublisher()
    }

    func send(_ event: AppEvent) {
        eventSubject.send(event)
    }
}

// Listening in any module
final class AnalyticsModule {
    private var cancellables = Set<AnyCancellable>()

    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
        }
    }
}
Consejo para entrevistas

Menciona que la elección entre acoplamiento fuerte (protocolos) y acoplamiento débil (eventos) depende del contexto: los eventos se adaptan a notificaciones globales, los protocolos a interacciones directas.

Pregunta 6: ¿Cómo estructuras los tests en una arquitectura modular?

La testabilidad es un criterio clave para puestos senior. Una buena arquitectura facilita las pruebas en todos los niveles.

Test unitario del ViewModel

Tests/UserViewModelTests.swiftswift
// 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 de integración del Use Case

Tests/GetUsersUseCaseTests.swiftswift
// 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)
    }
}

Pregunta 7: ¿Cómo manejar estados de UI complejos?

La gestión de estados es crítica en aplicaciones senior. Un enfoque estructurado evita bugs y simplifica la depuración.

Máquina de estados con Enum

ViewModels/CheckoutViewModel.swiftswift
// 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)
        }
    }
}

Este enfoque hace imposibles los estados inconsistentes (por ejemplo, isLoading = true con un error mostrado).

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Conclusión

Las entrevistas iOS senior evalúan la capacidad de elegir y justificar una arquitectura adecuada al contexto. Conclusiones clave:

Checklist de arquitectura iOS Senior:

✅ MVVM para apps de tamaño medio con SwiftUI o UIKit + Combine ✅ VIPER para equipos grandes y dominios de negocio complejos ✅ Clean Architecture para independencia del framework ✅ Inyección de dependencias sistemática para la testabilidad ✅ Patrón Coordinator para desacoplar la navegación ✅ Máquinas de estado para flujos complejos ✅ Tests en cada nivel: unitarios, integración, UI

En entrevistas, se debe demostrar:

  • Comprensión de los trade-offs (MVVM simple vs VIPER estructurado)
  • Experiencia práctica con ejemplos de proyectos reales
  • Capacidad de adaptar la arquitectura al contexto (tamaño del equipo, complejidad)
  • Dominio del testing como criterio de calidad arquitectónica

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#ios
#swift
#architecture
#mvvm
#viper
#interview
#design-patterns

Compartir

Artículos relacionados