iOS Senior Mülakat 2026: Mimari ve Tasarım Desenleri Soruları
iOS senior mülakatlarına MVVM, VIPER, Clean Architecture ve tasarım desenleri üzerine kilit sorularla hazırlanın. Swift kod örnekleriyle eksiksiz rehber.

iOS senior mülakatları mimari ve tasarım desenlerine büyük önem verir. Mülakatçılar Swift söziminin ötesinde, sürdürülebilir, test edilebilir ve ölçeklenebilir uygulamalar tasarlama becerisini değerlendirir.
Bu rehber MVVM, VIPER, Clean Architecture ve temel desenler hakkında en sık sorulan soruları, ayrıntılı yanıtlar ve mülakata hazır kod örnekleriyle ele alır.
Senior mülakatlarda teknik yanıt, akıl yürütmeden daha az önemlidir. Bir mimarinin verili bir bağlama neden uyduğu her zaman açıklanmalıdır, yalnızca nasıl uygulanacağı değil.
iOS mimarilerini anlamak: genel bir bakış
Belirli sorulara dalmadan önce iOS mimari manzarasını anlamak şarttır. Her desen farklı sorunları çözer ve farklı bağlamlara uyar.
MVC: Apple'ın tarihsel deseni
MVC (Model-View-Controller) Apple'ın varsayılan deseni olmaya devam ediyor, ancak karmaşık uygulamalarda "Massive View Controllers" sorunundan muzdarip.
// 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()
}
}ViewController'lar 500 satırı aştığında bu desen sorunlu hale gelir ve birim testleri neredeyse imkânsız kılar.
Soru 1: MVVM'i ve Swift uygulamasını açıklayın
MVVM (Model-View-ViewModel) sunum mantığını bir ViewModel'e ayırır, testleri kolaylaştırır ve ViewController'ın boyutunu azaltır.
MVVM, @Observable ve yerel data binding sayesinde SwiftUI ile parlar. UIKit ile bir binding mekanizması (Combine, closures) gereklidir.
Combine ile MVVM uygulaması
// 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)
}
}Combine binding'li View
// 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 ne UIKit'i ne de SwiftUI'yi tanır, bu da onu tamamen birim test edilebilir yapar.
Soru 2: MVVM yerine VIPER ne zaman tercih edilmeli?
VIPER (View-Interactor-Presenter-Entity-Router) sorumlulukların katı ayrımını ve gelişmiş gezinmeyi gerektiren karmaşık uygulamalar için uygundur.
Eksiksiz VIPER yapısı
// 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 mantığı orkestre eder
// 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 kayda değer bir boilerplate getirir. Kullanımı, ekip büyüklüğü (aynı modülde birden fazla geliştirici) veya iş alanının karmaşıklığı ile gerekçelendirilmelidir.
Soru 3: Clean Architecture iOS'ta nasıl uygulanır?
Clean Architecture, iş kurallarını merkeze yerleştirerek kodu eş merkezli halkalar şeklinde, framework'lerden bağımsız olarak organize eder.
Katman yapısı
// 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 }
}
}Repository deseniyle veri katmanı
// 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()
}
}
}Bu organizasyon her katmanı bağımsız test etmeyi ve uygulamaları (örneğin CoreData'dan SwiftData'ya geçişi) etki alanına dokunmadan değiştirmeyi mümkün kılar.
iOS mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Soru 4: Günlük olarak hangi tasarım desenlerini kullanırsınız?
Mülakatçılar teorik ezber değil, pratik desen hâkimiyeti bekler. iOS'ta en sık karşılaşılanlar şunlardır.
Property Wrapper'larla Dependency Injection
// 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)
}
}Gezinme için Coordinator deseni
// 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()
}
}Bu desen, gezinme mantığını ViewController'ların dışına taşıyarak onları daha hafif ve yeniden kullanılabilir hale getirir.
Soru 5: Modüller arası iletişim nasıl yönetilir?
Modüller arası iletişim büyük uygulamalarda kritiktir. İstenen bağlanma derecesine göre çeşitli yaklaşımlar mevcuttur.
Protokol tabanlı iletişim
// 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)
}
}Combine ile olay tabanlı iletişim
// 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
}
}
}Sıkı bağlanma (protokoller) ile gevşek bağlanma (olaylar) arasındaki seçimin bağlama bağlı olduğunu belirtmek gerekir: olaylar küresel bildirimlere, protokoller doğrudan etkileşimlere uygundur.
Soru 6: Modüler bir mimaride testler nasıl yapılandırılır?
Test edilebilirlik, senior pozisyonlar için önemli bir kriterdir. İyi bir mimari her seviyede testleri kolaylaştırır.
ViewModel'in birim testi
// 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()
}
}Use Case'in entegrasyon testi
// 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)
}
}Soru 7: Karmaşık UI durumları nasıl yönetilir?
Durum yönetimi senior uygulamalar için kritiktir. Yapılandırılmış bir yaklaşım hataları önler ve hata ayıklamayı kolaylaştırır.
Enum ile durum makinesi
// 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)
}
}
}Bu yaklaşım tutarsız durumları imkânsız kılar (örneğin görüntülenen bir hata ile birlikte isLoading = true).
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Sonuç
iOS senior mülakatları, bağlama uygun bir mimariyi seçme ve gerekçelendirme becerisini değerlendirir. Anahtar çıkarımlar:
iOS Senior mimari kontrol listesi:
✅ SwiftUI veya UIKit + Combine ile orta ölçekli uygulamalar için MVVM ✅ Büyük ekipler ve karmaşık iş alanları için VIPER ✅ Framework bağımsızlığı için Clean Architecture ✅ Test edilebilirlik için sistematik Dependency Injection ✅ Gezinmeyi ayırmak için Coordinator deseni ✅ Karmaşık akışlar için durum makineleri ✅ Her seviyede testler: birim, entegrasyon, UI
Mülakatlarda gösterilmesi gerekenler:
- Ödünleşmelerin anlaşılması (basit MVVM ile yapılandırılmış VIPER)
- Gerçek proje örnekleriyle pratik deneyim
- Mimariyi bağlama uyarlama becerisi (ekip büyüklüğü, karmaşıklık)
- Mimari kalite kriteri olarak test hâkimiyeti
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

StoreKit 2 Mülakatı: Abonelik Yönetimi ve Makbuz Doğrulama
StoreKit 2, abonelik yönetimi, makbuz doğrulama ve uygulama içi satın alma uygulaması hakkında iOS mülakat sorularında pratik Swift kod örnekleriyle uzmanlaşın.

Swift Testing Framework Mülakat 2026: #expect ve #require Makroları XCTest Karşısında
iOS mülakatları için yeni Swift Testing Framework'ünde uzmanlaş: #expect ve #require makroları, XCTest geçişi, ileri seviye desenler ve sık yapılan hatalar.

iOS Push Notifications Mülakatı 2026: APNs, token'lar ve sorun giderme
iOS mülakatlarına hazırlık için kapsamlı rehber: Push Notifications, APNs, token yönetimi ve sorun giderme. Sık sorulan sorular ve ayrıntılı yanıtlar.