{Orchard}
is to iOS what Timber is to Android
- static functions
- support for multiple logger
- automatic capture of file, function, and line information
- contextual logging with tags and custom icons
- optional timestamp logging
- optional occurence logging
Key Features
Setup
# PRESENTING CODE
import Orchard
#if DEBUG
Orchard.loggers.append(ConsoleLogger(level: Level.verbose))
#endif
Orchard.loggers.append(SentryLogger())
Setup
Usage
# PRESENTING CODE
// Focus on convenience
// log only messages
Orchard.v("verbose")
Orchard.i("info")
Orchard.d("debug")
Orchard.w("warning")
Orchard.e("error")
Orchard.f("fatal")
// only errors
let error = HttpError.blacklisted
Orchard.v(error)
Orchard.i(error)
Orchard.d(error)
Orchard.w(error)
Orchard.e(error)
Orchard.f(error)
// messages and errors
Orchard.v("verbose", error)
Orchard.i("info", error)
Orchard.d("debug", error)
Orchard.w("warning", error)
Orchard.e("error", error)
Orchard.f("fatal", error)
// messages and errors
Orchard.v("verbose", error)
Orchard.i("info", error)
Orchard.d("debug", error)
Orchard.w("warning", error)
Orchard.e("error", error)
Orchard.f("fatal", error)
// message and arguments
let args = ["lorem" : "ipsum"]
Orchard.v("verbose", args)
Orchard.i("info", args)
Orchard.d("debug", args)
Orchard.w("warning", args)
Orchard.e("error", args)
Orchard.f("fatal", args)
// message, error and arguments
Orchard.v("verbose", error, args)
Orchard.i("info", error, args)
Orchard.d("debug", error, args)
Orchard.w("warning", error, args)
Orchard.e("error", error, args)
Orchard.f("fatal", error, args)
icon [module] message error arguments
Advanced Setup - Timestamp, Occurrence, Log Level
# PRESENTING CODE
// Configure console logger
let consoleLogger = ConsoleLogger()
consoleLogger.level = Orchard.Level.verbose // default
consoleLogger.showTimesStamp = true // default: false
consoleLogger.showInvocation = true // default: false
Orchard.loggers.append(consoleLogger)
// Add custom loggers
class MyCustomLogger: Orchard.Logger {
// Implement logger methods
}
Orchard.loggers.append(MyCustomLogger())
icon time [module/filename:method:line] message error arguments
Advanced Usage - Sentry Breadcrumbs
Advanced Usage - Tag & Icon
# PRESENTING CODE
// Log with additional context
Orchard.e("Failed to save data", error, ["userId": user.id])
// Use tags for categorization
Orchard.tag("NetworkManager").d("Request started")
// Custom icons
Orchard.icon("🚀").i("App launched")
Advanced Usage - Stack Trace
# PRESENTING CODE
Expectation
Thread.printStackTrace() // does not exist
Advanced Usage - Stack Trace
# PRESENTING CODE
Reality
Thread.callStackSymbols.forEach {
print($0)
}
Advanced Usage - Stack Trace
# PRESENTING CODE
Solution*
Thread.printStackTrace()
*sort of & thanks @Dieter Lutz
{Q/A?}
{Service Locator}
is to iOS what koin is to Android*
*or a lightweight Swinject
- centralised object registry
- object retrieval
- modules, singleton or factory support
- circular dependency detection
- multi-thread safe
- property wrapper
- optional occurence logging
Key Features
Setup - Define Modules
# PRESENTING CODE
class MyModule: ServiceLocatorModule {
override func build() {
// singletons
single(ConfigProviderProtocol.self) {
ConfigProvider()
}
// get dependencies with resolve
single(URLFactory.self) {
URLFactory(remoteConfigProvider: self.resolve())
}
// factory
factory(AppLinkFactory.self) {
let settingsManager: SettingsManager = self.resolve()
return AppLinkFactory(descriptionFactory: settingsManager)
}
}
}
class AppModule: ServiceLocatorModule {
override func build() {
module { MyModule() }
}
}
Setup - Initialize
# PRESENTING CODE
lazy var myServiceLocator = startServiceLocator {
AppModule()
}
myServiceLocator.build()
Usage - Resolve (1/3)
# PRESENTING CODE
class MyCass {
@Inject(myServiceLocator) public var contactSheet: ContactSheet
}
func doSomething() {
@Inject(myServiceLocator) var contactSheet: ContactSheet
// Use contactSheet here
}
func doAnotherThing() {
let contactSheet : ContactSheet = serviceLocator.resolve()
// Use contactSheet here
}
Usage - Resolve by own Property Wrapper (2/3)
# PRESENTING CODE
@propertyWrapper
internal final class PluginInject<T>: Dependency<T> {
public var wrappedValue: T {
resolvedWrappedValue()
}
public init() {
super.init(MyPlugin.shared.myServiceLocator)
}
}
class MyClass {
@PluginInject var contactSheet: ContactSheet
}
func doSomething() {
@PluginInject var contactSheet: ContactSheet
}
Usage - Testing Resolve (3/3)
# PRESENTING CODE
@propertyWrapper
internal final class PluginInject<T>: Dependency<T> {
public var wrappedValue: T {
resolvedWrappedValue()
}
public init() {
#if DEBUG
if isRunningUnitTest {
if TestServiceLocatorProvider.shared == nil {
TestServiceLocatorProvider.shared = startServiceLocator {
PluginModule()
}
}
super.init(TestServiceLocatorProvider.shared)
} else {
super.init(EventsPlugin.shared.serviceLocator)
}
#else
super.init(EventsPlugin.shared.serviceLocator)
#endif
}
}
internal class TestServiceLocatorProvider {
internal static var shared: ServiceLocator!
}
public let isRunningUnitTest: Bool = {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}()
Usage - Logging
Build
Resolve
Usage - Detecting Circularity
import ProfisPartnerCore
class PluginModule: ServiceLocatorModule {
override func build() {
single {
A(b : self.resolve())
}
single {
B(a : self.resolve())
}
}
}
class A {
let b : B
init(b: B) {
self.b = b
}
}
class B {
let a : A
init(a: A) {
self.a = a
}
}
Usage - Detecting Circularity
Limitations & Gotchas
- eager service locator initalization due to threading and circularity detection
- property wrapper is not available in init blocks since they're initialized after the init block
NSRecursiveLock does not work well with Main Thread -> deadlocks particularly in observable objects in SwiftUIs
use resolve() instead
{Q/A?}
{Khan}
is to iOS what androidx.startup is to Android
- automated dependency resolution
- simplified and concurrent initialization
- optimized startup time
- priority management
- circular dependency detection
- optional logging
Key Features
Setup - Create Initializers & Set Dependency
# PRESENTING CODE
class SentryInitializer: Initializer {
static var dependencies: [Initializer.Type] = []
static func embark() async throws {
Orchard.loggers.append(SentryLogger())
}
}
class LoggingInitializer: Initializer {
static var dependencies: [Initializer.Type] = [SentryInitializer.self]
static func embark() async throws {
#if DEBUG
Orchard.loggers.append(ConsoleLogger())
#endif
}
}
class DependencyInitializer: Initializer {
static var dependencies: [Initializer.Type] = [LoggingInitializer.self]
@MainActor
static func embark() async throws {
CoreAppDelegate.shared.serviceLocator = startServiceLocator {
AppModule()
}
await CoreAppDelegate.shared.serviceLocator!.build()
}
}
Usage - Create Khan instance & conquer()
# PRESENTING CODE
let initializers: [Initializer.Type] = [
LoggingInitializer.self,
AppDataInitializer.self,
SentryInitializer.self,
// Add other initializers
]
do {
let khan = try Khan(initializers: initializers)
try await khan.conquer()
} catch {
print("Initialization failed: \(error)")
}
Advanced Usage - printDependencyTree
# PRESENTING CODE
let initializers: [Initializer.Type] = [
LoggingInitializer.self,
AppDataInitializer.self,
SentryInitializer.self,
// Add other initializers
]
do {
let khan = try Khan(initializers: initializers)
try await khan.conquer()
} catch {
print("Initialization failed: \(error)")
}
Advanced Usage - Detect Circularity
# PRESENTING CODE
class SentryInitializer: Initializer {
static var dependencies: [Initializer.Type] = [LoggingInitializer.self]
static func embark() async throws {
Orchard.loggers.append(SentryLogger())
}
}
Profis App
- Orchard - improves debugging & monitoring & less breakpoint-driven development
- Service Locator - gives you better control about object lifecycles, sharing caches, less object waste
- Khan - sanitizes app start steps and shifts focus to what should happen and what should be completed before