Cómo construir un editor de texto colaborativo usando Swift

Foto de rawpixel en Unsplash

Los editores de texto son cada vez más populares en estos días, ya sea que estén incrustados en un formulario de comentarios del sitio web o se usen como un bloc de notas. Hay muchos editores diferentes para elegir. En esta publicación, no solo aprenderemos cómo construir una hermosa aplicación móvil de editor de texto en iOS, sino también cómo hacer posible colaborar en una nota en tiempo real usando Pusher.

Sin embargo, tenga en cuenta que para mantener la aplicación simple, el artículo no cubrirá las ediciones concurrentes. Por lo tanto, solo una persona puede editar al mismo tiempo mientras otras miran.

La aplicación funcionará activando un evento cuando se ingrese algún texto. Este evento se enviará a Pusher y luego lo recogerá el dispositivo del colaborador y se actualizará automáticamente.

Para seguir este tutorial, necesitará lo siguiente:

  1. Cocoapods: para instalar, ejecute gem install cocoapods en su máquina
  2. Xcode
  3. Una aplicación Pusher: puede crear una cuenta y una aplicación gratuitas aquí
  4. Algunos conocimientos del lenguaje Swift
  5. Node.js

Por último, se necesita una comprensión básica de Swift y Node.js para seguir este tutorial.

Comenzando con nuestra aplicación iOS en Xcode

Inicie Xcode y cree un nuevo proyecto. Llamaré al mío Collabo. Después de seguir el asistente de configuración, y con el espacio de trabajo abierto, cierre Xcode y luego cd a la raíz de su proyecto y ejecute el comando pod init. Esto debería generar un Podfile para ti. Cambiar el contenido del Podfile:

# Descomente la siguiente línea para definir una plataforma global para su proyecto
    plataforma: ios, '9.0'
    objetivo 'textcollabo' hacer
      # Comente la siguiente línea si no está usando Swift y no quiere usar marcos dinámicos
      use_frameworks!
      # Pods para anonchat
      pod 'Alamofire'
      pod 'PusherSwift'
    fin

Ahora ejecute el comando pod install para que el administrador de paquetes de Cocoapods pueda obtener las dependencias necesarias. Cuando esto esté completo, cierre Xcode (si está abierto) y luego abra el archivo .xcworkspace que se encuentra en la raíz de la carpeta de su proyecto.

Diseñando las vistas para nuestra aplicación iOS

Vamos a crear algunas vistas para nuestra aplicación iOS. Estos serán la columna vertebral en la que conectaremos toda la lógica. Usando el storyboard de Xcode, haga que sus vistas se vean un poco como las capturas de pantalla a continuación.

Este es el archivo LaunchScreen.storyboard. Acabo de diseñar algo simple sin ninguna funcionalidad.

El próximo guión gráfico que diseñaremos es el Main.storyboard. Como su nombre lo indica, será el principal. Aquí es donde tenemos todas las vistas importantes que están unidas a cierta lógica.

Aquí tenemos tres vistas.

La primera vista está diseñada para parecerse exactamente a la pantalla de inicio, con la excepción de un botón que hemos vinculado para abrir la segunda vista.

La segunda vista es el controlador de navegación. Se adjunta a una tercera vista que es un ViewController. Hemos establecido la tercera vista como el controlador raíz de nuestro controlador de navegación.

En la tercera vista, tenemos un UITextView que es editable que se coloca en la vista. También hay una etiqueta que se supone que es un contador de caracteres. Este es el lugar donde incrementaremos los caracteres a medida que el usuario escriba texto en la vista de texto.

Codificación de la aplicación de editor de texto colaborativo iOS

Ahora que hemos creado con éxito las vistas necesarias para que se cargue la aplicación, lo siguiente que haremos será comenzar a codificar la lógica de la aplicación.

Cree un nuevo archivo de clase de cacao y asígnele el nombre TextEditorViewController y vincúlelo a la tercera vista en el archivo Main.storyboard. El TextViewController también debe adoptar el UITextViewDelegate. Ahora, puede Ctrl + arrastrar UITextView y también Ctrl + arrastrar UILabel en el archivo Main.storyboard a la clase TextEditorViewController.

Además, debe importar las bibliotecas PusherSwift y AlamoFire al TextViewController. Deberías tener algo parecido a esto después de que hayas terminado:

importar UIKit
    importar PusherSwift
    importar Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        @IBOutlet débil var textView: UITextView!
        @IBOutlet débil var charactersLabel: UILabel!
    }

Ahora necesitamos agregar algunas propiedades que necesitaremos más adelante en el controlador.

importar UIKit
    importar PusherSwift
    importar Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        static let API_ENDPOINT = "http: // localhost: 4000";
        @IBOutlet débil var textView: UITextView!
        @IBOutlet débil var charactersLabel: UILabel!
        var pusher: Pusher!
        var chillPill = true
        var placeHolderText = "Comience a escribir ..."
        var randomUuid: String = ""
    }

Ahora dividiremos la lógica en tres partes:

  1. Ver y eventos de teclado
  2. Métodos UITextViewDelegate
  3. Manejo de eventos Pusher.

Ver y eventos de teclado

Abra el TextEditorViewController y actualícelo con los siguientes métodos:

anular func viewDidLoad () {
        super.viewDidLoad ()

        // disparador de notificación
        NotificationCenter.default.addObserver (self, selector: #selector (keyboardWillShow), nombre: NSNotification.Name.UIKeyboardWillShow, objeto: nulo)
        NotificationCenter.default.addObserver (self, selector: #selector (keyboardWillHide), nombre: NSNotification.Name.UIKeyboardWillHide, objeto: nulo)

        // Reconocimiento de gestos
        view.addGestureRecognizer (UITapGestureRecognizer (target: self, action: #selector (tappedAwayFunction (_ :))))

        // Establecer el controlador como el delegado textView
        textView.delegate = self

        // Establecer la ID del dispositivo
        randomUuid = UIDevice.current.identifierForVendor! .uuidString

        // Escucha los cambios de Pusher
        listenForChanges ()
    }

    anular func viewWillAppear (_ animated: Bool) {
        super.viewWillAppear (animado)

        if self.textView.text == "" {
            self.textView.text = placeHolderText
            self.textView.textColor = UIColor.lightGray
        }
    }

    func keyboardWillShow (notificación: NSNotification) {
        si se deja keyboardSize = (notify.userInfo? [UIKeyboardFrameBeginUserInfoKey] como? NSValue) ?. cgRectValue {
            if self.charactersLabel.frame.origin.y == 1.0 {
                self.charactersLabel.frame.origin.y - = keyboardSize.height
            }
        }
    }

    func keyboardWillHide (notificación: NSNotification) {
        si se deja keyboardSize = (notify.userInfo? [UIKeyboardFrameBeginUserInfoKey] como? NSValue) ?. cgRectValue {
            if self.view.frame.origin.y! = 1.0 {
                self.charactersLabel.frame.origin.y + = keyboardSize.height
            }
        }
    }

En el método viewDidLoad, registramos las funciones del teclado para que respondan a los eventos del teclado. También agregamos reconocedores de gestos que cerrarán el teclado cuando toque fuera de UITextView. Y configuramos el delegado textView en el controlador mismo. Finalmente, llamamos a una función para escuchar nuevas actualizaciones (crearemos esto más adelante).

En el método viewWillAppear, simplemente pirateamos el UITextView para que tenga un texto de marcador de posición, porque, de forma predeterminada, el UITextView no tiene esa característica. Me pregunto por qué, Apple ...

En las funciones keyboardWillShow y keyboardWillHide, hicimos que la etiqueta de recuento de caracteres se elevara con el teclado y descendiera con él, respectivamente. Esto evitará que el teclado cubra la etiqueta cuando esté activo.

Métodos UITextViewDelegate

Actualice TextEditorViewController con lo siguiente:

func textViewDidChange (_ textView: UITextView) {
        charactersLabel.text = String (formato: "% i Characters", textView.text.characters.count)
        if textView.text.characters.count> = 2 {
            sendToPusher (texto: textView.text)
        }
    }
    func textViewShouldBeginEditing (_ textView: UITextView) -> Bool {
        self.textView.textColor = UIColor.black
        if self.textView.text == placeHolderText {
            self.textView.text = ""
        }
        volver verdadero
    }
    func textViewDidEndEditing (_ textView: UITextView) {
        if textView.text == "" {
            self.textView.text = placeHolderText
            self.textView.textColor = UIColor.lightGray
        }
    }
    func tappedAwayFunction (_ remitente: UITapGestureRecognizer) {
        textView.resignFirstResponder ()
    }

El método textViewDidChange simplemente actualiza la etiqueta de recuento de caracteres y también envía los cambios a Pusher utilizando nuestra API de back-end (que crearemos en un minuto).

TextViewShouldBeginEditing se obtiene de UITextViewDelegate y se activa cuando la vista de texto está a punto de editarse. Aquí, básicamente jugamos con el marcador de posición, igual que el método textViewDidEndEditing.

Finalmente, en tappedAwayFunction definimos la devolución de llamada de evento para el gesto que registramos en la sección anterior. En el método, básicamente descartamos el teclado.

Manejo de eventos de Pusher

Actualice el controlador con los siguientes métodos:

func sendToPusher (texto: cadena) {
        let params: Parameters = ["text": text, "from": randomUuid]
        Alamofire.request (TextEditorViewController.API_ENDPOINT + "/ update_text", método: .post, parámetros: params) .validate (). ResponseJSON {respuesta en
            cambiar respuesta.resultado {
            caso de éxito:
                print ("Exitoso")
            caso .failure (dejar error):
                imprimir (error)
            }
        }
    }
    func listenForChanges () {
        pusher = Pusher (clave: "PUSHER_KEY", opciones: PusherClientOptions (
            host: .cluster ("PUSHER_CLUSTER")
        ))
        let channel = pusher.subscribe ("colaborar")
        let _ = channel.bind (eventName: "text_update", callback: {(data: Any?) -> Anular en
            si let data = data as? [Cadena: AnyObject] {
                let fromDeviceId = data ["deviceId"] como! Cuerda
                if fromDeviceId! = self.randomUuid {
                    let text = data ["text"] como! Cuerda
                    self.textView.text = texto
                    self.charactersLabel.text = String (formato: "% i Characters", text.characters.count)
                }
            }
        })
        pusher.connect ()
    }

En el método sendToPusher, enviamos la carga útil a nuestra aplicación de back-end utilizando AlamoFire, que, a su vez, la enviará a Pusher.

En el método listenForChanges, escuchamos los cambios en el texto y, si hay alguno, aplicamos los cambios a la vista de texto.

Recuerde reemplazar la clave y el clúster con el valor real que obtuvo de su panel de control de Pusher.

Si ha seguido de cerca el tutorial, su TextEditorViewController debería verse así:

importar UIKit
    importar PusherSwift
    importar Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        static let API_ENDPOINT = "http: // localhost: 4000";
        @IBOutlet débil var textView: UITextView!
        @IBOutlet débil var charactersLabel: UILabel!
        var pusher: Pusher!
        var chillPill = true
        var placeHolderText = "Comience a escribir ..."
        var randomUuid: String = ""
        anular func viewDidLoad () {
            super.viewDidLoad ()
            // disparador de notificación
            NotificationCenter.default.addObserver (self, selector: #selector (keyboardWillShow), nombre: NSNotification.Name.UIKeyboardWillShow, objeto: nulo)
            NotificationCenter.default.addObserver (self, selector: #selector (keyboardWillHide), nombre: NSNotification.Name.UIKeyboardWillHide, objeto: nulo)
            // Reconocimiento de gestos
            view.addGestureRecognizer (UITapGestureRecognizer (target: self, action: #selector (tappedAwayFunction (_ :))))
            // Establecer el controlador como el delegado textView
            textView.delegate = self
            // Establecer la ID del dispositivo
            randomUuid = UIDevice.current.identifierForVendor! .uuidString
            // Escucha los cambios de Pusher
            listenForChanges ()
        }
        anular func viewWillAppear (_ animated: Bool) {
            super.viewWillAppear (animado)
            if self.textView.text == "" {
                self.textView.text = placeHolderText
                self.textView.textColor = UIColor.lightGray
            }
        }
        func keyboardWillShow (notificación: NSNotification) {
            si se deja keyboardSize = (notify.userInfo? [UIKeyboardFrameBeginUserInfoKey] como? NSValue) ?. cgRectValue {
                if self.charactersLabel.frame.origin.y == 1.0 {
                    self.charactersLabel.frame.origin.y - = keyboardSize.height
                }
            }
        }
        func keyboardWillHide (notificación: NSNotification) {
            si se deja keyboardSize = (notify.userInfo? [UIKeyboardFrameBeginUserInfoKey] como? NSValue) ?. cgRectValue {
                if self.view.frame.origin.y! = 1.0 {
                    self.charactersLabel.frame.origin.y + = keyboardSize.height
                }
            }
        }
        func textViewDidChange (_ textView: UITextView) {
            charactersLabel.text = String (formato: "% i Characters", textView.text.characters.count)
            if textView.text.characters.count> = 2 {
                sendToPusher (texto: textView.text)
            }
        }
        func textViewShouldBeginEditing (_ textView: UITextView) -> Bool {
            self.textView.textColor = UIColor.black
            if self.textView.text == placeHolderText {
                self.textView.text = ""
            }
            volver verdadero
        }
        func textViewDidEndEditing (_ textView: UITextView) {
            if textView.text == "" {
                self.textView.text = placeHolderText
                self.textView.textColor = UIColor.lightGray
            }
        }
        func tappedAwayFunction (_ remitente: UITapGestureRecognizer) {
            textView.resignFirstResponder ()
        }
        func sendToPusher (texto: cadena) {
            let params: Parameters = ["text": text, "from": randomUuid]
            Alamofire.request (TextEditorViewController.API_ENDPOINT + "/ update_text", método: .post, parámetros: params) .validate (). ResponseJSON {respuesta en
                cambiar respuesta.resultado {
                caso de éxito:
                    print ("Exitoso")
                caso .failure (dejar error):
                    imprimir (error)
                }
            }
        }
        func listenForChanges () {
            pusher = Pusher (clave: "PUSHER_KEY", opciones: PusherClientOptions (
                host: .cluster ("PUSHER_CLUSTER")
            ))
            let channel = pusher.subscribe ("colaborar")
            let _ = channel.bind (eventName: "text_update", callback: {(data: Any?) -> Anular en
                si let data = data as? [Cadena: AnyObject] {
                    let fromDeviceId = data ["deviceId"] como! Cuerda
                    if fromDeviceId! = self.randomUuid {
                        let text = data ["text"] como! Cuerda
                        self.textView.text = texto
                        self.charactersLabel.text = String (formato: "% i Characters", text.characters.count)
                    }
                }
            })
            pusher.connect ()
        }
    }

¡Excelente! Ahora tenemos que hacer el backend de la aplicación.

Compilación de la aplicación de nodo de fondo

Ahora que hemos terminado con la parte Swift, podemos enfocarnos en crear el backend Node.js para la aplicación. Vamos a utilizar Express para que podamos ejecutar algo rápidamente.

Cree un directorio para la aplicación web y luego cree algunos archivos nuevos.

El archivo index.js:

let path = require ('ruta');
    let Pusher = require ('empujador');
    let express = require ('express');
    let bodyParser = require ('body-parser');
    let app = express ();
    let pusher = new Pusher (require ('./ config.js'));
    app.use (bodyParser.json ());
    app.use (bodyParser.urlencoded ({extended: false}));
    app.post ('/ update_text', function (req, res) {
      carga útil var = {text: req.body.text, deviceId: req.body.from}
      pusher.trigger ('collabo', 'text_update', payload)
      res.json ({éxito: 200})
    });
    app.use (function (req, res, next) {
        var err = nuevo error ('No encontrado');
        err.status = 404;
        siguiente (err);
    });
    module.exports = app;
    app.listen (4000, function () {
      console.log ('¡Aplicación escuchando en el puerto 4000!');
    });

En el archivo JS anterior, estamos utilizando Express para crear una aplicación simple. En la ruta / update_text, simplemente recibimos la carga útil y se la pasamos a Pusher. Nada complicado allí.

Cree un archivo package.json también:

{
      "main": "index.js",
      "dependencias": {
        "body-parser": "^ 1.17.2",
        "express": "^ 4.15.3",
        "ruta": "^ 0.12.7",
        "empujador": "^ 1.5.1"
      }
    }

El archivo package.json es donde definimos todas las dependencias de NPM.

El último archivo a crear es un archivo config.js. Aquí es donde definiremos los valores de configuración para nuestra aplicación Pusher:

module.exports = {
      ID de aplicación: 'PUSHER_ID',
      clave: 'PUSHER_KEY',
      secreto: 'PUSHER_SECRET',
      clúster: 'PUSHER_CLUSTER',
      cifrado: verdadero
    };
Recuerde reemplazar la clave y el clúster con el valor real que obtuvo de su panel de control de Pusher.

Ahora ejecute npm install en el directorio y luego nodo index.js una vez que se complete la instalación de npm. ¡Debería ver una aplicación escuchando en el puerto 4000! mensaje.

Probar la aplicación

Una vez que tenga su servidor web de nodo local en ejecución, deberá realizar algunos cambios para que su aplicación pueda comunicarse con el servidor web local. En el archivo info.plist, realice los siguientes cambios:

Con este cambio, puede compilar y ejecutar su aplicación y se comunicará directamente con su aplicación web local.

Conclusión

En este artículo, hemos cubierto cómo construir un editor de texto colaborativo en tiempo real en iOS usando Pusher. Con suerte, has aprendido una o dos cosas siguiendo el tutorial. Para practicar, puede expandir los estados para admitir más instancias.

Esta publicación se publicó por primera vez a Pusher.