# iOSシニア面接 2026:アーキテクチャとデザインパターンの質問集 > MVVM、VIPER、Clean Architecture、デザインパターンに関する重要な質問でiOSシニア面接に備えます。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シニア面接ではアーキテクチャとデザインパターンが大きな比重を占めます。Swift構文の知識を超えて、面接官は保守性・テスト容易性・スケーラビリティを備えたアプリケーションを設計する力を評価します。 本ガイドでは、MVVM、VIPER、Clean Architecture、そして必須となるパターンに関する頻出質問を、詳細な解説と面接で使えるコード例とともに取り上げます。 > **面接官が本当に評価していること** > > シニア面接では、技術的な答えそのものよりも、その理由付けが重視されます。**なぜ**そのアーキテクチャが特定の文脈に適しているのかを必ず説明することが重要であり、**どう**実装するかの説明だけでは不十分です。 ## iOSアーキテクチャの全体像を理解する 個別の質問に踏み込む前に、iOSアーキテクチャの全体像を理解しておくことが欠かせません。それぞれのパターンは異なる課題を解決し、適する文脈も異なります。 ### MVC:Appleの伝統的なパターン MVC(Model-View-Controller)は依然として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() } } ``` このパターンはViewControllerが500行を超えると問題化し、ユニットテストはほぼ不可能になります。 ## 質問1:MVVMとSwiftでの実装を説明してください MVVM(Model-View-ViewModel)は表示ロジックをViewModelに分離し、テストを容易にしつつViewControllerのサイズを縮小します。 > **面接の重要ポイント** > > MVVMは`@Observable`とネイティブのデータバインディングのおかげで、SwiftUIと組み合わせると真価を発揮します。UIKitではバインディング機構(Combine、クロージャ)が必要です。 ### CombineによるMVVMの実装 ```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) } } ``` ### Combineバインディングを用いるView ```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も知らないため、ユニットテストで完全に検証可能です。 ## 質問2:MVVMではなくVIPERを選ぶべき場面は? 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は大量のボイラープレートをもたらします。導入の妥当性は、チーム規模(同一モジュールに複数の開発者がいる)や業務ドメインの複雑さで説明する必要があります。 ## 質問3:iOSでClean Architectureをどう実装するか? 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 } } } ``` ### 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:日常的にどのデザインパターンを使うか? 面接官は理論の暗唱ではなく、パターンの実用的な習熟度を期待します。iOSで頻出するものを以下に挙げます。 ### Property Wrapperを使うDependency Injection ```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パターン ```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() } } ``` このパターンによりナビゲーションロジックがViewControllerの外に切り出され、ViewControllerは軽量で再利用しやすくなります。 ## 質問5:モジュール間通信をどう扱うか? モジュール間通信は大規模アプリで非常に重要です。求める結合度に応じて複数のアプローチがあります。 ### プロトコルベースの通信 ```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) } } ``` ### 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 } } } ``` > **面接時のヒント** > > 強結合(プロトコル)と疎結合(イベント)の選択は文脈に依存することを伝えてください。イベントはグローバルな通知に、プロトコルは直接的な相互作用に向いています。 ## 質問6:モジュール構造のテストをどう構成するか? テスト容易性はシニアの重要な評価基準です。良いアーキテクチャはあらゆるレベルのテストを容易にします。 ### 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() } } ``` ### 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状態をどう扱うか? 状態管理はシニアアプリで極めて重要です。構造化されたアプローチによりバグを防ぎ、デバッグも単純化されます。 ### 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シニア面接では、文脈に応じたアーキテクチャを選定し、その理由を説明できる力が評価されます。要点を以下にまとめます。 **iOSシニア・アーキテクチャのチェックリスト:** ✅ SwiftUIまたはUIKit + Combineを用いる中規模アプリにはMVVM ✅ 大規模チームと複雑な業務ドメインにはVIPER ✅ フレームワーク非依存にはClean Architecture ✅ テスト容易性のための体系的なDependency Injection ✅ ナビゲーションを切り離すためのCoordinatorパターン ✅ 複雑なフローのためのステートマシン ✅ ユニット・統合・UIといった全レベルでのテスト **面接で示すべき点:** - トレードオフへの理解(シンプルなMVVM vs 構造化されたVIPER) - 実プロジェクトの例を伴う実践経験 - 文脈(チーム規模、複雑さ)に応じてアーキテクチャを調整する能力 - アーキテクチャ品質の基準としてのテストへの精通 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/ios/ios-senior-interview-architecture-design-patterns