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
Navigation Coordinator
// 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)
}
}
}