ELMaestro is a Swift framework that provides plugin system for iOS applications.
ELMaestro requires Swift 5 and Xcode 11.3.
Install with Carthage by adding the framework to your project's Cartfile.
github "Electrode-iOS/ELMaestro"
Install by adding ELMaestro.xcodeproj to your project and configuring your target to link ELMaestro.framework from ELMaestro
target.
There are two target that builds ELMaestro.framework
.
ELMaestro
: Creates dynamically linkedELMaestro.framework.
ELMaestro_static
: Creates statically linkedELMaestro.framework
.
ELMaestro provides a plugin system for managing a modular architecture of an application. It enables your app to be organized into self-contained modules that can subscribe to application delegate events. This prevents your app delegate from being bloated by feature-specific code and keeps concerns cleanly separated.
ELMaestro consists of three main concepts:
- ApplicationSupervisor - A registrar for plugin instances that handles forwarding app delegate events to plugins
- Plugin - A type that contains feature-specific implementation. Typically this type is encapsulated within a Swift framework.
- Plugin API - An interface that a plugin provides that other modules use to interact with the plugin
A Plugin is composed of two different protocols:
Pluggable
PluggableFeature
(optional)
The Pluggable
protocol defines the plugin identifier, plugin dependencies, as well as startup and initialization methods.
@objc
public protocol Pluggable {
var identifier: DependencyID { get }
var dependencies: [DependencyID]? { get }
init?(containerBundleID: String?)
func startup(_ supervisor: Supervisor)
}
Example implementation:
/// Identifier used to lookup the instance with `ApplicationSupervisor`
public let MyPluginID = "com.myorganization.mymodule"
extension MyPlugin: Pluggable {
let identifier: String = MyPluginID
/**
An array of identifiers of any other modules that
this module is dependent on
*/
let dependencies: [DependencyID]? = nil
/**
Called when the plugin is first initialized by
the `ApplicationSupervisor`
*/
required init?(containerBundleID: String?) {
}
/**
Called after all dependent plugins are started up
*/
func startup(_ supervisor: Supervisor) {
}
}
The PluggableFeature
protocol defines all of the application delegate events that a plugin can handle. Aside from applicationWillTerminate
and applicationDidReceiveMemoryWarning
, all of the app delegate methods are defined as optional
so you can choose which methods the plugin needs to handle.
@objc public protocol PluggableFeature : Pluggable {
/**
API factory method for a module's API it exports. You will likely want to
typecast this, ie:
let pluginAPI = supervisor.pluginAPI(forIdentifier: "com.myorg.mymodule") as? MyPluginAPI
*/
@objc optional public func pluginAPI() -> AnyObject?
/**
After all plugins have been started, the system will dispatch this to your plugin.
*/
@objc optional public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any]?) -> Bool
/**
Application lifecycle events
*/
public func applicationWillTerminate()
public func applicationDidReceiveMemoryWarning()
@objc optional public func applicationWillResignActive()
@objc optional public func applicationDidEnterBackground()
@objc optional public func applicationWillEnterForeground()
@objc optional public func applicationDidBecomeActive()
/**
Local and Remote Notification events
*/
@objc optional public func application(_ application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) -> <<error type>>
@objc optional public func application(_ application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) -> <<error type>>
@objc optional public func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) -> <<error type>>
@objc optional public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) -> <<error type>>
@objc optional public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) -> <<error type>>
@objc optional public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) -> <<error type>>
@objc optional public func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, forRemoteNotification userInfo: [AnyHashable : Any], completionHandler: () -> Void) -> <<error type>>
@objc optional public func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, withResponseInfo responseInfo: [AnyHashable : Any], completionHandler: () -> Void) -> <<error type>>
@objc optional public func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, forRemoteNotification userInfo: [AnyHashable : Any], withResponseInfo responseInfo: [AnyHashable : Any], completionHandler: () -> Void) -> <<error type>>
@objc optional public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: ([Any]?) -> Void) -> Bool
/**
Application events for background event handling
*/
@objc optional public func applicationHandleEventsForBackgroundURLSession(_ identifier: String, completionHandler: () -> Void)
/**
Application events for watchkit handling -- is this needed?
*/
@objc optional public func applicationHandleWatchKitExtensionRequest(_ userInfo: [AnyHashable : Any]?, reply: (([AnyHashable : Any]?) -> Void)!)
@objc optional public func applicationPerformActionForShortcutItem(_ shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) -> Bool
}
Example implementation:
extension MyPlugin: PluggableFeature {
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any]?) -> Bool {
// handle app launch event
}
public func applicationWillTerminate() {
// handle app will terminate event
}
public func applicationDidReceiveMemoryWarning() {
// handle memory warning event
}
}
The PluggableFeature
defines an optional function pluginAPI() -> AnyObject?
that enables a plugin to return an implementation for the public interface of the plugin.
For example, suppose the plugin needed to provide user data to other plugins. You can define an implementation for the plugin's API with a property to access the user's name.
@objc public protocol MyPluginAPI {
var username: String {get}
}
Define a type for the implementation and declare it as internal
to prevent it from being publicly accessible outside of the module.
internal final class MyPluginAPIImpl: MyPluginAPI {
let username: String = "mrmeeseeks"
}
Add a property to the plugin to contain the instance of the plugin API implementation.
@objc
open class MyPlugin: NSObject, Module {
open static let logging = Logger()
var pluginAPI = MyPluginAPIImpl()
open static func pluginClass() -> Pluggable.Type {
return MyPlugin.self
}
}
Return the instance of the plugin API in the pluginAPI()
function.
extension MyPlugin: PluggableFeature {
func pluginAPI() -> AnyObject? {
return pluginAPI
}
func applicationWillTerminate() {
}
func applicationDidReceiveMemoryWarning() {
}
}
The ApplicationSupervisor
is used to register a plugin. The ApplicationSupervisor
will handle loading the plugin, loading any of the plugin's dependencies first, and calling the startup()
method of the plugin once all dependent plugins are loaded.
The ApplicationSupervisor
will also handle sending app delegate events to the plugin.
Call loadPlugin
to register a plugin to load upon startup.
ApplicationSupervisor.sharedInstance.loadPlugin(MyPlugin.self)
After all plugins have been loaded, call the startup
method to start the plugins.
ApplicationSupervisor.sharedInstance.startup()
Use the pluginAPI(forIdentifier:)
method to get a reference to a plugin API.
guard let pluginAPI = ApplicationSupervisor.sharedInstance.pluginAPI(forIdentifier: MyPluginID) as? MyPluginAPI else {
// plugin API not found
return
}
let username = pluginAPI.username