Comprehensive plugin development framework for creating custom native functionality accessible from JavaScript.
Base class that all Capacitor plugins must inherit from, providing core functionality for JavaScript-to-native communication.
/**
* Base class for all Capacitor plugins providing core functionality
*/
@interface CAPPlugin : NSObject
// MARK: - Core Properties
/** Reference to the webview for direct access */
@property (nonatomic, weak, nullable) WKWebView *webView;
/** Reference to the bridge for accessing bridge functionality */
@property (nonatomic, weak, nullable) id<CAPBridgeProtocol> bridge;
/** Unique identifier for this plugin instance */
@property (nonatomic, strong, nonnull) NSString *pluginId;
/** Display name for this plugin */
@property (nonatomic, strong, nonnull) NSString *pluginName;
/** Registry of event listeners organized by event name */
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString *, NSMutableArray<CAPPluginCall *>*> *eventListeners;
/** Cached event arguments for listeners that join after events are fired */
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString *, NSMutableArray<id> *> *retainedEventArguments;
/** Whether to stringify Date objects in call responses */
@property (nonatomic, assign) BOOL shouldStringifyDatesInCalls;
// MARK: - Lifecycle Methods
/**
* Called after plugin initialization to perform setup
* Override this method instead of init() for plugin-specific initialization
*/
- (void)load;
/**
* Get the plugin identifier
* @returns Plugin identifier string
*/
-(NSString* _Nonnull)getId;
// MARK: - Event Handling
/**
* Register an event listener for a specific event
* @param eventName Name of the event to listen for
* @param listener Plugin call that will receive event notifications
*/
- (void)addEventListener:(NSString* _Nonnull)eventName listener:(CAPPluginCall* _Nonnull)listener;
/**
* Remove an event listener for a specific event
* @param eventName Name of the event
* @param listener Plugin call to remove
*/
- (void)removeEventListener:(NSString* _Nonnull)eventName listener:(CAPPluginCall* _Nonnull)listener;
/**
* Notify all listeners of an event with data
* @param eventName Name of the event to fire
* @param data Event data to send to listeners
*/
- (void)notifyListeners:(NSString* _Nonnull)eventName data:(NSDictionary<NSString *, id>* _Nullable)data;
/**
* Notify listeners with optional data retention for late joiners
* @param eventName Name of the event to fire
* @param data Event data to send to listeners
* @param retain Whether to retain data for listeners that join later
*/
- (void)notifyListeners:(NSString* _Nonnull)eventName data:(NSDictionary<NSString *, id>* _Nullable)data retainUntilConsumed:(BOOL)retain;
/**
* Get all listeners for a specific event
* @param eventName Name of the event
* @returns Array of plugin calls listening to the event
*/
- (NSArray<CAPPluginCall *>* _Nullable)getListeners:(NSString* _Nonnull)eventName;
/**
* Check if an event has any listeners
* @param eventName Name of the event
* @returns YES if event has listeners, NO otherwise
*/
- (BOOL)hasListeners:(NSString* _Nonnull)eventName;
/**
* Add a listener using the standard addEventListener pattern
* @param call Plugin call containing listener details
*/
- (void)addListener:(CAPPluginCall* _Nonnull)call;
/**
* Remove a listener using the standard removeEventListener pattern
* @param call Plugin call containing listener details
*/
- (void)removeListener:(CAPPluginCall* _Nonnull)call;
/**
* Remove all listeners for all events
* @param call Plugin call (response sent to this call)
*/
- (void)removeAllListeners:(CAPPluginCall* _Nonnull)call;
// MARK: - Permission Handling (Capacitor 3.0+)
/**
* Check current permissions for this plugin
* Override to implement plugin-specific permission checking
* @param call Plugin call to respond with permission status
*/
- (void)checkPermissions:(CAPPluginCall* _Nonnull)call;
/**
* Request permissions from the user
* Override to implement plugin-specific permission requests
* @param call Plugin call to respond with permission result
*/
- (void)requestPermissions:(CAPPluginCall* _Nonnull)call;
// MARK: - Navigation Control
/**
* Control webview navigation behavior
* @param navigationAction Navigation action being attempted
* @returns NSNumber with BOOL value: YES to block navigation, NO to allow, nil for default behavior
*/
- (NSNumber* _Nullable)shouldOverrideLoad:(WKNavigationAction* _Nonnull)navigationAction;
// MARK: - Configuration Access
/**
* Get plugin configuration object
* @returns PluginConfig instance with plugin-specific settings
*/
-(PluginConfig* _Nonnull)getConfig;
// MARK: - UI Utilities
/**
* Present a popover centered on screen
* @param vc View controller to present as popover
*/
-(void)setCenteredPopover:(UIViewController* _Nonnull) vc;
/**
* Present a popover centered on screen with specific size
* @param vc View controller to present as popover
* @param size Size for the popover
*/
-(void)setCenteredPopover:(UIViewController* _Nonnull) vc size:(CGSize) size;
@endUsage Examples:
import Capacitor
class MyPlugin: CAPPlugin {
override func load() {
super.load()
// Plugin initialization
let config = getConfig()
let apiKey = config.getString("apiKey", "default")
setupPlugin(apiKey: apiKey)
}
@objc func doSomething(_ call: CAPPluginCall) {
let message = call.getString("message", "Hello")
// Perform native work
performNativeTask(message: message) { result in
call.resolve(["result": result])
}
}
@objc func startListening(_ call: CAPPluginCall) {
// Add event listener
addEventListener("dataChanged", listener: call)
call.resolve()
}
@objc func checkPermissions(_ call: CAPPluginCall) {
let status = checkCameraPermission()
call.resolve(["camera": status])
}
@objc func requestPermissions(_ call: CAPPluginCall) {
requestCameraPermission { granted in
call.resolve(["camera": granted ? "granted" : "denied"])
}
}
private func performNativeTask(message: String, completion: @escaping (String) -> Void) {
// Simulate async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
completion("Processed: \(message)")
// Notify listeners
self.notifyListeners("dataChanged", data: ["newData": "Updated value"])
}
}
}Protocol that plugins must conform to for automatic discovery and method registration.
/**
* Protocol for plugin metadata and method registration
*/
@protocol CAPBridgedPlugin <NSObject>
/** Unique identifier for the plugin */
@property (nonnull, readonly) NSString *identifier;
/** JavaScript interface name */
@property (nonnull, readonly) NSString *jsName;
/** Array of plugin methods available to JavaScript */
@property (nonnull, readonly) NSArray<CAPPluginMethod *> *pluginMethods;
@endPlugin Registration Macros:
/**
* Plugin configuration macro
* @param plugin_id Objective-C class name
* @param js_name JavaScript interface name
*/
#define CAP_PLUGIN_CONFIG(plugin_id, js_name) \
- (NSString *)identifier { return @#plugin_id; } \
- (NSString *)jsName { return @js_name; }
/**
* Plugin method registration macro
* @param method_name Objective-C method name
* @param method_return_type Return type (promise/callback/none)
*/
#define CAP_PLUGIN_METHOD(method_name, method_return_type) \
[methods addObject:[[CAPPluginMethod alloc] initWithName:@#method_name returnType:method_return_type]]
/**
* Complete plugin definition macro
* @param objc_name Objective-C class name
* @param js_name JavaScript interface name
* @param methods_body Block defining plugin methods
*/
#define CAP_PLUGIN(objc_name, js_name, methods_body) \
@interface objc_name : NSObject \
@end \
@interface objc_name (CAPPluginCategory) <CAPBridgedPlugin> \
@end \
@implementation objc_name (CAPPluginCategory) \
- (NSArray *)pluginMethods { \
NSMutableArray *methods = [NSMutableArray new]; \
methods_body \
return methods; \
} \
CAP_PLUGIN_CONFIG(objc_name, js_name) \
@endReturn Type Constants:
/** Method returns no value */
#define CAPPluginReturnNone @"none"
/** Method uses callback pattern */
#define CAPPluginReturnCallback @"callback"
/** Method returns a promise */
#define CAPPluginReturnPromise @"promise"Usage Examples:
// Swift plugin implementation
class MyPlugin: CAPPlugin {
@objc func echo(_ call: CAPPluginCall) {
let message = call.getString("message", "")
call.resolve(["message": message])
}
@objc func openSettings(_ call: CAPPluginCall) {
DispatchQueue.main.async {
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl)
}
}
call.resolve()
}
}
// Objective-C plugin registration
CAP_PLUGIN(MyPlugin, MyPlugin,
CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(openSettings, CAPPluginReturnPromise);
)Class representing plugin method metadata for registration and invocation.
/**
* Plugin method metadata for registration and invocation
*/
@objc(CAPPluginMethod)
public class CAPPluginMethod: NSObject {
/**
* Initialize a plugin method
* @param name Method name as called from JavaScript
* @param returnType How the method returns values (promise/callback/none)
*/
public init(name: String, returnType: CAPPluginReturnType)
/** Method name for JavaScript calls */
public let name: String
/** Objective-C selector for the method */
public let selector: Selector
/** Return type defining how method responds */
public let returnType: CAPPluginReturnType
}
/**
* Plugin method return type enumeration
*/
public enum CAPPluginReturnType: Int {
/** Method returns no value */
case none
/** Method uses callback pattern */
case callback
/** Method returns promise */
case promise
}Manual Method Registration:
class MyPlugin: CAPPlugin {
// Manual method registration (alternative to macros)
override class func pluginMethods() -> [CAPPluginMethod] {
return [
CAPPluginMethod(name: "echo", returnType: .promise),
CAPPluginMethod(name: "openSettings", returnType: .promise),
CAPPluginMethod(name: "startListening", returnType: .callback)
]
}
}How plugins are registered with the bridge and initialized.
// Registration with bridge
override func viewDidLoad() {
super.viewDidLoad()
}
override func capacitorDidLoad() {
super.capacitorDidLoad()
// Register plugins after bridge is ready
bridge?.registerPluginType(MyPlugin.self)
bridge?.registerPluginType(AnotherPlugin.self)
}
// Alternative: Register during bridge initialization
override func instanceDescriptor() -> InstanceDescriptor {
let descriptor = InstanceDescriptor()
// Plugins can be configured via JSON as well
descriptor.pluginConfigurations = [
"MyPlugin": [
"apiKey": "your-api-key",
"enabled": true,
"timeout": 30
]
]
return descriptor
}Common patterns for complex plugin development.
Event-Driven Plugin:
class SensorPlugin: CAPPlugin {
private var sensorManager: SensorManager?
override func load() {
super.load()
sensorManager = SensorManager()
sensorManager?.delegate = self
}
@objc func startSensing(_ call: CAPPluginCall) {
let interval = call.getDouble("interval", 1000) // milliseconds
addEventListener("sensorData", listener: call)
sensorManager?.start(interval: interval / 1000.0)
call.resolve()
}
@objc func stopSensing(_ call: CAPPluginCall) {
sensorManager?.stop()
call.resolve()
}
}
extension SensorPlugin: SensorManagerDelegate {
func sensorDidUpdate(data: [String: Any]) {
notifyListeners("sensorData", data: data)
}
}Permission-Aware Plugin:
class LocationPlugin: CAPPlugin {
@objc override func checkPermissions(_ call: CAPPluginCall) {
let status = CLLocationManager.authorizationStatus()
let result = ["location": locationStatusToString(status)]
call.resolve(result)
}
@objc override func requestPermissions(_ call: CAPPluginCall) {
let manager = CLLocationManager()
manager.requestWhenInUseAuthorization()
// Wait for authorization result
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.checkPermissions(call)
}
}
@objc func getCurrentPosition(_ call: CAPPluginCall) {
guard CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways else {
call.reject("Location permission not granted")
return
}
// Get location...
}
}