Crea un plugin de Cordova para iOS

18 de abril de 2022
Tabla de contenidos

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:

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:

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)

Abrir en GitHub

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 de www/CordovaPluginExample.js.

CallbackHelper.swift

Abrir en GitHub

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

Continua leyendo el post principal