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
| Pattern | When to Use | When to Avoid | SwiftUI Integration |
|---|---|---|---|
| Singleton | Global configuration, shared resources | When you need multiple instances | ObservableObject + @Published |
| Observer | UI updates, data binding | Simple one-way data flow | Built-in with @Published |
| MVVM | Complex business logic | Simple static views | @StateObject + ObservableObject |
| Factory | Complex object creation | Simple object initialization | ViewBuilder 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
- SwiftUI Documentation - Official Apple documentation
- Swift by Sundell - Advanced Swift patterns and best practices
- Design Patterns: Elements of Reusable Object-Oriented Software by Gang of Four
- Clean Architecture in Swift by objc.io
- Design Patterns in Swift - Open source examples