Choosing the right architecture pattern in iOS applications is critical for project scalability and maintainability.
MVC (Model-View-Controller)
Traditional MVC
Apple's default pattern:
swift
// Model
struct User {
let id: String
let name: String
let email: String
}
// View
struct UserView: View {
let user: User
var body: some View {
VStack(alignment: .leading) {
Text(user.name)
.font(.title)
Text(user.email)
.foregroundColor(.gray)
}
}
}
// Controller
class UserController: ObservableObject {
@Published var user: User?
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: String) {
Task {
user = await service.fetchUser(id: id)
}
}
}MVC Pros/Cons
Pros:
- Simple and easy to understand
- Supported by Apple
- Ideal for small projects
Cons:
- Controller can become bloated in large projects
- Testing difficulties
- Tightly coupled
MVVM (Model-View-ViewModel)
MVVM Implementation
swift
// Model
struct User {
let id: String
let name: String
let email: String
}
// ViewModel
final class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
func loadUser(id: String) {
isLoading = true
errorMessage = nil
Task { @MainActor in
do {
let user = try await userService.fetchUser(id: id)
self.user = user
isLoading = false
} catch {
self.errorMessage = error.localizedDescription
isLoading = false
}
}
}
var displayName: String {
user?.name ?? "Guest"
}
}
// View
struct UserView: View {
@StateObject private var viewModel: UserViewModel
init(viewModel: UserViewModel = UserViewModel()) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView("Loading...")
} else if let user = viewModel.user {
UserDetailView(user: user)
} else if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
}
}
.task {
await viewModel.loadUser(id: "123")
}
}
}MVVM Pros/Cons
Pros:
- Testable
- Separated view and business logic
- Perfect fit with SwiftUI
- Reactive programming support
Cons:
- Boilerplate code
- Learning curve
- Overhead for small projects
VIPER (View-Interactor-Presenter-Entity-Router)
VIPER Components
swift
// Entity
struct User {
let id: String
let name: String
let email: String
}
// Interactor (Business Logic)
protocol UserInteractorProtocol {
func fetchUser(id: String)
}
final class UserInteractor: UserInteractorProtocol {
weak var presenter: UserPresenterProtocol?
private let service: UserService
func fetchUser(id: String) {
Task {
do {
let user = try await service.fetchUser(id: id)
await presenter?.didFetchUser(user)
} catch {
await presenter?.didFailToFetchUser(error)
}
}
}
}
// Presenter (Presentation Logic)
protocol UserPresenterProtocol: AnyObject {
func viewDidLoad()
func didFetchUser(_ user: User)
func didFailToFetchUser(_ error: Error)
}
final class UserPresenter: UserPresenterProtocol {
weak var view: UserViewProtocol?
var interactor: UserInteractorProtocol?
var router: UserRouterProtocol?
func viewDidLoad() {
view?.showLoading()
interactor?.fetchUser(id: "123")
}
func didFetchUser(_ user: User) {
view?.hideLoading()
view?.displayUser(user)
}
func didFailToFetchUser(_ error: Error) {
view?.hideLoading()
view?.showError(error.localizedDescription)
}
}
// View
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func displayUser(_ user: User)
func showError(_ message: String)
}
final class UserViewController: UIViewController, UserViewProtocol {
var presenter: UserPresenterProtocol?
func showLoading() {
// Show loading indicator
}
func hideLoading() {
// Hide loading indicator
}
func displayUser(_ user: User) {
// Update UI with user data
}
func showError(_ message: String) {
// Show error alert
}
override func viewDidLoad() {
super.viewDidLoad()
presenter?.viewDidLoad()
}
}
// Router (Navigation Logic)
protocol UserRouterProtocol {
func navigateToDetail(user: User)
}
final class UserRouter: UserRouterProtocol {
weak var viewController: UIViewController?
func navigateToDetail(user: User) {
let detailVC = UserDetailViewController()
detailVC.user = user
viewController?.navigationController?.pushViewController(detailVC, animated: true)
}
}VIPER Pros/Cons
Pros:
- Highly separated
- Testable
- Ideal for large teams
- Role-based development
Cons:
- Lots of boilerplate
- Complex structure
- Learning curve
- Overkill for small projects
Pattern Comparison
| Pattern | Complexity | Testability | Scalability | Best For | |---------|-----------|-------------|-------------|----------| | MVC | Low | Medium | Low | Small projects | | MVVM | Medium | High | High | Medium-large projects | | VIPER | High | Very High | Very High | Large enterprise projects |
Pattern Selection Criteria
Use MVC when:
- Project is small (< 10 screens)
- Single developer
- Need rapid prototyping
- Apple default preferred
Use MVVM when:
- Medium projects (10-50 screens)
- Testability is important
- Using SwiftUI
- Want reactive programming
Use VIPER when:
- Large enterprise projects (> 50 screens)
- Large teams
- Long-term maintenance is critical
- Need strict separation
Modern Approach: TCA (The Composable Architecture)
swift
import ComposableArchitecture
struct AppState: Equatable {
var users: [User] = []
var isLoading = false
}
enum AppAction {
case fetchUsers
case usersResponse(Result<[User], Error>)
}
struct AppEnvironment {
var userService: UserService
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment> {
state, action, environment in
switch action {
case .fetchUsers:
state.isLoading = true
return .task {
let result = await Result {
try await environment.userService.fetchUsers()
}
return .usersResponse(result)
}
case .usersResponse(.success(let users)):
state.users = users
state.isLoading = false
return .none
case .usersResponse(.failure):
state.isLoading = false
return .none
}
}Choosing the right architecture pattern is critical for project success!