# Logger System: Perfect for MVVM and Clean Architecture
Table of Contents
Across different iOS projects, I’ve long wanted a good logging system that really fits my architecture. Each app naturally has different needs that I had to abstract more or less.
What emerged from this: A lightweight logging system that gets by with Swift’s native capabilities and was specifically developed for modern iOS architecture patterns like MVVM and Clean Architecture. No external dependencies, just clean Swift code that integrates seamlessly into existing projects.
Logger Categories by Architecture LayerLink to heading
Each layer in Clean Architecture gets its own logger categories. This ensures clean separation and makes debugging individual architecture layers much easier.
Clean Architecture Layer | Logger Category | Purpose |
---|---|---|
Presentation Layer | .ui , .viewModel | Views, ViewModels, UI-State |
Domain Layer | .general , .auth | Use Cases, Business Logic |
Data Layer | .data , .network | Repositories, External APIs |
Infrastructure | .performance , .manual | Cross-cutting Concerns |
Log Levels OverviewLink to heading
Using the different log levels correctly is crucial for effective debugging and production monitoring. Here’s a practical overview of the available levels.
Level | Purpose | When to Use |
---|---|---|
Debug | Detailed execution flow | Development debugging |
Info | General information | Important app events |
Warning | Potential issues | Recoverable problems |
Error | Serious problems | Unrecoverable errors |
Critical | System failures | App-breaking issues |
Implementation Examples by LayerLink to heading
Let’s look at how logging is properly implemented in each architecture layer. These are simplified examples showing which logger category and typical logging patterns are used for each layer. In real projects, you’d naturally have more complex logic and additional error handling.
Presentation Layer - ViewModelLink to heading
ViewModels manage UI state and user interactions. This example shows basic logging for a ViewModel. Use .viewModel
category to track state changes and user actions without cluttering logs with UI framework details.
class ProductDetailViewModel: ObservableObject { private let logger = Logger.viewModel
func loadProduct(id: String) { logger.info("Loading product: \(id)") // Your actual implementation would include state management, // error handling, and interaction with use cases }}
Domain Layer - Use CaseLink to heading
Use Cases contain your core business logic and should be framework-independent. This example demonstrates basic Use Case logging. The .auth
and .general
categories help track business operations without external dependencies.
class AuthenticateUserUseCase { private let logger = Logger.auth
func execute(credentials: Credentials) -> AuthResult { logger.info("Authentication attempt for: \(credentials.username)") // Real implementation would include validation, repository calls, // token management, and comprehensive error handling }}
Data Layer - RepositoryLink to heading
Repositories abstract data access and caching logic. This simplified example shows Repository logging patterns. Use .data
category to track cache hits, data transformations, and storage operations separately from network calls.
class ProductRepository { private let logger = Logger.data
func fetchProduct(id: String) async -> Product? { logger.debug("Fetching product from cache: \(id)") // Production code would include cache management, data mapping, // fallback strategies, and coordination with network services }}
Infrastructure Layer - Network ServiceLink to heading
Network services handle external API communication. This basic example illustrates Network Service logging. The .network
category isolates connectivity issues and API responses from your business logic layers.
class NetworkService { private let logger = Logger.network
func performRequest(_ request: URLRequest) async { logger.info("API call: \(request.url?.absoluteString ?? "unknown")") // Real implementation would handle authentication, retries, // response parsing, connectivity checks, and timeout management }}
Configuration StrategiesLink to heading
Different environments need different logging strategies. Here I’ll show how to configure the logger system for various development phases and deployment scenarios.
Development EnvironmentLink to heading
During development, you want detailed information for debugging. Enable verbose logging for the layers you’re currently working on, while keeping other layers at higher levels to reduce noise.
// Debug specific layers you're working onLogConfiguration.shared.setLevel(.debug, for: .viewModel) // See all state changesLogConfiguration.shared.setLevel(.info, for: .data) // Track data flowLogConfiguration.shared.setLevel(.warning, for: .network) // Only network issues
This setup helps you focus on your current work area without being overwhelmed by irrelevant logs from other layers.
Production EnvironmentLink to heading
In production, performance and storage are critical. Keep logging minimal but capture enough information for troubleshooting critical issues.
// Minimal logging for performanceLogConfiguration.shared.setGlobalLevel(.warning) // Only warnings and aboveLogConfiguration.shared.setLevel(.error, for: .network) // Critical network failures
This ensures your app runs smoothly while still capturing serious problems that need immediate attention.
Testing EnvironmentLink to heading
During testing, focus logging on the specific functionality being tested. This makes test failures easier to diagnose and keeps test logs clean.
// Focus on specific test scenariosLogConfiguration.shared.setLevel(.debug, for: .auth) // Authentication flow testingLogConfiguration.shared.setLevel(.error, for: .ui) // Hide UI noise during unit tests
For integration tests, you might enable .debug
for multiple layers to trace the complete flow through your architecture.
Advanced ConfigurationLink to heading
For production apps, consider building a simple settings UI that allows real-time log level adjustments during development and testing phases. This enables dynamic debugging without app restarts and gives you fine-grained control over logging output.
// Example: Settings-based configurationstruct LoggerSettingsView: View { @State private var selectedLevel: LogLevel = .info
var body: some View { Picker("Log Level", selection: $selectedLevel) { ForEach(LogLevel.allCases, id: \.self) { level in Text(level.description) } } .onChange(of: selectedLevel) { newLevel in LogConfiguration.shared.setGlobalLevel(newLevel) } }}
ConclusionLink to heading
This logger system provides a clean, architecture-focused approach to debugging iOS apps, completely without external dependencies. Built entirely with Swift’s native capabilities, it integrates seamlessly into MVVM and Clean Architecture patterns while maintaining clear separation of concerns across all layers.
The category-based structure enables precise debugging strategies and scales from development all the way to production environments. Perfect for teams who value clean code and architectural integrity.
Get the complete implementation here: GitHub Gist