Build a Cordova plugin for iOS
Main post
This post forms part of this tutorial:
You might want to check that post first before continuing with this one. You will learn how to:
- Prepare the base plugin configuration
- Create the JavaScript interface
- Use the plugin in a Cordova app
You could find the source code of this tutorial on GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example.
Start
We are going to implement in Swift the 4 methods defined in our JavaScript interface www/CordovaPluginExample.js:
- greeting
- countdownTimer
- writeFile
- bitcoinCurrentPrice
Create a new directory ios inside src.
Then create the files CordovaPluginExample.swift and CallbackHelper.swift inside ios.
π cordova-plugin-example
βββ π src
βββ π ios
βββ π CordovaPluginExample.swift
βββ π CallbackHelper.swift
CallbackHelper.swift is a helper class to keep our code in CordovaPluginExample.swift cleaner.
External dependencies
We are going to use Alomofire to obtain Bitcoin’s current price from https://api.coindesk.com/v1/bpi/currentprice.json . We could do this in JavaScript, but it’s a simple and easy example on how to use external dependencies.
To add an external dependency with the latests Cordova versions, we use podspec:
<platform name="ios">
...
<podspec>
<config>
<!-- CocoaPods repositories -->
<source url="https://github.com/CocoaPods/Specs.git"/>
</config>
<pods use-frameworks="true">
<pod name="Alamofire" spec="~> 5.5" />
<!-- Or install using a Git repository -->
<!-- <pod name="Alamofire" options=":git => 'https://github.com/Alamofire/Alamofire.git', :tag => '5.5.0'" /> -->
<!-- <pod name="Alamofire" git="https://github.com/Alamofire/Alamofire.git" tag="5.5.0" /> -->
</pods>
</podspec>
...
</platform>
You must have CocoaPods installed:
sudo gem install cocoapods
If you have an error downloading a pod version, try to update your local pods data:
pod repo update
Native dependencies
We could also use iOS native dependencies. We use the <framework> tag for that:
<platform name="ios">
...
<framework src="Speech.framework"/>
...
</platform>
Use another Cordova plugin as a dependency
To add Swift support to our app, we need the plugin cordova-plugin-add-swift-support. Just add it as a dependency in plugin.xml:
<platform name="ios">
...
<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
...
</platform>
If you don’t want to add this dependency in plugin.xml, you could also add it in your Cordova app’s config.xml.
Edit Info.plist
We can use <config-file> tag to add new configurations and <edit-config> tag to edit existing configurations in Info.plist.
Declare permissions
Fot this tutorial we need to set to true the properties UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace to be able to view our app Documents directory on the Files app.
<platform name="ios">
...
<config-file target="*-Info.plist" parent="UIFileSharingEnabled">
<true/>
</config-file>
<config-file target="*-Info.plist" parent="LSSupportsOpeningDocumentsInPlace">
<true/>
</config-file>
...
</platform>
We don’t need any special permission for this plugin. If you have to add a usage description for a special permission, you could do it like this:
<platform name="ios">
...
<config-file target="*-Info.plist" parent="NSMicrophoneUsageDescription">
<string>Explaing here why you need to use the microphone.</string>
</config-file>
...
</platform>
Final plugin.xml configuration
For this plugin example, this is our final plugin.xml configuration for iOS:
<!-- ios -->
<platform name="ios">
<config-file parent="/*" target="config.xml">
<feature name="CordovaPluginExample">
<param name="ios-package" value="CordovaPluginExample"/>
</feature>
</config-file>
<config-file target="*-Info.plist" parent="UIFileSharingEnabled">
<true/>
</config-file>
<config-file target="*-Info.plist" parent="LSSupportsOpeningDocumentsInPlace">
<true/>
</config-file>
<source-file src="src/ios/CordovaPluginExample.swift"/>
<source-file src="src/ios/CallbackHelper.swift"/>
<podspec>
<config>
<source url="https://github.com/CocoaPods/Specs.git"/>
</config>
<pods use-frameworks="true">
<pod name="Alamofire" spec="~> 5.5" />
</pods>
</podspec>
<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
</platform>
The first <config-file> is used to edit a cordova-ios related file used to register our plugin main class in Cordova.
<source-file> is used to copy our source files to our cordova app. We could copy directories too:
<source-file src="src/ios/<directory-name>" target="src/"/> />
iOS source code structure
π cordova-plugin-example
βββ π src
β βββ π android
β βββ π ios
β β βββ π CordovaPluginExample.swift
β β βββ π CallbackHelper.swift
β βββ π electron
βββ π www
β βββ π CordovaPluginExample.js
βββ π .gitignore
βββ π package.json
βββ π plugin.xml
βββ π README.md
Swift source code
CordovaPluginExample.swift (Main plugin class)
import Foundation
import Alamofire
@objc(CordovaPluginExample) class CordovaPluginExample : CDVPlugin {
var callbackHelper: CallbackHelper?
func log(_ message: String) {
NSLog("[CordovaPluginExample] %@", message)
}
// This method will be called on Cordova initialization.
// Use it to initialize everything you may need later.
// In this example, we will initialize our CallbackHelper class.
override func pluginInitialize() {
log("Initializing Cordova plugin example");
super.pluginInitialize()
callbackHelper = CallbackHelper(self.commandDelegate!)
}
// This is our simplest example.
// Returns the string "Hello {name}!"
// or "Hello!" if a name is not received.
@objc(greeting:) func greeting(command: CDVInvokedUrlCommand) {
let name = command.arguments[0] as? String ?? ""
let greeting = name.isEmpty ? "Hello!" : "Hello \(name)"
callbackHelper!.sendString(command, greeting)
}
// Returns a number every second, from "seconds" parameter value to 0.
//
// With this example we will see how can we return
// multiple values over time, like an Observable.
//
// To return multiple values and keep the "connection"
// open, just set the parameter "keepCallback" of
// CallbackHelper methods to true
@objc(countdownTimer:) func countdownTimer(command: CDVInvokedUrlCommand) {
let seconds = command.arguments[0] as? Int ?? 10
var secondsLeft = seconds > 0 ? seconds : 10
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
let keepCallback = secondsLeft > 0
self.callbackHelper!.sendNumber(command, secondsLeft, keepCallback)
if (keepCallback) {
secondsLeft -= 1
} else {
timer.invalidate()
}
}
timer.fire()
}
// With this method we can create and write
// a file in our app's Documents directory.
@objc(writeFile:) func writeFile(command: CDVInvokedUrlCommand) {
let fileName = command.arguments[0] as? String;
let text = command.arguments[1] as? String;
if (fileName == nil || text == nil) {
callbackHelper!.sendError(command, "BAD_ARGS")
return
}
do {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileUrl = documentsDirectory.appendingPathComponent(fileName!)
log("Writing file at path: " + fileUrl.absoluteString)
try text!.write(to: fileUrl, atomically: false, encoding: .utf8)
callbackHelper!.sendEmpty(command)
} catch {
print(error)
callbackHelper!.sendError(command, "COULD_NOT_CREATE_FILE")
}
}
// This example is just to show how can we use
// the external dependencies we declared previously,
// in this case, Alomofire
@objc(bitcoinCurrentPrice:) func bitcoinCurrentPrice(command: CDVInvokedUrlCommand) {
let url = "https://api.coindesk.com/v1/bpi/currentprice.json"
AF.request(url).responseString { response in
switch response.result {
case .success:
let json = (try? JSONSerialization.jsonObject(with: response.data!, options: [])) as? [String:AnyObject]
self.callbackHelper!.sendJson(command, json)
case let .failure(error):
print(error)
self.callbackHelper!.sendError(command, "REQUEST_ERROR")
}
}
}
}
In iOS we don’t need a method
executelike in Android. We just have to use the same method names as we defined inwww/CordovaPluginExample.js.
CallbackHelper.swift
import Foundation
class CallbackHelper {
let commandDelegate: CDVCommandDelegate
// We need the CDVCommandDelegate from the main class
init(_ commandDelegate: CDVCommandDelegate) {
self.commandDelegate = commandDelegate
}
// This is the main method of this helper class.
// It receives as parameters the received command,
// the pluginResult we want to return, and the
// keepCallback option.
// By setting setKeepCallbackAs(true) we can
// return multiple values over time for the same command.
// Use setKeepCallbackAs(false) if you don't
// want to return more values.
private func send(_ command: CDVInvokedUrlCommand, _ pluginResult: CDVPluginResult, _ keepCallback: Bool = false) {
pluginResult.setKeepCallbackAs(keepCallback)
self.commandDelegate.send(
pluginResult,
callbackId: command.callbackId
)
}
// Returns an error result with a message
func sendError(_ command: CDVInvokedUrlCommand, _ message: String) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_ERROR,
messageAs: message
)
self.send(command, pluginResult!)
}
// Returns an OK response with no value
func sendEmpty(_ command: CDVInvokedUrlCommand, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK)
self.send(command, pluginResult!, keepCallback)
}
// Returns an OK response with a string value
func sendString(_ command: CDVInvokedUrlCommand, _ string: String, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: string
)
self.send(command, pluginResult!, keepCallback)
}
// Returns an OK response with a number value
func sendNumber(_ command: CDVInvokedUrlCommand, _ number: Int, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: number
)
self.send(command, pluginResult!, keepCallback)
}
// Returns an OK response with a JSON object
func sendJson(_ command: CDVInvokedUrlCommand, _ json: [String:AnyObject]?, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: json
)
self.send(command, pluginResult!, keepCallback)
}
}
Threading
For heavy tasks or blocking calls, you should create a new thread with commandDelegate.run.
@objc(heavyTask:) func heavyTask(command: CDVInvokedUrlCommand) {
self.commandDelegate.run {
// your code
}
}