Entrevista iOS Sênior 2026: Perguntas sobre Arquitetura e Padrões de Design

Prepare-se para entrevistas iOS sênior com perguntas-chave sobre MVVM, VIPER, Clean Architecture e padrões de design. Guia completo com exemplos de código Swift.

Diagrama de arquitetura iOS com padrões MVVM e VIPER para entrevista técnica sênior

As entrevistas iOS sênior dão grande ênfase à arquitetura e aos padrões de design. Além da sintaxe Swift, os entrevistadores avaliam a capacidade de projetar aplicativos manuteníveis, testáveis e escaláveis.

Este guia cobre as perguntas mais frequentes sobre MVVM, VIPER, Clean Architecture e padrões essenciais, com respostas detalhadas e exemplos de código prontos para a entrevista.

O que os entrevistadores realmente avaliam

Em entrevistas sênior, a resposta técnica importa menos do que o raciocínio. É sempre necessário explicar por que uma arquitetura se adapta a um determinado contexto, não apenas como implementá-la.

Entendendo as arquiteturas iOS: uma visão geral

Antes de mergulhar em perguntas específicas, é essencial entender o cenário arquitetural do iOS. Cada padrão resolve problemas diferentes e se adapta a contextos distintos.

MVC: o padrão histórico da Apple

MVC (Model-View-Controller) continua sendo o padrão padrão da Apple, mas sofre com o problema dos "Massive View Controllers" em aplicativos complexos.

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()
    }
}

Esse padrão se torna problemático quando os ViewControllers ultrapassam 500 linhas, tornando os testes unitários praticamente impossíveis.

Pergunta 1: Explique MVVM e sua implementação em Swift

MVVM (Model-View-ViewModel) separa a lógica de apresentação em uma ViewModel, facilitando os testes e reduzindo o tamanho do ViewController.

Ponto-chave em entrevistas

MVVM brilha com SwiftUI graças ao @Observable e ao data binding nativo. Com UIKit, é necessário um mecanismo de binding (Combine, closures).

Implementação MVVM com 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)
    }
}

A View com 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() }
    }
}

A ViewModel não conhece nem UIKit nem SwiftUI, o que a torna totalmente testável de forma unitária.

Pergunta 2: Quando escolher VIPER em vez de MVVM?

VIPER (View-Interactor-Presenter-Entity-Router) se adapta a aplicativos complexos que exigem separação rigorosa de responsabilidades e navegação avançada.

Estrutura completa do 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)
}

O Presenter orquestra a 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)
    }
}
Aviso para entrevistas

VIPER introduz um boilerplate considerável. Seu uso deve ser justificado pelo tamanho da equipe (vários devs em um mesmo módulo) ou pela complexidade do domínio de negócio.

Pergunta 3: Como implementar Clean Architecture no iOS?

Clean Architecture organiza o código em círculos concêntricos, com as regras de negócio no centro, independentes dos frameworks.

Estrutura em camadas

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 }
    }
}

A camada de dados com o padrão 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()
        }
    }
}

Essa organização permite testar cada camada de forma independente e trocar implementações (por exemplo, migrar do CoreData para o SwiftData) sem tocar no domínio.

Pronto para mandar bem nas entrevistas de iOS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Pergunta 4: Quais padrões de design você usa diariamente?

Os entrevistadores esperam domínio prático dos padrões, não recitação teórica. Estes são os mais frequentes no iOS.

Injeção de Dependência com 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)
    }
}

Padrão Coordinator para a navegação

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()
    }
}

Esse padrão remove a lógica de navegação dos ViewControllers, tornando-os mais leves e reutilizáveis.

Pergunta 5: Como gerenciar a comunicação entre módulos?

A comunicação entre módulos é crucial em aplicativos grandes. Existem várias abordagens dependendo do acoplamento desejado.

Comunicação baseada em 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)
    }
}

Comunicação orientada a eventos com 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
        }
    }
}
Dica para entrevistas

Mencione que a escolha entre acoplamento forte (protocolos) e acoplamento fraco (eventos) depende do contexto: eventos se adaptam a notificações globais, protocolos a interações diretas.

Pergunta 6: Como estruturar testes em uma arquitetura modular?

A testabilidade é um critério importante para vagas sênior. Uma boa arquitetura facilita os testes em todos os níveis.

Teste unitário da 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()
    }
}

Teste de integração do 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)
    }
}

Pergunta 7: Como lidar com estados de UI complexos?

O gerenciamento de estado é crítico em aplicativos sênior. Uma abordagem estruturada evita bugs e simplifica a depuração.

Máquina de estados com 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)
        }
    }
}

Essa abordagem torna impossíveis os estados inconsistentes (por exemplo, isLoading = true com um erro exibido).

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Conclusão

As entrevistas iOS sênior avaliam a capacidade de escolher e justificar uma arquitetura adequada ao contexto. Pontos-chave:

Checklist de arquitetura iOS Sênior:

✅ MVVM para apps de tamanho médio com SwiftUI ou UIKit + Combine ✅ VIPER para equipes grandes e domínios de negócio complexos ✅ Clean Architecture para independência do framework ✅ Injeção de Dependência sistemática para testabilidade ✅ Padrão Coordinator para desacoplar a navegação ✅ Máquinas de estado para fluxos complexos ✅ Testes em todos os níveis: unitários, integração, UI

Em entrevistas, deve-se demonstrar:

  • Compreensão dos trade-offs (MVVM simples vs VIPER estruturado)
  • Experiência prática com exemplos de projetos reais
  • Capacidade de adaptar a arquitetura ao contexto (tamanho da equipe, complexidade)
  • Domínio dos testes como critério de qualidade arquitetural

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

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

Compartilhar

Artigos relacionados