Skip to main content
Full technical details in your project at /docs/modules/Core.md

What’s new

  • v2.2.0 — the localized error surface lives next to Core: AppError.localizedUserMessage (en + es) is provided by the Localization package, and every UI error call site now displays it instead of the raw userMessage. See the AI and Localization modules for how this threads through the app.
  • v2.0DeepLinkBus (in Core) is @Observable. Consumers that used .onReceive($bus.latest) should migrate to .onChange(of: bus.latest). SwiftUI’s observation machinery does the rest.
  • Swift 6 strict concurrency across the package.

What You Get

  • AppError enum - Maps all system errors to user-friendly messages
  • AppLogger - OSLog-based with automatic PII redaction
  • ResultOrError - Functional error handling patterns
  • Theme types - ThemeManager with a Theme enum (system, light, dark, aurora, obsidian)
  • Zero dependencies - Pure foundation layer
Time saved: 6-10 hours of error handling boilerplate, logging infrastructure, and testing.

Key Components

AppError (Production Type)

Here’s a shortened version of the production enum from the boilerplate:
public enum AppError: Error, Equatable, CustomStringConvertible, LocalizedError, Sendable {
    case network(code: Int, message: String?)
    case decoding
    case unauthorized
    case rateLimited(retryAfter: TimeInterval?)
    // Additional cases omitted for brevity.

    // User-friendly messages
    public var userMessage: String {
        switch self {
        case .network(_, let message) where message?.localizedCaseInsensitiveContains("offline") == true:
            return "You're offline. Please check your internet connection."
        case .network(_, let message) where message?.localizedCaseInsensitiveContains("timeout") == true:
            return "Request timed out. Please try again."
        case .unauthorized:
            return "Invalid email or password. Please check your credentials and try again."
        case .rateLimited:
            return "Too many requests. Please wait a moment and try again."
        // ... handles all cases with actionable messages
        }
    }
}
SignInViewModel:
// From SwiftAIBoilerplatePro/AppShell/SignInView.swift
func signInWithApple() async {
    do {
        _ = try await authClient.signInWithApple()
        AppLogger.info("Apple sign in successful", category: AppLogger.ui)
        errorMessage = nil
    } catch {
        let appError = AppError.from(error)            // Auto-maps to AppError
        errorMessage = appError.localizedUserMessage   // Localized (en + es) message for the UI
        AppLogger.error("Apple sign in failed: \(error)", category: AppLogger.ui)
    }
}
localizedUserMessage is the localized counterpart to AppError.userMessage, supplied by the Localization package and used by every UI error call site. Production Value:
  • ✅ Auto-maps URLError, NSError, custom errors
  • ✅ Preserves technical details for logging
  • ✅ Provides actionable user messages
  • ✅ Sendable for Swift 6 compatibility
  • ✅ Equatable for testing

AppLogger

Structured logging with PII redaction:
// Usage
AppLogger.debug("Message sent", category: AppLogger.ui)
AppLogger.info("API call succeeded", category: AppLogger.networking)
AppLogger.error("Failed to save", category: AppLogger.storage)

// Redact sensitive data before logging
AppLogger.debug("API Key: \(AppLogger.redacted(apiKey))", category: AppLogger.networking)
Categories (each a Logger; pass as category:):
  • AppLogger.networking - HTTP requests
  • AppLogger.ai - LLM interactions
  • AppLogger.ui - User interface events
  • AppLogger.feature - Feature operations
  • AppLogger.storage - SwiftData / Keychain / persistence
  • AppLogger.notifications - Notification operations
  • AppLogger.auth - Authentication flows
  • AppLogger.payments - Subscriptions and purchases
AppLogger.subsystem is the shared subsystem identifier (a String), not a category.

Customization Examples

Add Custom Error Type

// In AppError
case myFeature(reason: MyFeatureErrorReason)

enum MyFeatureErrorReason {
    case invalidInput
    case processingFailed
}

// Add user message
var userMessage: String {
    switch self {
    case .myFeature(.invalidInput):
        return "Please check your input"
    case .myFeature(.processingFailed):
        return "Processing failed. Please try again"
    }
}

Add Custom Log Category

import OSLog

extension AppLogger {
    static let myFeature = Logger(
        subsystem: subsystem,
        category: "MyFeature"
    )
}

// Use it
AppLogger.info("Feature initialized", category: AppLogger.myFeature)

Key Files

ComponentLocation
AppErrorPackages/Core/Sources/Core/AppError.swift
AppLoggerPackages/Core/Sources/Core/AppLogger.swift
DiagnosticsPackages/Core/Sources/Core/Diagnostics/

Dependencies

  • None - Core has zero dependencies (by design)

Used By

  • ✅ All other modules (foundation layer)
  • ✅ ViewModels (error handling)
  • ✅ Services (logging)

Best Practices

  • Always map errors to AppError
  • Use specific error reasons
  • Provide actionable user messages
  • Log technical details separately
  • Prefer AppLogger over print() in app code (print() is fine in #Preview blocks)
  • Choose appropriate log level
  • Use correct category
  • Redact PII automatically

Learn More

Full Documentation

Complete Core guide

Networking Module

Uses Core for errors and logging

Storage Module

Uses Core for error handling

Architecture

See how Core fits in

Test Coverage

95%+ - Core is foundational, so it carries some of the highest coverage in the codebase. Its tests run alongside everything else in the single workspace pass: ~598 tests across 12 package test targets (the 11 reusable packages plus the TestSupport test-infrastructure package) and the app test suites, all driven by one Boilerplate.xctestplan run. Tests include:
  • Error mapping scenarios
  • Logger output verification
  • PII redaction