Interactive Guide: Design Patterns in Swift – Modern SwiftUI Best Practices

20 min read • Updated for Swift 5.9 & SwiftUI

Why Design Patterns Matter in Swift

Design patterns aren't just academic concepts—they're battle-tested solutions that make your Swift code more maintainable, testable, and scalable. Think of them as blueprints that help you avoid common pitfalls while leveraging Swift's powerful features like protocols, generics, and property wrappers.

What You'll Learn:

  • When to use (and when NOT to use) each pattern
  • Modern SwiftUI implementations with @StateObject and @ObservedObject
  • Thread safety and testing considerations
  • Real-world examples you can use in production

Interactive Pattern Explorer

Select a pattern below to see its implementation, use cases, and potential pitfalls:

Singleton Pattern

Ensures only one instance exists. Great for shared resources, but use sparingly to avoid global state issues.

Singleton Pattern - Basic vs SwiftUI-Compatible
// ❌ Basic Singleton (problematic for SwiftUI)
class SettingsManager {
    static let shared = SettingsManager()
    private init() {}
    var isDarkModeEnabled: Bool = false
}

// ✅ SwiftUI-Compatible Singleton
class SettingsManager: ObservableObject {
    static let shared = SettingsManager()
    private init() {}
    
    @Published var isDarkModeEnabled: Bool = false
    
    // Thread-safe property access
    private let queue = DispatchQueue(label: "settings.queue")
    private var _userPreferences: [String: Any] = [:]
    
    func setPreference<T>(_ key: String, value: T) {
        queue.async {
            self._userPreferences[key] = value
        }
    }
}

When NOT to Use:

  • When you need multiple instances for testing
  • For stateless utility functions (use static methods instead)
  • When dependency injection would be cleaner

Complete SwiftUI Demo App

Here's a production-ready example that combines all patterns with modern Swift features:

Complete SwiftUI Demo App - All Patterns Combined
import SwiftUI
import Combine

// MARK: - Models
struct Task: Identifiable, Codable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
    let createdAt = Date()
}

// MARK: - Singleton Service
class TaskService: ObservableObject {
    static let shared = TaskService()
    private init() {}
    
    @Published var allTasks: [Task] = []
    
    func addTask(_ task: Task) {
        allTasks.append(task)
    }
    
    func toggleTask(_ task: Task) {
        if let index = allTasks.firstIndex(where: { $0.id == task.id }) {
            allTasks[index].isCompleted.toggle()
        }
    }
}

// MARK: - MVVM ViewModel
@MainActor
class TaskListViewModel: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var newTaskTitle: String = ""
    @Published var showCompleted: Bool = true
    
    private let taskService = TaskService.shared
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // Observer pattern: Subscribe to service changes
        taskService.$allTasks
            .sink { [weak self] tasks in
                self?.updateFilteredTasks(tasks)
            }
            .store(in: &cancellables)
    }
    
    private func updateFilteredTasks(_ allTasks: [Task]) {
        tasks = showCompleted ? allTasks : allTasks.filter { !$0.isCompleted }
    }
    
    func addTask() {
        guard !newTaskTitle.isEmpty else { return }
        
        // Factory pattern: Create task
        let task = TaskFactory.createTask(title: newTaskTitle)
        taskService.addTask(task)
        newTaskTitle = ""
    }
    
    func toggleTask(_ task: Task) {
        taskService.toggleTask(task)
    }
}

// MARK: - Factory
enum TaskFactory {
    static func createTask(title: String, type: TaskType = .regular) -> Task {
        var task = Task(title: title)
        
        switch type {
        case .urgent:
            task.title = "🚨 " + task.title
        case .regular:
            break
        }
        
        return task
    }
}

enum TaskType {
    case regular, urgent
}

// MARK: - SwiftUI Views
struct ContentView: View {
    @StateObject private var viewModel = TaskListViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                TaskInputView(
                    text: $viewModel.newTaskTitle,
                    onAdd: viewModel.addTask
                )
                
                Toggle("Show Completed", isOn: $viewModel.showCompleted)
                    .padding(.horizontal)
                
                List(viewModel.tasks) { task in
                    TaskRow(task: task) {
                        viewModel.toggleTask(task)
                    }
                }
            }
            .navigationTitle("Modern Task App")
        }
    }
}

Testing Your Patterns

Good patterns should be testable. Here's how to make your code test-friendly:

Testing Best Practices:

  • Use protocol abstractions for singletons to enable mocking
  • Inject dependencies in ViewModels for easier testing
  • Test business logic separately from UI code
  • Use @MainActor testing methods for SwiftUI ViewModels
Testing Design Patterns - Testable Singleton and Unit Tests
// Testable Singleton with Protocol
protocol SettingsManagerProtocol {
    var isDarkModeEnabled: Bool { get set }
}

class SettingsManager: SettingsManagerProtocol, ObservableObject {
    static let shared: SettingsManagerProtocol = SettingsManager()
    @Published var isDarkModeEnabled: Bool = false
    private init() {}
}

// Mock for testing
class MockSettingsManager: SettingsManagerProtocol {
    var isDarkModeEnabled: Bool = false
}

// Unit Test
@MainActor
class TaskListViewModelTests: XCTestCase {
    func testAddTask() async {
        let mockService = MockTaskService()
        let viewModel = TaskListViewModel(taskService: mockService)
        
        viewModel.newTaskTitle = "Test Task"
        viewModel.addTask()
        
        XCTAssertEqual(mockService.tasks.count, 1)
        XCTAssertEqual(mockService.tasks.first?.title, "Test Task")
    }
}

Advanced Considerations

Pattern Comparison

PatternWhen to UseWhen to AvoidSwiftUI Integration
SingletonGlobal configuration, shared resourcesWhen you need multiple instancesObservableObject + @Published
ObserverUI updates, data bindingSimple one-way data flowBuilt-in with @Published
MVVMComplex business logicSimple static views@StateObject + ObservableObject
FactoryComplex object creationSimple object initializationViewBuilder functions

Tools & Resources

Development Tools

  • Xcode with SwiftUI Previews for rapid prototyping
  • Swift Package Manager for modular architecture
  • XCTest for unit testing patterns
  • Instruments for performance profiling

Learning Resources

  • Swift by Sundell - Advanced Swift patterns
  • Apple's SwiftUI documentation
  • objc.io - Functional Swift patterns
  • RayWenderlich - iOS architecture tutorials

Key Takeaways

  • Start Simple: Don't over-engineer. Use patterns when they solve real problems.
  • SwiftUI First: Leverage @StateObject, @ObservedObject, and @Published for reactive patterns.
  • Test Early: Design patterns should make testing easier, not harder.
  • Performance Matters: Consider the performance implications of each pattern.
  • Modern Swift: Use async/await, actors, and other modern Swift features with patterns.

Further Reading