Phỏng vấn iOS Senior 2026: Câu hỏi về Kiến trúc và Design Pattern

Chuẩn bị phỏng vấn iOS senior với các câu hỏi quan trọng về MVVM, VIPER, Clean Architecture và design pattern. Hướng dẫn đầy đủ với ví dụ mã Swift.

Sơ đồ kiến trúc iOS với pattern MVVM và VIPER cho phỏng vấn kỹ thuật senior

Các buổi phỏng vấn iOS senior đặt nặng vào kiến trúc và design pattern. Vượt ra ngoài cú pháp Swift, người phỏng vấn đánh giá khả năng thiết kế ứng dụng dễ bảo trì, dễ kiểm thử và có khả năng mở rộng.

Hướng dẫn này bao gồm các câu hỏi thường gặp nhất về MVVM, VIPER, Clean Architecture và các pattern thiết yếu, cùng với câu trả lời chi tiết và ví dụ mã sẵn sàng cho phỏng vấn.

Điều người phỏng vấn thực sự đánh giá

Trong phỏng vấn senior, câu trả lời kỹ thuật ít quan trọng hơn so với lập luận. Hãy luôn giải thích tại sao một kiến trúc phù hợp với bối cảnh nhất định, chứ không chỉ cách triển khai nó.

Hiểu các kiến trúc iOS: tổng quan

Trước khi đi vào các câu hỏi cụ thể, việc hiểu bức tranh kiến trúc của iOS là điều cần thiết. Mỗi pattern giải quyết các vấn đề khác nhau và phù hợp với những bối cảnh khác nhau.

MVC: pattern lịch sử của Apple

MVC (Model-View-Controller) vẫn là pattern mặc định của Apple, nhưng gặp vấn đề "Massive View Controllers" trong các ứng dụng phức tạp.

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

Pattern này trở nên có vấn đề khi ViewController vượt quá 500 dòng, khiến unit test gần như bất khả thi.

Câu hỏi 1: Giải thích MVVM và cách triển khai trong Swift

MVVM (Model-View-ViewModel) tách logic trình bày sang một ViewModel, giúp việc kiểm thử dễ dàng hơn và giảm kích thước của ViewController.

Điểm quan trọng trong phỏng vấn

MVVM tỏa sáng cùng SwiftUI nhờ @Observable và data binding tích hợp sẵn. Với UIKit, cần một cơ chế binding (Combine, closure).

Triển khai MVVM với 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)
    }
}

View với 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() }
    }
}

ViewModel không biết đến UIKit hay SwiftUI, nhờ vậy hoàn toàn có thể được kiểm thử bằng unit test.

Câu hỏi 2: Khi nào nên chọn VIPER thay vì MVVM?

VIPER (View-Interactor-Presenter-Entity-Router) phù hợp với ứng dụng phức tạp đòi hỏi tách biệt nghiêm ngặt các trách nhiệm và điều hướng nâng cao.

Cấu trúc VIPER đầy đủ

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

Presenter điều phối logic

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)
    }
}
Lưu ý khi phỏng vấn

VIPER tạo ra một lượng boilerplate đáng kể. Việc sử dụng cần được biện minh bằng quy mô đội (nhiều dev cùng làm trên một module) hoặc độ phức tạp của domain nghiệp vụ.

Câu hỏi 3: Làm thế nào để triển khai Clean Architecture trên iOS?

Clean Architecture sắp xếp mã thành các vòng tròn đồng tâm, với các quy tắc nghiệp vụ ở trung tâm, độc lập với framework.

Cấu trúc các tầng

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

Tầng dữ liệu với pattern 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()
        }
    }
}

Cách tổ chức này cho phép kiểm thử từng tầng độc lập và thay đổi triển khai (ví dụ: chuyển từ CoreData sang SwiftData) mà không ảnh hưởng đến domain.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Câu hỏi 4: Bạn sử dụng những design pattern nào hằng ngày?

Người phỏng vấn mong đợi sự thông thạo thực tế, không phải đọc lý thuyết. Đây là những pattern phổ biến nhất trên iOS.

Dependency Injection với 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)
    }
}

Pattern Coordinator cho điều hướng

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

Pattern này đưa logic điều hướng ra khỏi ViewController, giúp chúng nhẹ hơn và dễ tái sử dụng.

Câu hỏi 5: Làm thế nào để xử lý giao tiếp giữa các module?

Giao tiếp giữa các module là yếu tố quan trọng trong các ứng dụng lớn. Có nhiều cách tiếp cận tùy mức độ ghép cặp mong muốn.

Giao tiếp dựa trên protocol

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

Giao tiếp hướng sự kiện với 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
        }
    }
}
Mẹo phỏng vấn

Nên đề cập rằng việc lựa chọn giữa ghép cặp chặt (protocol) và ghép cặp lỏng (event) phụ thuộc vào bối cảnh: event phù hợp với thông báo toàn cục, protocol phù hợp với tương tác trực tiếp.

Câu hỏi 6: Làm thế nào để tổ chức kiểm thử trong kiến trúc module?

Khả năng kiểm thử là tiêu chí quan trọng đối với vị trí senior. Kiến trúc tốt giúp việc kiểm thử dễ dàng ở mọi cấp độ.

Unit test cho 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()
    }
}

Tích hợp 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)
    }
}

Câu hỏi 7: Làm thế nào để xử lý các trạng thái UI phức tạp?

Quản lý trạng thái là yếu tố then chốt trong các ứng dụng cấp senior. Một cách tiếp cận có cấu trúc giúp tránh bug và đơn giản hóa quá trình debug.

State machine với 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)
        }
    }
}

Cách tiếp cận này khiến các trạng thái không nhất quán trở thành điều bất khả thi (ví dụ isLoading = true đồng thời hiển thị lỗi).

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Kết luận

Phỏng vấn iOS senior đánh giá khả năng chọn và biện minh cho một kiến trúc phù hợp với bối cảnh. Các điểm chính:

Checklist kiến trúc iOS Senior:

✅ MVVM cho ứng dụng cỡ trung với SwiftUI hoặc UIKit + Combine ✅ VIPER cho đội lớn và domain nghiệp vụ phức tạp ✅ Clean Architecture cho sự độc lập với framework ✅ Dependency Injection có hệ thống để bảo đảm khả năng kiểm thử ✅ Pattern Coordinator để tách rời điều hướng ✅ State machine cho các luồng phức tạp ✅ Kiểm thử ở mọi cấp độ: unit, tích hợp, UI

Trong phỏng vấn cần thể hiện:

  • Hiểu các trade-off (MVVM đơn giản so với VIPER có cấu trúc)
  • Kinh nghiệm thực tế với ví dụ từ dự án thực
  • Khả năng điều chỉnh kiến trúc theo bối cảnh (quy mô đội, độ phức tạp)
  • Sự thông thạo về kiểm thử như tiêu chí chất lượng kiến trúc

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan