Service Customization

In many cases, customizing the behaviour of the Rover SDK will involve either creating custom implementations of Rover protocols, or perhaps extending and overriding existing Rover objects. Thus, you will need to register your own factories into the SDK’s built-in dependency injection system to provide them to the rest of Rover.

See the Deep Dive section below for greater details.


Creating your Custom Implementation of a Rover Service

Start by creating your own implementation of a service from the Rover platform. We’ll use a hypothetical service here, RoverReplaceableService, for illustration.

/// Our implementation of the hypothetical RoverReplaceableService:
class MyOverriddenRoverReplaceableService: RoverReplaceableService {
    init(neededRoverDependency: NeededRoverDependency) {
    }

    /* overridden methods here */
}

Creating an Assembler

The way to expose your own implementation created above to Rover is by creating your own custom Assembler akin to those found in the Rover modules. Assemblers work by registering all of the components provided by a Rover module.

A subsequent Assembler can override registered factories from a former one, which is key for overriding Rover services.

Within your Assembler you’ll want to register a Factory closure to create an instance of your implementation within your custom Assembler (assuming that the context of your call to Rover.initialize() is in your AppDelegate).

// now we'll create an Assembler (that is, a dependency injection module) here
// to override a previous one, in this case in the form of an extension to an
// AppDelegate. Ensure that you add it in the list after the official Rover
// Assembler that originally adds the service you wish to override!

/// Extending our AppDelegate to be a custom Assembler.
extension AppDelegate: Assembler {
    public func assemble(container: Container) {
        container.register(
            RoverReplaceableService.self
        ) { resolver in
            // This closure body is the Factory.  Now we'll construct the
            // instance we want to provide for this type, and let it be returned:
            MyOverriddenRoverReplaceableService(
                // the resolver allows you to look up any dependencies from
                // the Rover object graph.
                resolver.resolve(NeededRoverDependency.self)!
            )
        }
    }
}

/// Your AppDelegate itself.
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 
        /* ... */

        Rover.initialize(
            assemblers: [
                // The standard Rover assemblers, as usual.
                /* ..., */

                // now we'll add our AppDelegate itself, extended to be an
                // Assembler above, here to override one of the previous
                // built-in Assemblers.

                // Ensure that you add it in the list after the official Rover
                // assembler that originally adds the service you wish to override!
                self
            ]
        )
    }
}

If a given implementation of that object included with the Rover SDK allows itself to be extended, you may override that instead. Many Rover services are resolved through their protocols, though, so in that case you’ll want to be sure to register your overridden version by the protocol type.


Deep Dive: The Dependency Injection Framework

The Rover SDK’s internal dependency injection system serves to keep track of and construct all of the various components of the SDK, henceforth referred to as Services. Note that Rover refers to all of its internal objects as Services, be they stateful singletons or stateless transient view models.

The Rover.shared singleton is thusly a Dependency Injection Container, which globally holds the entire Rover object graph.

Each Rover module (e.g. Foundation, Experiences, Location, and so on) comes with an Assembler that will register() into the Container a Factory for each of the object types they are responsible for, possibly assigning them names to disambiguate differently configured variants of the same type. This is done through the Assembler’s assemble method.

The Factories are simply closures that return newly minted object instances of their types. The Factory closures are given a Resolver from which they can request any needed dependencies. They are lazily executed when something tries to resolve a dependency, and thus the entire object graph is instantiated lazily as needed.

The Assemblers themselves also can contain some startup logic to execute at registration time (that is, at app startup) to run any side effects, like setting themselves up to receive events from iOS or third party frameworks (e.g., location updates), through the Assembler’s optionally implementable containerDidAssemble method.