or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

bridge-integration.mdconfiguration.mdindex.mdmethod-calls.mdnotifications.mdplugin-development.md
tile.json

plugin-development.mddocs/

Plugin Development

Comprehensive plugin development framework for creating custom native functionality accessible from JavaScript.

Capabilities

CAPPlugin Base Class

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;

@end

Usage 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"])
        }
    }
}

CAPBridgedPlugin Protocol

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;

@end

Plugin 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) \
@end

Return 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);
)

CAPPluginMethod

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)
        ]
    }
}

Plugin Registration and Lifecycle

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
}

Advanced Plugin Patterns

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...
    }
}