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
execute
like 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
}
}