In-app logging + diagnostics primitives for iOS / macOS apps. UI-free by design — you bring the SwiftUI views, DebugKit gives you the observable ring buffers and the structured log facade behind them.
| Type | Role |
|---|---|
AppLogger |
Process-wide facade. Calls land in os.Logger (Console.app) and optionally fan out to a sink closure. |
LogEntry |
Structured payload for one log line (level, category, message, metadata, source location). |
LogLevel |
Closed enum: trace / debug / info / notice / warning / error / critical. |
LogCategory, LogSubcategory |
String-backed wrappers. Extend with your own cases per app. |
DiagnosticEntry, DiagnosticsCollector |
@Observable ring buffer of warning/error events for the in-app inspector. |
LogStreamCollector |
@Observable ring buffer of every log entry — full chronological stream. |
UI is intentionally not included. Each consuming app builds its own SwiftUI inspector against the observables.
.package(url: "https://github.com/berardino95/DebugKit.git", from: "0.1.0")Then import:
import DebugKitimport DebugKit
extension LogCategory {
static let routing: LogCategory = "routing"
static let coreData: LogCategory = "coreData"
static let ui: LogCategory = "ui"
}The package ships .general and .lifeCycle; everything else is your
vocabulary.
import DebugKit
// Use the global `appLogger`, or instantiate `AppLogger(subsystem:)`
// for advanced setups.
appLogger.info("App launched", category: .lifeCycle)
do {
try await save(itinerary)
} catch {
appLogger.error(
"Save failed",
category: .coreData,
subcategory: .save,
metadata: [
"id": itinerary.id.uuidString,
"error": error.localizedDescription,
]
)
}@MainActor
final class AppDelegate: NSObject, UIApplicationDelegate {
let diagnostics = DiagnosticsCollector()
let stream = LogStreamCollector()
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
#if DEBUG
appLogger.sink = { [diagnostics, stream] entry in
Task { @MainActor in
stream.record(entry)
guard entry.level >= .warning else { return }
diagnostics.record(.init(
source: entry.category?.rawValue ?? "App",
severity: entry.level == .warning ? .warning : .error,
debugMessage: "\(entry.fileName):\(entry.line) \(entry.function)"
))
}
}
#endif
return true
}
}DebugKit doesn't enforce a convention but rewards one. The pattern that
plays well with the structured metadata field:
do {
try await someCriticalOperation()
} catch {
appLogger.error(
"Short human description",
category: .coreData,
subcategory: .save,
metadata: [
"id": "...",
"error": error.localizedDescription, // Apple-localized
]
)
throw error
}Log at the boundary where the error stops propagating; let intermediate layers re-throw.
Both collectors can serialize their current buffer. DebugKit does no
disk I/O — you get a String or Data and decide where it goes
(ShareLink, .fileExporter, upload, sandbox URL, …).
// Plain-text, one line per entry. Good for .log / .txt attachments.
let text = stream.exportedText()
// JSON array of LogEntry — stable, machine-readable, pretty-printed.
let data = try stream.exportedJSON()DiagnosticsCollector exposes the same two methods over
DiagnosticEntry. Text lines look like:
2026-05-22T10:12:33.421Z [warning] coreData/save MyView.swift:42 save(_:) — Save failed {error=..., id=42}
SwiftUI example:
ShareLink(
item: stream.exportedText(),
preview: SharePreview("debug.log")
)LogEntry, DiagnosticEntry, LogLevel, LogCategory and
LogSubcategory are all Codable, so you can plug them into your own
encoder if the bundled JSON shape doesn't fit.
iOS 17 / macOS 14 / tvOS 17 / watchOS 10 / visionOS 1. Requires Swift 6.
MIT.