Wawancara iOS Senior 2026: Pertanyaan Arsitektur dan Design Pattern
Persiapkan wawancara iOS senior dengan pertanyaan kunci tentang MVVM, VIPER, Clean Architecture, dan design pattern. Panduan lengkap dengan contoh kode Swift.

Wawancara iOS senior menempatkan penekanan besar pada arsitektur dan design pattern. Selain sintaks Swift, pewawancara mengevaluasi kemampuan merancang aplikasi yang mudah dipelihara, dapat diuji, dan skalabel.
Panduan ini mencakup pertanyaan paling sering tentang MVVM, VIPER, Clean Architecture, dan pattern penting, dengan jawaban terperinci dan contoh kode siap wawancara.
Pada wawancara senior, jawaban teknis kurang penting dibandingkan pemikiran di baliknya. Selalu jelaskan mengapa suatu arsitektur cocok untuk konteks tertentu, bukan hanya bagaimana mengimplementasikannya.
Memahami arsitektur iOS: tinjauan umum
Sebelum membahas pertanyaan spesifik, penting untuk memahami lanskap arsitektur iOS. Setiap pattern menyelesaikan masalah berbeda dan cocok untuk konteks berbeda.
MVC: pattern historis Apple
MVC (Model-View-Controller) tetap menjadi pattern bawaan Apple, tetapi mengalami masalah "Massive View Controllers" pada aplikasi yang kompleks.
// 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 ini menjadi bermasalah ketika ViewController melebihi 500 baris, sehingga membuat unit test hampir mustahil.
Pertanyaan 1: Jelaskan MVVM dan implementasinya di Swift
MVVM (Model-View-ViewModel) memisahkan logika presentasi ke dalam ViewModel, memudahkan pengujian dan mengurangi ukuran ViewController.
MVVM bersinar bersama SwiftUI berkat @Observable dan data binding bawaan. Dengan UIKit, dibutuhkan mekanisme binding (Combine, closure).
Implementasi MVVM dengan Combine
// 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 dengan binding Combine
// 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 tidak mengenal UIKit maupun SwiftUI, sehingga sepenuhnya dapat diuji secara unit.
Pertanyaan 2: Kapan memilih VIPER dibanding MVVM?
VIPER (View-Interactor-Presenter-Entity-Router) cocok untuk aplikasi kompleks yang memerlukan pemisahan tanggung jawab yang ketat dan navigasi tingkat lanjut.
Struktur VIPER lengkap
// 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 mengorkestrasi logika
// 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 memunculkan boilerplate yang besar. Penggunaannya harus dibenarkan oleh ukuran tim (beberapa developer pada satu modul) atau kompleksitas domain bisnis.
Pertanyaan 3: Bagaimana menerapkan Clean Architecture di iOS?
Clean Architecture mengatur kode dalam lingkaran konsentris, dengan aturan bisnis di tengah, lepas dari framework.
Struktur lapisan
// 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 }
}
}Lapisan data dengan pattern Repository
// 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()
}
}
}Penataan ini memungkinkan pengujian setiap lapisan secara independen dan mengganti implementasi (misalnya migrasi dari CoreData ke SwiftData) tanpa menyentuh domain.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Pertanyaan 4: Pattern desain apa yang Anda gunakan sehari-hari?
Pewawancara mengharapkan penguasaan pattern secara praktis, bukan hafalan teori. Berikut pattern paling sering pada iOS.
Dependency Injection dengan Property Wrappers
// 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 untuk navigasi
// 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 ini memindahkan logika navigasi keluar dari ViewController, sehingga lebih ringan dan dapat digunakan kembali.
Pertanyaan 5: Bagaimana menangani komunikasi antar modul?
Komunikasi antar modul sangat penting pada aplikasi besar. Ada beberapa pendekatan tergantung pada tingkat kopling yang diinginkan.
Komunikasi berbasis protokol
// 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)
}
}Komunikasi berbasis event dengan Combine
// 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
}
}
}Sebutkan bahwa pilihan antara kopling kuat (protokol) dan kopling longgar (event) bergantung pada konteks: event cocok untuk notifikasi global, protokol untuk interaksi langsung.
Pertanyaan 6: Bagaimana menyusun pengujian dalam arsitektur modular?
Kemampuan untuk diuji adalah kriteria penting bagi posisi senior. Arsitektur yang baik memudahkan pengujian di semua tingkat.
Unit test ViewModel
// 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()
}
}Tes integrasi Use Case
// 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)
}
}Pertanyaan 7: Bagaimana menangani state UI yang kompleks?
Pengelolaan state krusial pada aplikasi level senior. Pendekatan terstruktur menghindari bug dan menyederhanakan debugging.
State machine dengan Enum
// 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)
}
}
}Pendekatan ini membuat state yang tidak konsisten menjadi mustahil (misalnya isLoading = true dengan error yang ditampilkan).
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Kesimpulan
Wawancara iOS senior menilai kemampuan memilih dan membenarkan arsitektur yang sesuai dengan konteks. Poin-poin utama:
Checklist arsitektur iOS Senior:
✅ MVVM untuk aplikasi berukuran sedang dengan SwiftUI atau UIKit + Combine ✅ VIPER untuk tim besar dan domain bisnis kompleks ✅ Clean Architecture untuk kemandirian dari framework ✅ Dependency Injection sistematis untuk kemampuan diuji ✅ Pattern Coordinator untuk memisahkan navigasi ✅ State machine untuk alur yang kompleks ✅ Pengujian di setiap level: unit, integrasi, UI
Pada wawancara, perlu ditunjukkan:
- Pemahaman trade-off (MVVM sederhana vs VIPER terstruktur)
- Pengalaman praktis dengan contoh proyek nyata
- Kemampuan menyesuaikan arsitektur dengan konteks (ukuran tim, kompleksitas)
- Penguasaan pengujian sebagai kriteria kualitas arsitektur
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Wawancara StoreKit 2: Manajemen Langganan dan Validasi Tanda Terima
Kuasai pertanyaan wawancara iOS tentang StoreKit 2, manajemen langganan, validasi tanda terima, dan implementasi pembelian dalam aplikasi dengan contoh kode Swift praktis.

Swift Testing Framework Wawancara 2026: Makro #expect dan #require vs XCTest
Kuasai Swift Testing Framework baru untuk wawancara iOS: makro #expect dan #require, migrasi dari XCTest, pola lanjutan, dan jebakan umum.

Wawancara iOS Push Notifications 2026: APNs, token, dan troubleshooting
Panduan lengkap untuk mempersiapkan wawancara iOS tentang Push Notifications, APNs, manajemen token, dan troubleshooting. Pertanyaan umum dengan jawaban mendetail.