{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

Conclusion

{Q/A?}

Code

By Jan Rabe