Missed the live Ionic Event? Check out all the product announcements, new releases, and more.

DocsPluginsCLI

Capacitor iOS Plugin Guide

Building Capacitor plugins for iOS involves writing Swift (or Objective-C) to interface with Apple’s iOS SDKs.

Getting Started

To get started, first generate a plugin as shown in the Getting Started section of the Plugin guide.

Next, open my-plugin/ios/Plugin.xcworkspace in Xcode.

Plugin Basics

A Capacitor plugin for iOS is a simple Swift class that extends CAPPlugin and has some exported methods that will be callable from JavaScript.

Once your plugin is generated, you can start editing it by opening Plugin.swift.

Simple Example

In the generated example, there is a simple echo plugin with an echo function that simply returns a value that it was given.

This example demonstrates a few core components of Capacitor plugins: receiving data from a Plugin Call, and returning data back to the caller:

Plugin.swift

import Capacitor

@objc(MyPlugin)
public class MyPlugin: CAPPlugin {
  @objc func echo(_ call: CAPPluginCall) {
    let value = call.getString("value") ?? ""
    call.resolve([
        "value": value
    ])
  }
}

Accessing Call Data

Each plugin method receives an instance of CAPPluginCall containing all the information of the plugin method invocation from the client.

A client can send any data that can be JSON serialized, such as numbers, text, booleans, objects, and arrays. This data is accessible on the options field of the call instance, or by using convenience methods such as getString or getObject. Passing and accessing some of these values has some peculiarities to be aware of, as discussed separately.

For example, here is how you’d get data passed to your method:

@objc func storeContact(_ call: CAPPluginCall) {
  let name = call.getString("yourName") ?? "default name"
  let address = call.getObject("address") ?? [:]
  let isAwesome = call.getBool("isAwesome") ?? false

  guard let id = call.options["id"] as? String else {
    call.reject("Must provide an id")
    return
  }

  // ...

  call.resolve()
}

Notice the various ways data can be accessed on the CAPPluginCall instance, including how to require options using guard.

Returning Data Back

A plugin call can either succeed or fail. Plugin calls borrow method names from JavaScript promises: call resolve() to indicate success (optionally returning data) and use reject() to indicate failure with an error message.

The resolve() method of CAPPluginCall takes a dictionary and supports JSON-serializable data types. Here’s an example of returning data back to the client:

call.resolve([
  "added": true,
  "info": [
    "id": id
  ]
])

To fail, or reject a call, call reject(), passing an error string and optionally an error code and Error instance:

call.reject(error.localizedDescription, nil, error)

Export to Capacitor

To make sure Capacitor can see your plugin, you must do two things: export your Swift class to Objective-C, and register it using the provided Capacitor Objective-C Macros.

To export your Swift class to Objective-C, make sure to add @objc(YourPluginClass) above your Swift class, and add @objc before any plugin method, as shown above.

To register your plugin with Capacitor, you’ll need to create a new Objective-C file (with a .m extension, not .h!) corresponding to your plugin (such as MyPlugin.m) and use the Capacitor macros to register the plugin, and each method that you will use. Important: you must use the New File dialog in Xcode to do this. You’ll then be prompted by Xcode to create a Bridging Header, which you must do.

Finally, register the plugin by adding the required Capacitor plugin macros into your new .m file:

#import <Capacitor/Capacitor.h>

CAP_PLUGIN(MyPlugin, "MyPlugin",
  CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
)

This makes MyPlugin, and the echo method available to the Capacitor web runtime, indicating to Capacitor that the echo method will return a Promise.

Permissions

If your plugin has functionality on iOS that requires permissions from the end user, then you will need to implement the permissions pattern.

Before following this section, make sure you’ve set up your permission aliases and status interfaces. If you haven’t, see the Permissions section in the Web guide.

Implementing Permissions

Add the checkPermissions() and requestPermissions() methods to your Swift plugin class.

 import Capacitor

 @objc(MyPlugin)
 public class MyPlugin: CAPPlugin {
     ...

+    @objc override public func checkPermissions(_ call: CAPPluginCall) {
+        // TODO
+    }

+    @objc override public func requestPermissions(_ call: CAPPluginCall) {
+        // TODO
+    }
 }

checkPermissions()

This method should return the current status of permissions in your plugin, which should be a dictionary that matches the structure of the permission status definition you defined. Typically, this information is available directly on the frameworks you’re using.

In the example below, we map the current authorization status from location services into a permission state and associate the location alias with that state.

@objc override func checkPermissions(_ call: CAPPluginCall) {
    let locationState: String

    switch CLLocationManager.authorizationStatus() {
    case .notDetermined:
        locationState = "prompt"
    case .restricted, .denied:
        locationState = "denied"
    case .authorizedAlways, .authorizedWhenInUse:
        locationState = "granted"
    @unknown default:
        locationState = "prompt"
    }

    call.resolve(["location": locationState])
}

requestPermissions()

Block-based APIs

If the framework supports a block-based API for requesting permission, it’s possible to complete the operation within the single method.

In the example below, we request video access from AVCaptureDevice and then use our own checkPermissions method to check the current status of permissions and then fulfill the call.

@objc override func requestPermissions(_ call: CAPPluginCall) {
    AVCaptureDevice.requestAccess(for: .video) { [weak self] _ in
        self?.checkPermissions(call)
    }
}

Delegate-based APIs

If the framework uses a delegate (or callback) API, completing the operation means that the original call will need to be saved and then retrieved once the callback has been invoked.

var permissionCallID: String?
var locationManager: CLLocationManager?

@objc override func requestPermissions(_ call: CAPPluginCall) {
    if let manager = locationManager, CLLocationManager.locationServicesEnabled() {
        if CLLocationManager.authorizationStatus() == .notDetermined {
            call.save()
            permissionCallID = call.callbackId
            manager.requestWhenInUseAuthorization()
        } else {
            checkPermissions(call)
        }
    } else {
        call.reject("Location services are disabled")
    }
}

public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    if let callID = permissionCallID, let call = bridge?.getSavedCall(callID) {
        checkPermissions(call)
        bridge?.releaseCall(call)
    }
}

Multiple Permissions

When several types of permissions are required, a DispatchGroup is a convenient way to synchronize the multiple calls.

let store = CNContactStore()

@objc override func requestPermissions(_ call: CAPPluginCall) {
    // get the permissions to check or default to all of them
    var permissions = call.getArray("types", String.self) ?? []
    if permissions.isEmpty {
        permissions = ["contacts", "camera"]
    }

    let group = DispatchGroup()
    if permissions.contains("contacts") {
        group.enter()
        store.requestAccess(for: .contacts) { (_, _) in
            group.leave()
        }
    }
    if permissions.contains("camera") {
        group.enter()
        AVCaptureDevice.requestAccess(for: .video) { _ in
            group.leave()
        }
    }
    group.notify(queue: DispatchQueue.main) {
        self.checkPermissions(call)
    }
}

Error Handling

Unavailable

This error can be thrown to indicate that the functionality can’t be used right now, usually because it requires a newer iOS version.

@objc override func methodThatUsesNewIOSFramework(_ call: CAPPluginCall) {
    if #available(iOS 14, *) {
        // TODO implementation
    } else {
        call.unavailable("Not available in iOS 13 or earlier.")
    }
}

It is recommended to gracefully degrade the experience with older APIs as much as possible. Use unavailable sparingly.

Unimplemented

Use this error to indicate that a method can’t be implemented for iOS.

@objc override func methodThatRequiresAndroid(_ call: CAPPluginCall) {
    call.unimplemented("Not implemented on iOS.")
}

Plugin Events

Plugins can emit their own events that you can listen by attaching a listener to the plugin object like this:

import { MyPlugin } from 'my-plugin';

MyPlugin.addListener('myPluginEvent', (info: any) => {
  console.log('myPluginEvent was fired');
});

To emit the event from the Swift plugin class:

self.notifyListeners("myPluginEvent", data: [:])

To remove a listener from the plugin object:

import { MyPlugin } from 'my-plugin';

const myPluginEventListener = MyPlugin.addListener(
  'myPluginEvent',
  (info: any) => {
    console.log('myPluginEvent was fired');
  },
);

myPluginEventListener.remove();

It is also possible to trigger global events on window. See the docs for triggerJSEvent.

Presenting Native Screens

You can present native screens over the app by using Capacitor’s UIViewController.

Override navigation

Capacitor plugins can override the webview navigation. For that the plugin can override - (NSNumber *)shouldOverrideLoad:(WKNavigationAction *)navigationAction method. Returning true causes the WebView to abort loading the URL. Returning false causes the WebView to continue loading the URL. Returning nil will defer to the default Capacitor policy.

Previous
<- Development Workflow
Next
Android Guide ->
Contribute ->