Crea un plugin de Cordova para iOS
Post principal
Este post forma parte de este tutorial:
Si no has leido ese otro post, es posible que necesites leerlo antes de continuar con este. Aprenderás a:
- Preparar la configuración base del plugin
- Crear la interfaz de JavaScript
- Usar el plugin en una app de Cordova
Puedes ver el código fuente de este tutorial en GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example
Inicio
Vamos a implementar en Swift los 4 métodos que definimos en nuestra interfaz de JavaScript www/CordovaPluginExample.js
:
- greeting
- countdownTimer
- writeFile
- bitcoinCurrentPrice
Crea un nuevo directorio ios
dentro de src
.
Después crea los ficheros CordovaPluginExample.swift
y CallbackHelper.swift
dentro de ios
.
📁 cordova-plugin-example
└── 📁 src
└── 📁 ios
├── 📄 CordovaPluginExample.swift
└── 📄 CallbackHelper.swift
CallbackHelper.swift
es una clase de ayuda para mantener el código de CordovaPluginExample.swift
más limpio.
Dependencias externas
Vamos a usar Alomofire para obtener el precio actual del Bitcoin de https://api.coindesk.com/v1/bpi/currentprice.json . Podríamos hacer esto con JavaScript en el navegador, pero es un ejemplo sencillo con el que mostrar como usar dependencias externas.
Para añadir una dependencia externa en las últimas versiones de Cordova, usamos podspec:
<platform name="ios">
...
<podspec>
<config>
<!-- Repositorios de CocoaPods -->
<source url="https://github.com/CocoaPods/Specs.git"/>
</config>
<pods use-frameworks="true">
<pod name="Alamofire" spec="~> 5.5" />
<!-- O instala la dependencia usando un repositorio de Git -->
<!-- <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>
Debemos tener CocoaPods instalado:
sudo gem install cocoapods
Si obtienes algún error al descargar alguna versión de un pod, prueba a actualizar los datos locales de los pods:
pod repo update
Dependencias nativas
Podemos usar también dependencias nativas de iOS. Usamos el tag <framework>
para ello:
<platform name="ios">
...
<framework src="Speech.framework"/>
...
</platform>
Usa otro plugin de Cordova como dependencia
Para que nuestra app pueda usar código escrito en Swift, necesitamos el plugin cordova-plugin-add-swift-support. Simplemente añade el plugin como una dependencia en el plugin.xml
:
<platform name="ios">
...
<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
...
</platform>
Si no quieres añadir esta dependencia en el plugin.xml
, puedes añadirlo en el config.xml
de tu app de Cordova.
Editar Info.plist
Podemos usar el tag <config-file> para añadir nuevas configuraciones y el tag <edit-config> para editar configuraciones existentes en Info.plist
.
Declarar permisos
Para este tutorial debemos asignar el valor true
a las propiedades UIFileSharingEnabled
y LSSupportsOpeningDocumentsInPlace
para poder ver el directorio Documents
de la aplicación dentro de la app Files de iOS.
<platform name="ios">
...
<config-file target="*-Info.plist" parent="UIFileSharingEnabled">
<true/>
</config-file>
<config-file target="*-Info.plist" parent="LSSupportsOpeningDocumentsInPlace">
<true/>
</config-file>
...
</platform>
Para este plugin no necesitamos ningún permiso especial. Si necesitáis especificar para que estais usando un permiso especial, podéis hacerlo así:
<platform name="ios">
...
<config-file target="*-Info.plist" parent="NSMicrophoneUsageDescription">
<string>Explica aquí por qué necesitas usar el micrófono</string>
</config-file>
...
</platform>
Configuración final de plugin.xml
Esta es la configuración final para iOS de nuestro plugin.xml:
<!-- 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>
El primer <config-file>
se usa para editar un fichero de cordova-ios
que es usado por Cordova para registrar la clase principal en Swift de nuestro plugin en la app generada por Cordova.
<source-file>
se usa para copiar el código fuente a la aplicación (platforms/ios
). Podemos copiar directorios también, en vez de fichero a fichero:
<source-file src="src/ios/<directory-name>" target="src/"/> />
Estructura del código fuente de iOS
📁 cordova-plugin-example
├── 📁 src
│ ├── 📁 android
│ ├── 📁 ios
│ │ ├── 📄 CordovaPluginExample.swift
│ │ └── 📄 CallbackHelper.swift
│ └── 📁 electron
├── 📁 www
│ └── 📄 CordovaPluginExample.js
├── 📄 .gitignore
├── 📄 package.json
├── 📄 plugin.xml
└── 📄 README.md
Código fuente en Swift
CordovaPluginExample.swift (Clase principal del plugin)
import Foundation
import Alamofire
@objc(CordovaPluginExample) class CordovaPluginExample : CDVPlugin {
var callbackHelper: CallbackHelper?
func log(_ message: String) {
NSLog("[CordovaPluginExample] %@", message)
}
// Este método es llamado con la inicialización de Cordova.
// Usa este método para inicializar todo lo que necesites
// más adelante. En este ejemplo inicializamos la clase
// CallbackHelper.
override func pluginInitialize() {
log("Initializing Cordova plugin example");
super.pluginInitialize()
callbackHelper = CallbackHelper(self.commandDelegate!)
}
// Este es el ejemplo más sencillo.
// Devuelve el string "Hello {name}!"
// o "Hello!" si no se recibe ningún nombre.
@objc(greeting:) func greeting(command: CDVInvokedUrlCommand) {
let name = command.arguments[0] as? String ?? ""
let greeting = name.isEmpty ? "Hello!" : "Hello \(name)"
callbackHelper!.sendString(command, greeting)
}
// Devuelve un número cada segundo, desde el valor
// del parámetro "seconds" hasta 0.
//
// Este ejemplo sirve para mostrar como devolver
// más de un valor a lo largo del tiempo, como
// un Observable.
//
// Para devolver multiples valores y mantener
// la "conexión" abierta, tenemos que pasar el valor
// "true" al parametro "keepCallback" de los
// métodos de CallbackHelper
@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()
}
// Con este método podemos crear y escribir
// un fichero de la carpeta Documents
// de la aplicación
@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")
}
}
// Este ejemplo sirve solo para mostrar como usar
// la dependencia que hemos añadido en el plugin.xml
// con <podspec>, en este caso 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")
}
}
}
}
En iOS no tenemos un método
execute
como en Android. Solo es necesario que el nombre de los métodos sea igual que los nombres de las acciones definidas en las funciones dewww/CordovaPluginExample.js
.
CallbackHelper.swift
import Foundation
class CallbackHelper {
let commandDelegate: CDVCommandDelegate
// Necesitamos el CDVCommandDelegate de la clase principal
init(_ commandDelegate: CDVCommandDelegate) {
self.commandDelegate = commandDelegate
}
// Este es el método principal de esta clase.
// Tiene como parámetros el "comando" recivido,
// el resultado que queremos devolver y la opción
// "keepCallback" para poder seguir enviando valores.
//
// Si "keepCallback" es true, podemos enviar otra
// respuesta más para la misma llamada al plugin.
// Cuando no quieras devolver más valores,
// deja este parámetro a false.
private func send(_ command: CDVInvokedUrlCommand, _ pluginResult: CDVPluginResult, _ keepCallback: Bool = false) {
pluginResult.setKeepCallbackAs(keepCallback)
self.commandDelegate.send(
pluginResult,
callbackId: command.callbackId
)
}
// Devuelve un objeto de error con un mensaje
func sendError(_ command: CDVInvokedUrlCommand, _ message: String) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_ERROR,
messageAs: message
)
self.send(command, pluginResult!)
}
// Devuelve un OK sin valor
func sendEmpty(_ command: CDVInvokedUrlCommand, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK)
self.send(command, pluginResult!, keepCallback)
}
// Devuelve un OK con un string
func sendString(_ command: CDVInvokedUrlCommand, _ string: String, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: string
)
self.send(command, pluginResult!, keepCallback)
}
// Devuelve un OK con un número
func sendNumber(_ command: CDVInvokedUrlCommand, _ number: Int, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: number
)
self.send(command, pluginResult!, keepCallback)
}
// Devuelve un OK con un objeto JSON
func sendJson(_ command: CDVInvokedUrlCommand, _ json: [String:AnyObject]?, _ keepCallback: Bool = false) {
let pluginResult = CDVPluginResult(
status: CDVCommandStatus_OK,
messageAs: json
)
self.send(command, pluginResult!, keepCallback)
}
}
Threading
Para tareas pesadas o llamadas bloqueantes, se debería crear un nuevo hilo con commandDelegate.run
.
@objc(heavyTask:) func heavyTask(command: CDVInvokedUrlCommand) {
self.commandDelegate.run {
// tu código
}
}