Skip to content
All essays
iOSMarch 27, 202516 min

iOS Architecture Patterns: MVC, MVVM, VIPER

Common architecture patterns in iOS applications. When to use which pattern?

Ü
Ümit Uz
Mobile & Full Stack Developer

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!

Related essays

Next essay
Swift Performance Optimization: Best Practices