Skip to main content

iOS Architecture Patterns

MVVM (Model-View-ViewModel)

Basic MVVM Structure

// Model
struct User: Identifiable, Codable {
let id: UUID
let name: String
let email: String
let avatarURL: URL?
}

// ViewModel
class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}

@MainActor
func loadUsers() async {
isLoading = true
errorMessage = nil

do {
users = try await userService.fetchUsers()
} catch {
errorMessage = error.localizedDescription
}

isLoading = false
}

func deleteUser(_ user: User) {
users.removeAll { $0.id == user.id }
// Additional deletion logic
}
}

// View
struct UserListView: View {
@StateObject private var viewModel = UserListViewModel()

var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView("Loading users...")
} else {
List(viewModel.users) { user in
UserRowView(user: user)
.swipeActions {
Button("Delete", role: .destructive) {
viewModel.deleteUser(user)
}
}
}
}
}
.navigationTitle("Users")
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") { }
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.loadUsers()
}
}
}

Advanced MVVM with Dependency Injection

// Service Protocol
protocol UserServiceProtocol {
func fetchUsers() async throws -> [User]
func createUser(_ user: User) async throws -> User
func updateUser(_ user: User) async throws -> User
func deleteUser(_ id: UUID) async throws
}

// Concrete Service
class UserService: UserServiceProtocol {
private let networkManager: NetworkManagerProtocol

init(networkManager: NetworkManagerProtocol = NetworkManager()) {
self.networkManager = networkManager
}

func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
return try await networkManager.fetch([User].self, from: url)
}

func createUser(_ user: User) async throws -> User {
let url = URL(string: "https://api.example.com/users")!
return try await networkManager.post(user, to: url)
}

func updateUser(_ user: User) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(user.id)")!
return try await networkManager.put(user, to: url)
}

func deleteUser(_ id: UUID) async throws {
let url = URL(string: "https://api.example.com/users/\(id)")!
try await networkManager.delete(from: url)
}
}

// Enhanced ViewModel with error handling
class EnhancedUserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var searchText = ""

private let userService: UserServiceProtocol
private var cancellables = Set<AnyCancellable>()

var filteredUsers: [User] {
if searchText.isEmpty {
return users
}
return users.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}

init(userService: UserServiceProtocol) {
self.userService = userService
setupBindings()
}

private func setupBindings() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}

@MainActor
func loadUsers() async {
await performAsyncOperation {
self.users = try await self.userService.fetchUsers()
}
}

@MainActor
func createUser(_ user: User) async {
await performAsyncOperation {
let newUser = try await self.userService.createUser(user)
self.users.append(newUser)
}
}

@MainActor
private func performAsyncOperation<T>(_ operation: @escaping () async throws -> T) async {
isLoading = true
errorMessage = nil

do {
_ = try await operation()
} catch {
errorMessage = error.localizedDescription
}

isLoading = false
}
}

Coordinator Pattern

// Coordinator Protocol
protocol Coordinator: ObservableObject {
var navigationPath: NavigationPath { get set }
func start()
}

// Main App Coordinator
class AppCoordinator: Coordinator {
@Published var navigationPath = NavigationPath()
@Published var presentedSheet: SheetType?
@Published var presentedFullScreenCover: FullScreenType?

enum SheetType: Identifiable {
case settings, profile, help
var id: String {
switch self {
case .settings: return "settings"
case .profile: return "profile"
case .help: return "help"
}
}
}

enum FullScreenType: Identifiable {
case onboarding, tutorial
var id: String {
switch self {
case .onboarding: return "onboarding"
case .tutorial: return "tutorial"
}
}
}

func start() {
// Initial setup
}

func navigateToUserDetail(_ user: User) {
navigationPath.append(user)
}

func navigateToSettings() {
presentedSheet = .settings
}

func dismissSheet() {
presentedSheet = nil
}

func popToRoot() {
navigationPath = NavigationPath()
}
}

// Usage in main app
struct AppView: View {
@StateObject private var coordinator = AppCoordinator()

var body: some View {
NavigationStack(path: $coordinator.navigationPath) {
HomeView()
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
.sheet(item: $coordinator.presentedSheet) { sheetType in
switch sheetType {
case .settings:
SettingsView()
case .profile:
ProfileView()
case .help:
HelpView()
}
}
.environmentObject(coordinator)
}
}

Multicast Delegate Pattern

Implementation

// Multicast Delegate Protocol
protocol UserServiceDelegate: AnyObject {
func userService(_ service: UserService, didUpdateUsers users: [User])
func userService(_ service: UserService, didFailWithError error: Error)
}

// Multicast Delegate Implementation
class MulticastDelegate<T> {
private var delegates = NSHashTable<AnyObject>.weakObjects()

func add(_ delegate: T) {
delegates.add(delegate as AnyObject)
}

func remove(_ delegate: T) {
delegates.remove(delegate as AnyObject)
}

func invoke(_ invocation: (T) -> Void) {
for delegate in delegates.allObjects {
if let delegate = delegate as? T {
invocation(delegate)
}
}
}
}

// Enhanced User Service with Multicast Delegate
class UserServiceWithDelegate: UserServiceProtocol {
private let networkManager: NetworkManagerProtocol
private let multicastDelegate = MulticastDelegate<UserServiceDelegate>()

init(networkManager: NetworkManagerProtocol = NetworkManager()) {
self.networkManager = networkManager
}

func addDelegate(_ delegate: UserServiceDelegate) {
multicastDelegate.add(delegate)
}

func removeDelegate(_ delegate: UserServiceDelegate) {
multicastDelegate.remove(delegate)
}

func fetchUsers() async throws -> [User] {
do {
let users = try await networkManager.fetch([User].self, from: URL(string: "https://api.example.com/users")!)
multicastDelegate.invoke { delegate in
delegate.userService(self, didUpdateUsers: users)
}
return users
} catch {
multicastDelegate.invoke { delegate in
delegate.userService(self, didFailWithError: error)
}
throw error
}
}
}

// ViewModel using Multicast Delegate
class UserListViewModelWithDelegate: ObservableObject, UserServiceDelegate {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?

private let userService: UserServiceWithDelegate

init(userService: UserServiceWithDelegate = UserServiceWithDelegate()) {
self.userService = userService
self.userService.addDelegate(self)
}

deinit {
userService.removeDelegate(self)
}

func loadUsers() async {
isLoading = true
errorMessage = nil

do {
_ = try await userService.fetchUsers()
} catch {
errorMessage = error.localizedDescription
}

isLoading = false
}

// MARK: - UserServiceDelegate
func userService(_ service: UserService, didUpdateUsers users: [User]) {
DispatchQueue.main.async {
self.users = users
}
}

func userService(_ service: UserService, didFailWithError error: Error) {
DispatchQueue.main.async {
self.errorMessage = error.localizedDescription
}
}
}

Repository Pattern

Data Layer Architecture

// Repository Protocol
protocol UserRepositoryProtocol {
func fetchUsers() async throws -> [User]
func fetchUser(id: UUID) async throws -> User
func saveUser(_ user: User) async throws
func deleteUser(id: UUID) async throws
}

// Local Repository (Core Data)
class LocalUserRepository: UserRepositoryProtocol {
private let coreDataStack: CoreDataStack

init(coreDataStack: CoreDataStack) {
self.coreDataStack = coreDataStack
}

func fetchUsers() async throws -> [User] {
// Core Data implementation
return []
}

func fetchUser(id: UUID) async throws -> User {
// Core Data implementation
throw NSError(domain: "Not implemented", code: -1)
}

func saveUser(_ user: User) async throws {
// Core Data implementation
}

func deleteUser(id: UUID) async throws {
// Core Data implementation
}
}

// Remote Repository (API)
class RemoteUserRepository: UserRepositoryProtocol {
private let networkManager: NetworkManagerProtocol

init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}

func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
return try await networkManager.fetch([User].self, from: url)
}

func fetchUser(id: UUID) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
return try await networkManager.fetch(User.self, from: url)
}

func saveUser(_ user: User) async throws {
let url = URL(string: "https://api.example.com/users")!
_ = try await networkManager.post(user, to: url)
}

func deleteUser(id: UUID) async throws {
let url = URL(string: "https://api.example.com/users/\(id)")!
try await networkManager.delete(from: url)
}
}

// Combined Repository with Caching
class CachedUserRepository: UserRepositoryProtocol {
private let localRepository: UserRepositoryProtocol
private let remoteRepository: UserRepositoryProtocol
private let cache = NSCache<NSString, [User]>()

init(localRepository: UserRepositoryProtocol, remoteRepository: UserRepositoryProtocol) {
self.localRepository = localRepository
self.remoteRepository = remoteRepository
}

func fetchUsers() async throws -> [User] {
// Try cache first
if let cachedUsers = cache.object(forKey: "users") {
return cachedUsers
}

// Try remote, fallback to local
do {
let users = try await remoteRepository.fetchUsers()
cache.setObject(users, forKey: "users")

// Save to local storage
for user in users {
try await localRepository.saveUser(user)
}

return users
} catch {
// Fallback to local data
return try await localRepository.fetchUsers()
}
}

func fetchUser(id: UUID) async throws -> User {
// Try cache first
if let cachedUsers = cache.object(forKey: "users"),
let user = cachedUsers.first(where: { $0.id == id }) {
return user
}

// Try remote, fallback to local
do {
return try await remoteRepository.fetchUser(id: id)
} catch {
return try await localRepository.fetchUser(id: id)
}
}

func saveUser(_ user: User) async throws {
// Save to both repositories
try await remoteRepository.saveUser(user)
try await localRepository.saveUser(user)

// Update cache
cache.removeObject(forKey: "users")
}

func deleteUser(id: UUID) async throws {
// Delete from both repositories
try await remoteRepository.deleteUser(id: id)
try await localRepository.deleteUser(id: id)

// Update cache
cache.removeObject(forKey: "users")
}
}

Clean Architecture

Domain Layer

// Use Cases (Interactors)
protocol FetchUsersUseCase {
func execute() async throws -> [User]
}

protocol CreateUserUseCase {
func execute(_ user: User) async throws -> User
}

// Concrete Use Cases
class FetchUsersUseCaseImpl: FetchUsersUseCase {
private let userRepository: UserRepositoryProtocol

init(userRepository: UserRepositoryProtocol) {
self.userRepository = userRepository
}

func execute() async throws -> [User] {
return try await userRepository.fetchUsers()
}
}

class CreateUserUseCaseImpl: CreateUserUseCase {
private let userRepository: UserRepositoryProtocol

init(userRepository: UserRepositoryProtocol) {
self.userRepository = userRepository
}

func execute(_ user: User) async throws -> User {
try await userRepository.saveUser(user)
return user
}
}

// Dependency Injection Container
class DIContainer {
static let shared = DIContainer()

private let coreDataStack = CoreDataStack()
private let networkManager = NetworkManager()

lazy var userRepository: UserRepositoryProtocol = {
let localRepo = LocalUserRepository(coreDataStack: coreDataStack)
let remoteRepo = RemoteUserRepository(networkManager: networkManager)
return CachedUserRepository(localRepository: localRepo, remoteRepository: remoteRepo)
}()

lazy var fetchUsersUseCase: FetchUsersUseCase = {
FetchUsersUseCaseImpl(userRepository: userRepository)
}()

lazy var createUserUseCase: CreateUserUseCase = {
CreateUserUseCaseImpl(userRepository: userRepository)
}()
}

// Clean Architecture ViewModel
class CleanUserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?

private let fetchUsersUseCase: FetchUsersUseCase
private let createUserUseCase: CreateUserUseCase

init(fetchUsersUseCase: FetchUsersUseCase = DIContainer.shared.fetchUsersUseCase,
createUserUseCase: CreateUserUseCase = DIContainer.shared.createUserUseCase) {
self.fetchUsersUseCase = fetchUsersUseCase
self.createUserUseCase = createUserUseCase
}

@MainActor
func loadUsers() async {
isLoading = true
errorMessage = nil

do {
users = try await fetchUsersUseCase.execute()
} catch {
errorMessage = error.localizedDescription
}

isLoading = false
}

@MainActor
func createUser(_ user: User) async {
do {
let newUser = try await createUserUseCase.execute(user)
users.append(newUser)
} catch {
errorMessage = error.localizedDescription
}
}
}

Redux-like State Management

State Management Pattern

// App State
struct AppState {
var users: [User] = []
var isLoading = false
var errorMessage: String?
var selectedUser: User?
}

// Actions
enum AppAction {
case loadUsers
case usersLoaded([User])
case loadUsersFailed(Error)
case selectUser(User)
case createUser(User)
case deleteUser(UUID)
}

// Reducer
func appReducer(state: inout AppState, action: AppAction) {
switch action {
case .loadUsers:
state.isLoading = true
state.errorMessage = nil

case .usersLoaded(let users):
state.users = users
state.isLoading = false

case .loadUsersFailed(let error):
state.errorMessage = error.localizedDescription
state.isLoading = false

case .selectUser(let user):
state.selectedUser = user

case .createUser(let user):
state.users.append(user)

case .deleteUser(let id):
state.users.removeAll { $0.id == id }
}
}

// Store
class AppStore: ObservableObject {
@Published private(set) var state: AppState
private let userService: UserServiceProtocol

init(userService: UserServiceProtocol = UserService()) {
self.state = AppState()
self.userService = userService
}

func dispatch(_ action: AppAction) {
appReducer(state: &state, action: action)

// Handle side effects
switch action {
case .loadUsers:
Task {
await loadUsers()
}
default:
break
}
}

@MainActor
private func loadUsers() async {
do {
let users = try await userService.fetchUsers()
dispatch(.usersLoaded(users))
} catch {
dispatch(.loadUsersFailed(error))
}
}
}

// View using Redux-like pattern
struct ReduxUserListView: View {
@StateObject private var store = AppStore()

var body: some View {
NavigationView {
Group {
if store.state.isLoading {
ProgressView("Loading users...")
} else {
List(store.state.users) { user in
UserRowView(user: user)
.onTapGesture {
store.dispatch(.selectUser(user))
}
.swipeActions {
Button("Delete", role: .destructive) {
store.dispatch(.deleteUser(user.id))
}
}
}
}
}
.navigationTitle("Users")
.alert("Error", isPresented: .constant(store.state.errorMessage != nil)) {
Button("OK") { }
} message: {
Text(store.state.errorMessage ?? "")
}
}
.onAppear {
store.dispatch(.loadUsers)
}
}
}