# สัมภาษณ์ iOS Senior 2026: คำถามเรื่องสถาปัตยกรรมและ Design Pattern > เตรียมตัวสัมภาษณ์ iOS senior ด้วยคำถามสำคัญเกี่ยวกับ MVVM, VIPER, Clean Architecture และ design pattern คู่มือครบถ้วนพร้อมตัวอย่างโค้ด Swift - Published: 2026-03-02 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, swift, architecture, mvvm, viper, interview, design-patterns - Reading time: 12 min --- การสัมภาษณ์ iOS senior ให้ความสำคัญอย่างมากกับสถาปัตยกรรมและ design pattern นอกจากไวยากรณ์ของ Swift แล้ว ผู้สัมภาษณ์ยังประเมินความสามารถในการออกแบบแอปพลิเคชันที่บำรุงรักษาง่าย ทดสอบได้ และขยายตัวได้ คู่มือนี้ครอบคลุมคำถามที่พบบ่อยที่สุดเกี่ยวกับ MVVM, VIPER, Clean Architecture และ pattern ที่จำเป็น พร้อมคำตอบโดยละเอียดและตัวอย่างโค้ดที่พร้อมใช้งานในการสัมภาษณ์ > **สิ่งที่ผู้สัมภาษณ์ประเมินจริง ๆ** > > ในการสัมภาษณ์ระดับ senior คำตอบทางเทคนิคสำคัญน้อยกว่าการให้เหตุผล ควรอธิบายเสมอว่า**ทำไม**สถาปัตยกรรมหนึ่งจึงเหมาะกับบริบทหนึ่ง ไม่ใช่เพียง**วิธี**การนำไปใช้ ## ทำความเข้าใจสถาปัตยกรรม iOS: ภาพรวม ก่อนเข้าสู่คำถามเฉพาะ การเข้าใจภูมิทัศน์ของสถาปัตยกรรม iOS เป็นสิ่งจำเป็น แต่ละ pattern แก้ปัญหาที่แตกต่างกันและเหมาะกับบริบทที่ต่างกัน ### MVC: pattern ดั้งเดิมของ Apple MVC (Model-View-Controller) ยังคงเป็น pattern เริ่มต้นของ Apple แต่ประสบปัญหา "Massive View Controllers" ในแอปที่ซับซ้อน ```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() } } ``` pattern นี้กลายเป็นปัญหาเมื่อ ViewController เกิน 500 บรรทัด ทำให้การทำ unit test แทบเป็นไปไม่ได้ ## คำถามที่ 1: อธิบาย MVVM และการนำไปใช้ใน Swift MVVM (Model-View-ViewModel) แยก presentation logic ออกไปยัง ViewModel ทำให้ทดสอบได้ง่ายและลดขนาดของ ViewController > **จุดสำคัญในการสัมภาษณ์** > > MVVM โดดเด่นเมื่อใช้กับ SwiftUI ด้วย `@Observable` และ data binding ในตัว ส่วน UIKit ต้องใช้กลไก binding (Combine, closure) ### การใช้งาน MVVM ด้วย 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) } } ``` ### View ที่ใช้ 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() } } } ``` ViewModel ไม่รู้จัก UIKit หรือ SwiftUI จึงสามารถทำ unit test ได้อย่างเต็มที่ ## คำถามที่ 2: เมื่อใดควรเลือก VIPER แทน MVVM? VIPER (View-Interactor-Presenter-Entity-Router) เหมาะกับแอปที่ซับซ้อนซึ่งต้องการการแยกหน้าที่อย่างเข้มงวดและการนำทางขั้นสูง ### โครงสร้าง 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) } ``` ### Presenter ทำหน้าที่ประสานงานลอจิก ```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) } } ``` > **คำเตือนระหว่างสัมภาษณ์** > > VIPER เพิ่ม boilerplate จำนวนมาก ควรอธิบายเหตุผลของการใช้ด้วยขนาดทีม (มีนักพัฒนาหลายคนทำงานในโมดูลเดียว) หรือความซับซ้อนของโดเมนธุรกิจ ## คำถามที่ 3: นำ Clean Architecture ไปใช้บน iOS อย่างไร? Clean Architecture จัดเรียงโค้ดเป็นวงกลมซ้อนกัน โดยมีกฎทางธุรกิจอยู่ตรงกลาง เป็นอิสระจากเฟรมเวิร์ก ### โครงสร้างแบ่งชั้น ```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 } } } ``` ### ชั้นข้อมูลด้วย 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() } } } ``` การจัดระเบียบนี้ทำให้สามารถทดสอบแต่ละชั้นได้อย่างอิสระ และเปลี่ยนการนำไปใช้ (เช่น ย้ายจาก CoreData ไปยัง SwiftData) โดยไม่กระทบโดเมน ## คำถามที่ 4: ใช้ design pattern ใดเป็นประจำ? ผู้สัมภาษณ์คาดหวังความเชี่ยวชาญจากการใช้งานจริง ไม่ใช่การท่องจำทฤษฎี ต่อไปนี้คือ pattern ที่พบบ่อยที่สุดบน iOS ### Dependency Injection ด้วย 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) } } ``` ### Coordinator Pattern สำหรับการนำทาง ```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() } } ``` pattern นี้นำลอจิกการนำทางออกจาก ViewController ทำให้เบาขึ้นและนำกลับมาใช้ใหม่ได้ง่ายขึ้น ## คำถามที่ 5: จัดการการสื่อสารระหว่างโมดูลอย่างไร? การสื่อสารระหว่างโมดูลเป็นเรื่องสำคัญในแอปขนาดใหญ่ มีหลายแนวทางขึ้นกับระดับการคัปปลิ้งที่ต้องการ ### การสื่อสารผ่าน protocol ```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) } } ``` ### การสื่อสารแบบ event-driven ด้วย 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 } } } ``` > **เคล็ดลับการสัมภาษณ์** > > ควรกล่าวถึงว่าการเลือกระหว่างคัปปลิ้งแน่น (protocol) กับคัปปลิ้งหลวม (event) ขึ้นอยู่กับบริบท: event เหมาะกับการแจ้งเตือนทั่วระบบ ส่วน protocol เหมาะกับการมีปฏิสัมพันธ์โดยตรง ## คำถามที่ 6: จัดโครงสร้างการทดสอบในสถาปัตยกรรมแบบโมดูลอย่างไร? ความสามารถในการทดสอบเป็นเกณฑ์สำคัญสำหรับตำแหน่งระดับ senior สถาปัตยกรรมที่ดีช่วยให้ทดสอบได้ง่ายในทุกระดับ ### Unit test ของ 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() } } ``` ### Integration test ของ 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) } } ``` ## คำถามที่ 7: จัดการสถานะ UI ที่ซับซ้อนอย่างไร? การจัดการสถานะมีความสำคัญสูงในแอประดับ senior แนวทางที่มีโครงสร้างจะป้องกันบั๊กและทำให้การ debug ง่ายขึ้น ### State machine ด้วย 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) } } } ``` แนวทางนี้ทำให้สถานะที่ขัดแย้งกันเป็นไปไม่ได้ (เช่น `isLoading = true` พร้อมกับแสดงข้อผิดพลาด) ## สรุป การสัมภาษณ์ iOS senior ประเมินความสามารถในการเลือกและให้เหตุผลกับสถาปัตยกรรมที่เหมาะกับบริบท ประเด็นสำคัญ: **Checklist สถาปัตยกรรม iOS Senior:** ✅ MVVM สำหรับแอปขนาดกลางที่ใช้ SwiftUI หรือ UIKit + Combine ✅ VIPER สำหรับทีมขนาดใหญ่และโดเมนธุรกิจที่ซับซ้อน ✅ Clean Architecture เพื่อความเป็นอิสระจากเฟรมเวิร์ก ✅ Dependency Injection อย่างเป็นระบบเพื่อความสามารถในการทดสอบ ✅ Coordinator Pattern เพื่อแยกการนำทางออกจากเลเยอร์อื่น ✅ State machine สำหรับโฟลว์ที่ซับซ้อน ✅ การทดสอบในทุกระดับ: unit, integration, UI **ในการสัมภาษณ์ ต้องแสดงให้เห็น:** - ความเข้าใจในเรื่อง trade-off (MVVM แบบเรียบง่าย vs VIPER ที่มีโครงสร้าง) - ประสบการณ์เชิงปฏิบัติพร้อมตัวอย่างจากโปรเจกต์จริง - ความสามารถในการปรับสถาปัตยกรรมให้เข้ากับบริบท (ขนาดทีม, ความซับซ้อน) - ความเชี่ยวชาญในการทดสอบในฐานะเกณฑ์คุณภาพของสถาปัตยกรรม --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/ios-senior-interview-architecture-design-patterns