Welcome to Orchard - your new best friend for logging on iOS! ππ±
Inspired by the clever wordplay behind Timber, the Android logging system (logs and timber, get it?), we decided to bring a slice of that punny brilliance to iOS with Orchard. After all, what's more fitting than logging apples in the world of iPhones?
But let's get serious for a moment. Just because you're developing on iOS doesn't mean your logging has to be... shall we say, subpar? With Orchard, you can elevate your logging game with style and efficiency.
Orchard is a versatile logging system for Swift applications, designed to provide flexible and contextual logging capabilities.
- Key Features
- π± Demo App
- Quick Start
- Usage Guide
- Advanced Configuration
- How to install
- Requirements
- License
- π― Six Log Levels: verbose, debug, info, warning, error, and fatal
- π Multiple Logging Backends: Support for custom loggers (Console, File, Remote, etc.)
- π·οΈ Tags: Categorize logs by module, feature, or context
- π¨ Custom Icons: Visual indicators for different log types
- π Automatic Invocation Tracking: Captures file, function, and line information
- β° Timestamp Support: Optional millisecond-precision timestamps
- π Flexible Parameters: Any combination of message, error, and arguments
- π§΅ Thread-Safe: Safe for concurrent logging operations
Want to see Orchard in action? Check out the iOS Demo App:
cd Examples/OrchardDemo
open OrchardDemo.xcodeprojPress β+R in Xcode to run the interactive demo with live log display!
Add the Orchard package to your Swift project via Swift Package Manager.
import Orchard
// Configure console logger with optional features
let consoleLogger = ConsoleLogger()
consoleLogger.showTimesStamp = true // Show timestamps
consoleLogger.showInvocation = true // Show file, function, line
Orchard.loggers.append(consoleLogger)Never wonder where a log came from again. Orchard automatically captures file, function, and line:
Orchard.i("User logged in")
// Output: βΉοΈ 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():168] User logged in
// ^ module ^^ ^^ file ^^ ^^^ function ^^^ ^^ line number ^^^^^^ messageToggle invocation details on/off:
let logger = ConsoleLogger()
logger.showInvocation = true // Shows full path: /Module/function():line
logger.showInvocation = false // Shows only module nameMix and match any combination of message, error, and arguments:
// Message only
Orchard.i("App started")
// Error only
Orchard.e(error)
// Message + Error
Orchard.e("Failed to load data", error)
// Message + Arguments
Orchard.i("User action", ["action": "tap", "button": "submit"])
// Error + Arguments
Orchard.e(error, ["context": "startup", "retry": "false"])
// Message + Error + Arguments
Orchard.e("Request failed", error, ["url": "api.example.com", "method": "POST"])
// Everything: tag, icon, message, error, and arguments
Orchard.tag("API").icon("π₯").e("Critical failure", error, ["endpoint": "/users", "status": "500", "retry": "3"])Log to multiple destinations simultaneously:
// Send logs to console, file, and remote server at once
Orchard.loggers.append(ConsoleLogger())
Orchard.loggers.append(FileLogger())
Orchard.loggers.append(RemoteLogger())
// Every log call reaches all registered loggers
Orchard.i("User logged in")
// β Appears in console
// β Written to log file
// β Sent to analytics serverCustomize what information appears in your logs:
let consoleLogger = ConsoleLogger()
// Enable/disable timestamps
consoleLogger.showTimesStamp = true // Shows: 13:54:48.402:
// Enable/disable invocation details
consoleLogger.showInvocation = true // Shows: /Filename.function:line
Orchard.loggers.append(consoleLogger)For more control, use ConsoleLoggerConfig to customize timestamp format and module name mapping.
Option 1: Builder-style configuration (recommended)
let consoleLogger = ConsoleLogger { config in
config.showTimestamp = true
config.timestampFormat = "yyyy-MM-dd HH:mm:ss.SSS" // Custom date format
config.showInvocation = true
config.moduleNameMapper = { moduleName in
// Custom module name transformation
// The mapper receives the extracted module name and can transform it
return moduleName.replacingOccurrences(of: "OrchardDemo", with: "Demo")
}
}
Orchard.loggers.append(consoleLogger)Option 2: Config struct initialization
let config = ConsoleLoggerConfig(
showTimestamp: true,
timestampFormat: "yyyy-MM-dd HH:mm:ss.SSS", // Custom date format
showInvocation: true,
moduleNameMapper: { moduleName in
// Custom module name transformation
return moduleName.components(separatedBy: "/").last ?? moduleName
}
)
let consoleLogger = ConsoleLogger(config: config)
Orchard.loggers.append(consoleLogger)Option 3: Modify config after initialization
let consoleLogger = ConsoleLogger()
consoleLogger.config.timestampFormat = "HH:mm:ss"
consoleLogger.config.moduleNameMapper = { moduleName in
return "[\(moduleName.uppercased())]"
}
Orchard.loggers.append(consoleLogger)Configuration Options:
showTimestamp: Enable/disable timestamps (default:true)timestampFormat: DateFormatter format string (default:"HH:mm:ss.SSS")showInvocation: Show file/function/line details (default:true)moduleNameMapper: Transform module names with a custom closure (default:nil)- The closure receives the extracted module name from
fileId.moduleNameFromFile - Return the transformed module name to display in logs
- The closure receives the extracted module name from
Example with custom module name mapper:
// Shorten module names: "MyApp/ViewModels/UserViewModel" β "UserViewModel"
let logger = ConsoleLogger { config in
config.moduleNameMapper = { moduleName in
return moduleName.components(separatedBy: "/").last ?? moduleName
}
}
Orchard.loggers.append(logger)
Orchard.i("User logged in")
// Output: βΉοΈ 13:54:48.403: [UserViewModel/login:42] User logged inThe killer feature: combine tags and custom icons for instantly recognizable, categorized logs:
// Payment processing
Orchard.tag("Payment").icon("π³").i("Payment processed: $99.99")
// Output: π³ 13:54:48.403: [Payment/ContentView.runAllExamples():184] Payment processed: $99.99
// Analytics tracking
Orchard.tag("Analytics").icon("π").d("Event tracked: button_click")
// Output: π 13:54:48.403: [Analytics/ContentView.runAllExamples():185] Event tracked: button_click
// Network monitoring
Orchard.tag("Network").icon("π").i("Connected to server")
// Output: π 13:54:48.403: [Network/ContentView.runAllExamples():181] Connected to serverLog errors with full context - message, error object, and metadata in one call:
enum NetworkError: Error, CustomStringConvertible {
case timeout
case connectionFailed(reason: String)
var description: String {
switch self {
case .timeout:
return "NetworkError: Request timed out"
case .connectionFailed(let reason):
return "NetworkError: Connection failed - \(reason)"
}
}
}
// Message with error
Orchard.e("Operation failed", NetworkError.connectionFailed(reason: "Host unreachable"))
// Output: β 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():201] Operation failed NetworkError: Connection failed - Host unreachable
// Full context: tag, icon, message, and error
Orchard.tag("Network").icon("β οΈ").e("Request timeout", NetworkError.timeout)
// Output: β οΈ 13:54:48.413: [Network/ContentView.runAllExamples():202] Request timeout NetworkError: Request timed out
// Add metadata for debugging
Orchard.tag("API").icon("π₯").e("Request failed", NetworkError.timeout, ["endpoint": "/users", "retry": "3"])
// Output: π₯ 13:54:48.413: [API/ContentView.runAllExamples():203] Request failed NetworkError: Request timed out {"endpoint":"/users","retry":"3"}Attach key-value pairs for rich, queryable logs - perfect for analytics and debugging:
// User events
Orchard.i("User logged in", ["userId": "12345", "userName": "John", "method": "oauth"])
// Output: βΉοΈ 13:54:48.413: [OrchardDemo/ContentView.runAllExamples():205] User logged in {"userId":"12345","userName":"John","method":"oauth"}
// Performance metrics
Orchard.tag("Performance").d("API response", ["duration": "245ms", "endpoint": "/api/users", "status": "200"])
// Output: π 13:54:48.413: [Performance/ContentView.runAllExamples():206] API response {"duration":"245ms","endpoint":"/api/users","status":"200"}
// Error context
Orchard.tag("Database").e("Query failed", error, ["query": "SELECT * FROM users", "retry": "false"])Professional logging with built-in visual hierarchy:
Orchard.v("Verbose: Detailed debug information")
// Output: π¬ 13:54:48.402: [OrchardDemo/ContentView.runAllExamples():166] Verbose: Detailed debug information
Orchard.d("Debug: Development information")
// Output: π 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():167] Debug: Development information
Orchard.i("Info: General information")
// Output: βΉοΈ 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():168] Info: General information
Orchard.w("Warning: Something needs attention")
// Output: β οΈ 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():169] Warning: Something needs attention
Orchard.e("Error: Something went wrong")
// Output: β 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():170] Error: Something went wrong
Orchard.f("Fatal: Critical error!")
// Output: β‘οΈ 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():171] Fatal: Critical error!Categorize logs by module, feature, or any context you need:
// Network operations
Orchard.tag("Network").i("HTTP request completed successfully")
// Output: βΉοΈ 13:54:48.403: [Network/ContentView.runAllExamples():174] HTTP request completed successfully
// Database operations
Orchard.tag("Database").d("Query executed in 45ms")
// Output: π 13:54:48.403: [Database/ContentView.runAllExamples():175] Query executed in 45ms
// Authentication
Orchard.tag("Auth").w("Token will expire in 5 minutes")
// Output: β οΈ 13:54:48.403: [Auth/ContentView.runAllExamples():176] Token will expire in 5 minutesOverride default level icons for visual categorization:
Orchard.icon("π").i("App launched successfully")
// Output: π 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():179] App launched successfully
Orchard.icon("πΎ").d("Data saved to disk")
// Output: πΎ 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():180] Data saved to disk
Orchard.icon("π").w("Certificate will expire soon")
// Output: π 13:54:48.403: [OrchardDemo/ContentView.runAllExamples():181] Certificate will expire soonCreate your own logger by implementing the Orchard.Logger protocol:
class FileLogger: Orchard.Logger {
let level: Orchard.Level = .info
var tag: String?
var icon: Character?
func info(_ message: String?, _ error: Error?, _ args: [String: CustomStringConvertible]?,
_ file: String, _ fileId: String, _ function: String, _ line: Int) {
// Write to file...
let logEntry = "\(Date()): [\(tag ?? "App")] \(message ?? "")"
// Append to file
}
// Implement other level methods (verbose, debug, warning, error, fatal)...
}
Orchard.loggers.append(FileLogger())| Latest Version |
|---|
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/kibotu/Orchard", from: "<latest-version>"),
],
targets: [
.target(
name: "YourTarget",
dependencies: ["Orchard"]
)
]Or add it directly in Xcode:
- File β Add Package Dependencies
- Enter:
https://github.com/kibotu/Orchard - Select the latest version shown above
- iOS 15.0 or later
- Xcode 16.0 or later
Contributions welcome!
Copyright 2024 Jan Rabe & CHECK24 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
