Blog

Lista de tareas con IONIC 4+Javascript

El objetivo

En este ejercicio vamos a desarrollar una aplicación para gestionar varias listas de tareas.

La funcionalidad a implementar será la siguiente:

  • Gestionar dos listas de tareas independientes.
  • Añadir tareas indicando la fecha, la descripción de la tarea, y un icono identificativo de la prioridad.
  • Borrar de golpe todas las tareas de una lista.
  • Borrar una sola tarea de una lista determinada mediante un botón oculto que se visualizará al deslizar hacia la derecha la tarea a borrar.
  • Pedir confirmación antes de borrar cualquier tarea.
  • Visualizar y editar los detalles de cada tarea haciendo clic sobre el elemento correspondiente de la lista.
  • Permitir ordenar las tareas arrastrando cualquier elemento a una nueva posición.

Elementos HTML de IONIC 4

El encabezado principal de la aplicación

Utilizaremos tres botones de tipo icono para acceder a las funciones básicas desde la pantalla principal:

  • <ion-icon name="trash"></ion-icon>: Para borrar todas las tareas de la lista seleccionada.
  • <ion-icon name="reorder"></ion-icon>: Para activar los botones que nos permitirán reordenar las tareas arrastrando y soltando.
  • <ion-icon name="add"></ion-icon>: Para crear una nueva tarea.

Los agruparemos junto con el título de la aplicación en el encabezado del fichero index.html de la siguiente forma:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>ToDo!</ion-title>
    <ion-buttons slot="primary">
      <ion-button onclick="deleteItem()" color="danger">
        <ion-icon slot="icon-only" name="trash"></ion-icon>
      </ion-button>  
      <ion-button onclick="toggleReorder()">
        <ion-icon slot="icon-only" name="reorder"></ion-icon>
      </ion-button>
      <ion-button onclick="addEditItem()">
        <ion-icon slot="icon-only" name="add"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>       
</ion-header>

Más adelante veremos el código JavaScript necesario para llevar a cabo las funciones correspondientes:

  • deleteItem()
  • toggleReorder()
  • addEditItem()

Las listas de tareas

Para mantener dos listas separadas, desde el archivo index.html utilizaremos el elemento <ion-tabs></ion-tabs>  que personalizaremos escogiendo un icono y un texto descriptivo. Por ejemplo, si quisiéramos utilizar una lista de tareas para el instituto y otra para casa podríamos utilizar los iconos school y home respectivamente:

<ion-tabs color="primary">
  <ion-tab tab="school">
    <ion-list lines="full">
      <ion-reorder-group>
        ...
      </ion-reorder-group>
    </ion-list>
  </ion-tab>
  <ion-tab tab="home">
    <ion-list lines="full">
      <ion-reorder-group>
        ...
      </ion-reorder-group>
    </ion-list>
  </ion-tab>
  <ion-tab-bar slot="bottom" color="primary">
    <ion-tab-button tab="home"><ion-icon name="home"></ion-icon></ion-tab-button>
    <ion-tab-button tab="school"><ion-icon name="school"></ion-icon></ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

Además utilizaremos el nuevo elemento <ion-reorder-group></ion-reorder-group> que nos permitirá reordenar las tareas que coloquemos dentro. Bastará con pulsar encima de un botón habilitado a tal efecto, y sin soltar arrastrar la tarea en cuestión a la nueva posición. En la documentación de IONIC podemos observar cómo esta simple etiqueta gestiona todo el proceso de arrastrar y soltar.

Las tareas dentro de las listas

Utilizaremos elementos <ion-item-sliding></ion-item-sliding> para cada tarea dentro de las listas. De esta forma podremos tener oculto un botón rojo con una papelera, que aparecerá al deslizar la tarea hacia la derecha, y nos permitirá borrar de la lista sólo ese elemento:

<ion-item-sliding>
  ...
  <ion-item-option color="danger" onclick="...">
    <ion-icon slot="icon-only" name="trash"></ion-icon>
  </ion-item-option>
</ion-item-sliding>

Utilizaremos también un elemento <ion-item></ion-item> para agrupar todos los datos de la tarea. De esta forma, bastará con hacer clic sobre una determinada tarea para acceder a los detalles de la misma: <ion-item onclick="..."></ion-item> . Un poco más adelante detallaremos la funcionalidad JavaScript necesaria para crear y mostrar un formulario que nos permita visualizar y editar la tarea seleccionada.

Resaltaremos la descripción de la tarea con una etiqueta <h2></h2>, y a continuación mostremos la fecha de la misma dentro de un párrafo (etiqueta <p></p>). Encerraremos ambos elementos con una etiqueta <ion-label text-wrap></ion-label> que permitirá que el tamaño de cada elemento de la lista se ajuste para contener todo el texto descriptivo de la tarea.

Resumiendo, cada elemento de la lista de tareas vendrá especificado por el siguiente código HTML:

<ion-item-sliding>
  <ion-item onclick="...">
    <ion-label text-wrap>
      <h2>...</h2>
      <p>...</p>
    </ion-label>
    <ion-icon slot="end" name="..."></ion-icon>
    <ion-reorder slot="end"></ion-reorder>
  </ion-item>
  <ion-item-options side="start">
    <ion-item-option color="danger" onclick="...">
      <ion-icon slot="icon-only" name="trash"></ion-icon>
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

El formulario con los detalles de cada tarea

Para visualizar y editar los detalles de cada tarea (al crear una nueva, o al hacer clic sobre una ya existente) utilizaremos el siguiente código HTML:

<ion-header>
  <ion-toolbar>
    <ion-title>ToDo - Task details</ion-title>
    <ion-buttons slot="primary">
      <ion-button color="danger"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
      <ion-button color="primary"><ion-icon slot="icon-only" name="checkmark"></ion-icon></ion-button>
    </ion-buttons>       
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item>
      <ion-label position="floating">Select date</ion-label>
      <ion-datetime display-format="D MMM YYYY" max="2050-12-31" value="..."></ion-datetime>            
    </ion-item>
    <ion-item>
      <ion-label position="floating">Enter task</ion-label>
      <ion-input value="..."></ion-input>
    </ion-item>
  </ion-list>

  <ion-segment value="...">
    <ion-segment-button value="radio-button-off">
      <ion-icon name="radio-button-off"></ion-icon>
    </ion-segment-button>  
    <ion-segment-button value="radio-button-on">
      <ion-icon name="radio-button-on"></ion-icon>
    </ion-segment-button>  
    <ion-segment-button value="snow">
      <ion-icon name="snow"></ion-icon>
    </ion-segment-button>
    <ion-segment-button value="flame">
      <ion-icon name="flame"></ion-icon>
    </ion-segment-button>
  </ion-segment>
</ion-content>

En el encabezado (<ion-header></ion-header>) simplemente tenemos dos botones para confirmar o cancelar la edición o creación de la tarea.

En el contenido principal del formulario (<ion-content></ion-content>)  tendremos tres campos:

  • <ion-datetime><ion-datetime>: Para indicar la fecha de la tarea. Si estamos creando una nueva tarea, en dicho valor introduciremos la fecha actual utilizando código JavaScript.
  • <ion-input></ion-input>: Para introducir o modificar la descripción de la tarea. Estará en blanco si se trata de una tarea nueva.
  • <ion-segment></ion-segment>: Para seleccionar la prioridad de la tarea. En nuestro ejemplo proponemos 4 opciones (radio-button-off para tareas pendientes, radio-button-on para tarea finalizada, snow para una prioridad baja, y flame para una prioridad alta), aunque los iconos utilizados son una simple sugerencia, y se pueden cambiar fácilmente según el gusto de cada uno.

El código JavaScript

Accediendo a cada una de las listas

Por simplificar un poco el código, utilizaremos tres funciones para acceder y actualizar los dos elementos principales (tabslists):

function getTab() {
    return(document.querySelector('ion-tab:not(.tab-hidden)'));
}

function getList(tab = getTab()) {
    let list = localStorage.getItem('todo-list-'+tab.tab);
    return list ? JSON.parse(list) : [];
}

function saveList(tab, list) {
    localStorage.setItem('todo-list-'+tab.tab, JSON.stringify(list));
    printList(tab);
}

La última función (saveList()) nos permitirá guardar de manera persistente cada una de las listas de tareas, utilizando localStorage, de forma que al actualizar el contenido del navegador (o cerrar la app y volverla a abrir), podamos volver a cargar la lista de tareas.

Construyendo cada elemento de la lista de tareas

Mediante la siguiente función obtendremos todo el código necesario para cada tarea, variando simplemente el texto descriptivo de la misma, la fecha, y el icono indicativo de la prioridad:

function printList(tab) {
    tab.querySelector('ion-reorder-group').innerHTML = "";

    getList(tab).forEach((item, index) => {
        tab.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
           <ion-item onClick="addEditItem(`+index+`)">
             <ion-label text-wrap>
               <h2>`+item.text+`</h2>
               <p>`+item.date.slice(0,10)+`</p>
             </ion-label>
             <ion-icon slot="end" name="`+item.icon+`"></ion-icon>
             <ion-reorder slot="end"></ion-reorder>
          </ion-item>
          <ion-item-options side="start">
            <ion-item-option color="danger" onClick="deleteItem(`+index+`)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
            </ion-item-option>
          </ion-item-options>
        </ion-item-sliding>`;
    });    
}

Para añadir una nueva tarea o editar los detalles de una tarea existente

Utilizaremos la misma función (addEditItem(index)) para añadir una nueva tarea o modificar una existente. En ella, siguiendo las instrucciones indicadas en la documentación de IONIC, crearemos un cuadro de diálogo modal que mostrará el formulario para introducir o actualizar los detalles de la tarea:

function addEditItem(index = false) {
    closeItems();
    let list = getList();
    let item = null;

    if (index !== false) item = list[index];
    else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

    const modal = document.createElement('ion-modal');
    modal.component = document.createElement('div');
    modal.component.innerHTML = `
        <ion-header>
            ...
        </ion-header>
        <ion-content>
            ...
        </ion-content>`;
    
 modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

    modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

        if (!newText.length) {
            error('The task cannot be empty');
        }
        else {
            let newItem = { text:newText, date:newDate, icon:newIcon };
            if (index !== false) list[index] = newItem; 
            else list.unshift(newItem);
            saveList(getTab(), list);
            modal.dismiss();
        }
    });

    document.querySelector('ion-app').appendChild(modal);
    modal.present();
}

En la primera parte de la función, si recibimos como parámetro la tarea a modificar, recuperaremos los detalles de la misma (fecha, texto descriptivo e icono con la prioridad):

if (index !== false) item = list[index];
else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

Como se puede observar en la fecha, si en vez de modificar una tarea existente, estamos creando una nueva (parámetro de la función con valor false), entonces obtendremos la fecha actual.

A continuación, crearemos el formulario de edición o creación de tareas, con un encabezado con los botones correspondientes para cancelar y confirmar, y también con el contenido principal, utilizando el código HTML indicado previamente:

const modal = document.createElement('ion-modal');
modal.component = document.createElement('div');
modal.component.innerHTML = `
    <ion-header>
      ...
    </ion-header>
    <ion-content>
      ...
    </ion-content>`;

Finalmente, nos queda capturar los eventos de clic en el botón de cancelar (color="danger") o confirmar (color="primary"). En el primer caso simplemente cerraremos el cuadro de diálogo sin hacer nada más:

modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

En caso de confirmar la edición o creación de una nueva tarea, primero cogeremos los valores introducidos en el formulario:

modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

    ...
});

En segundo lugar comprobaremos la longitud de la descripción de la tarea, para asegurarnos que no está vacía, en cuyo caso mostraríamos un error:

function error(message) {
  const alert = document.createElement('ion-alert');
  alert.message = message;
  alert.buttons = ['OK'];

  document.querySelector('ion-app').appendChild(alert);
  alert.present();
}

function addEditItem(index = false) {
    ...
        if (!newText.length) {
            error('The task cannot be empty');
        }
    ...
}});

Si estamos modificando una tarea existente, crearemos la nueva tarea con los nuevos datos del formulario, la reemplazaremos por la existente, guardaremos la lista, y cerraremos el cuadro de diálogo.

Si estamos añadiendo una nueva tarea, crearemos un nuevo elemento <ion-item-sliding></ion-item-sliding> que contendrá la fecha, descripción e icono introducidos en el formulario. A continuación añadiremos dicha tarea a la lista que estemos modificando, la guardaremos, y cerraremos el cuadro de diálogo:

let newItem = { text:newText, date:newDate, icon:newIcon };

if (index !== false) { list[index] = newItem; }
else { list.unshift(newItem); }

saveList(getTab(), list);
modal.dismiss();

Reordenando las tareas

La totalidad de la funcionalidad necesaria para reordenar las tareas de una lista ya nos la proporciona IONIC mediante el elemento <ion-reorder></ion-reorder>. Lo único que debemos hacer por nuestra parte, es cambiar el valor del atributo disabled del elemento <ion-reorder-group></ion-reorder-group> de la lista seleccionada:

function toggleReorder() {
    closeItems();
    let reorder = getTab().querySelector('ion-reorder-group');
    reorder.disabled = !reorder.disabled;
}

En caso de que se esté mostrando algún botón de borrado de una tarea, se ocultará para habilitar correctamente la funcionalidad de reordenar elementos.

Cuando la opción de reordenar se encuentra activa, se mostrará un nuevo icono en todas las tareas. Si lo mantenemos pulsado, nos permitirá arrastrar la tarea correspondiente para soltarla en una nueva posición.

Al volver a pulsar el botón de reordenar ubicado en el encabezado de la aplicación, la opción de reordenar se desactivará, evitando que cambiemos accidentalmente el orden de las tareas.

Borrando tareas

Para borrar todas las tareas de la lista seleccionada, pulsaremos el botón que hemos puesto en el encabezado principal de la aplicación. Y para borrar una tarea específica, pulsaremos el botón que aparece al deslizar la tarea hacia la derecha. En ambos casos, utilizaremos la misma función JavaScript (deleteItem(index)), pasando como parámetro el elemento HTML que contiene la tarea a borrar, o el valor false para borrar todas las tareas de la lista seleccionada.

Para evitar borrados accidentales, pediremos confirmación al usuario utilizando un cuadro de diálogo de tipo Alert que ya nos proporciona IONIC:

function deleteItem(index = false) {
    const alert = document.createElement('ion-alert');
  
    alert.header = index !== false ? 'Delete item' : 'Delete all',
    alert.message = 'Are you sure?',
    alert.buttons =  
            [{
                text: 'YES',
                handler: () => {
                    let list = getList();
                    if (index !== false) { list.splice(index, 1); }
                    else { list.length = 0; }
                    saveList(getTab(), list);
                }
            }, {
                text: 'NO',
                role: 'cancel'
            }]

    document.querySelector('ion-app').appendChild(alert);
    alert.present();
}

Como se puede observar, al confirmar el borrado, si se recibe un elemento por parámetro, sólo se borra dicho elemento. En caso contrario, se borrará toda la lista, utilizando una cadena vacía para eliminar todo el código HTML que contenía. Finalmente, guardaremos la lista correspondiente:

...
    let list = getList();
    if (index !== false) { list.splice(index, 1); }
    else { list.length = 0; }
    saveList(getTab(), list);
...

Eventos a tener en cuenta

Cada vez que reordenemos las tareas de alguna lista, deberemos volverla a guardar para que aparezcan en el orden correcto al volver a abrir la aplicación. Para ello capturaremos el evento ionItemReorder, tal como se indica en la documentación de IONIC, apartado de JavaScript:

document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
...
function moveItem(indexes) {
    let tab = getTab();
    let list = getList(tab);
    let item = list[indexes.from];
    list.splice(indexes.from, 1);
    list.splice(indexes.to, 0, item);
    indexes.complete();
    saveList(tab, list);
}

Cada vez que cerremos un cuadro de diálogo (eventos ionModalDidDismiss y ionAlertDidDismiss) cerraremos los elementos de la lista que pudieran estar deslizados hacia la derecha.

document.addEventListener("ionModalDidDismiss", closeItems);
document.addEventListener("ionAlertDidDismiss", closeItems);
...
function closeItems() {
    getTab().querySelector('ion-list').closeSlidingItems();
}

Y por último, cada vez que iniciemos la aplicación (evento onload), leeremos las listas que se encuentran guardadas en el navegador:

function onLoad() {
    ...
    document.querySelectorAll('ion-tab').forEach(function(t) { printList(t); });
}

En resumen…

El fichero index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Reorder</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="cordova.js"></script>
  <script src="todo.js"></script>
</head>

<body onload="onLoad()">
  <ion-app>
    <ion-menu content-id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="danger">
          <ion-buttons slot="start">
            <ion-menu-button></ion-menu-button>
          </ion-buttons>  
          <ion-title>Menu</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-content>
        <ion-menu-toggle>
          <ion-list lines="full">
            <ion-item-divider>
              <ion-label>Reorder and Share</ion-label>
            </ion-item-divider>
            <ion-item onclick="toggleReorder()">
              <ion-label>Enable/Disable reorder</ion-label>
              <ion-icon slot="end" name="reorder"></ion-icon>
            </ion-item>
            <ion-item onclick="share()">
              <ion-label>Share</ion-label>
              <ion-icon slot="end" name="share"></ion-icon>
            </ion-item>
            <ion-item-divider>
              <ion-label>Delete</ion-label>
            </ion-item-divider>          
            <ion-item onclick="deleteItem()">
              <ion-label>Delete all</ion-label>
              <ion-icon slot="end" name="trash" color="danger"></ion-icon>
            </ion-item>
          </ion-list>
        </ion-menu-toggle>
      </ion-content>
    </ion-menu>

    <div class="ion-page" id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="primary">
          <ion-buttons slot="start">
            <ion-button>
              <ion-menu-button></ion-menu-button>
            </ion-button>
          </ion-buttons>          
          <ion-title>ToDo!</ion-title>
          <ion-buttons slot="end">
            <ion-button onclick="addEditItem()">
              <ion-icon name="add" slot="icon-only"></ion-icon>
            </ion-button>
          </ion-buttons>            
        </ion-toolbar>
      </ion-header>
      <ion-content>
        <ion-tabs>
          <ion-tab tab="school">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab tab="home">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab-bar slot="bottom" color="primary">
            <ion-tab-button tab="school">
              <ion-icon name="school"></ion-icon>
            </ion-tab-button>
            <ion-tab-button tab="home">
              <ion-icon name="home"></ion-icon>
            </ion-tab-button>
          </ion-tab-bar>
        </ion-tabs>
      </ion-content>
    </div>
  </ion-app>
</body>
</html>

El fichero todo.js

function onLoad() { 
    document.addEventListener("ionModalDidDismiss", closeItems);
    document.addEventListener("ionAlertDidDismiss", closeItems);
    document.addEventListener("ionDidOpen", closeItems);
    document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
    document.querySelectorAll('ion-tab').forEach(function(t) { printList(t); });
}

function getTab() {
    return(document.querySelector('ion-tab:not(.tab-hidden)'));
}

function getList(tab = getTab()) {
    let list = localStorage.getItem('todo-list-'+tab.tab);
    return list ? JSON.parse(list) : [];
}

function saveList(tab, list) {
    localStorage.setItem('todo-list-'+tab.tab, JSON.stringify(list));
    printList(tab);
}

function printList(tab) {
    tab.querySelector('ion-reorder-group').innerHTML = "";

    getList(tab).forEach((item, index) => {
        tab.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
           <ion-item onClick="addEditItem(`+index+`)">
             <ion-label text-wrap>
               <h2>`+item.text+`</h2>
               <p>`+item.date.slice(0,10)+`</p>
             </ion-label>
             <ion-icon slot="end" name="`+item.icon+`"></ion-icon>
             <ion-reorder slot="end"></ion-reorder>
          </ion-item>
          <ion-item-options side="start">
            <ion-item-option color="danger" onClick="deleteItem(`+index+`)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
            </ion-item-option>
          </ion-item-options>
        </ion-item-sliding>`;
    });    
}

function error(message) {
  const alert = document.createElement('ion-alert');
  alert.message = message;
  alert.buttons = ['OK'];

  document.querySelector('ion-app').appendChild(alert);
  alert.present();
}

function toggleReorder() {
    closeItems();
    let reorder = getTab().querySelector('ion-reorder-group');
    reorder.disabled = !reorder.disabled;
}

function closeItems() {
    getTab().querySelector('ion-list').closeSlidingItems();
}

function addEditItem(index = false) {
    closeItems();
    let list = getList();
    let item = null;

    if (index !== false) item = list[index];
    else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

    const modal = document.createElement('ion-modal');
    modal.component = document.createElement('div');
    modal.component.innerHTML = `
        <ion-header>
            <ion-toolbar>
                <ion-title>ToDo - `+(index !== false ? 'Edit task' : 'New task')+`</ion-title>
                <ion-buttons slot="primary">
                    <ion-button color="danger"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
                    <ion-button color="primary"><ion-icon slot="icon-only" name="checkmark"></ion-icon></ion-button>
                </ion-buttons>       
            </ion-toolbar>
        </ion-header>
        <ion-content>
            <ion-list>
                <ion-item>
                    <ion-label position="floating">Select date</ion-label>
                    <ion-datetime display-format="D MMM YYYY" max="2050-12-31" value="`+item.date+`"></ion-datetime>            
                </ion-item>
                <ion-item>
                    <ion-label position="floating">Enter task</ion-label>
                    <ion-input value="`+item.text+`"></ion-input>
                </ion-item>
            </ion-list>
            <ion-segment value="`+item.icon+`">
                <ion-segment-button value="radio-button-off">
                    <ion-icon name="radio-button-off"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="radio-button-on">
                    <ion-icon name="radio-button-on"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="snow">
                    <ion-icon name="snow"></ion-icon>
                </ion-segment-button>
                <ion-segment-button value="flame">
                    <ion-icon name="flame"></ion-icon>
                </ion-segment-button>
            </ion-segment>
        </ion-content>`;

    modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

    modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

        if (!newText.length) {
            error('The task cannot be empty');
        }
        else {
            let newItem = { text:newText, date:newDate, icon:newIcon };
            if (index !== false) list[index] = newItem; 
            else list.unshift(newItem);
            saveList(getTab(), list);
            modal.dismiss();
        }
    });

    document.querySelector('ion-app').appendChild(modal);
    modal.present();
}

function moveItem(indexes) {
    let tab = getTab();
    let list = getList(tab);
    let item = list[indexes.from];
    list.splice(indexes.from, 1);
    list.splice(indexes.to, 0, item);
    indexes.complete();
    saveList(tab, list);
}

function deleteItem(index = false) {
    const alert = document.createElement('ion-alert');
  
    alert.header = index !== false ? 'Delete item' : 'Delete all',
    alert.message = 'Are you sure?',
    alert.buttons =  
            [{
                text: 'YES',
                handler: () => {
                    let list = getList();
                    if (index !== false) { list.splice(index, 1); }
                    else { list.length = 0; }
                    saveList(getTab(), list);
                }
            }, {
                text: 'NO',
                role: 'cancel'
            }]

    document.querySelector('ion-app').appendChild(alert);
    alert.present();
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la aplicación de lista de tareas y probar la funcionalidad resultante utilizando el código especificado.

Galería de imágenes con IONIC 4

El objetivo

En este ejercicio pretendemos desarrollar una aplicación para visualizar imágenes que se encuentran agrupadas en categorías. Para ello sugerimos implementar la siguiente funcionalidad:

  • Crear pestañas o tabs para cada una de las categorías, agrupando así las imágenes, y permitiendo con un solo click cambiar entre una categoría u otra.
  • Utilizando la pantalla táctil de nuestros dispositivos, habilitar el gesto swipe para acceder a la imagen anterior o posterior.
  • Activar la reproducción automática mediante un botón, permitiendo a su vez pausar la visualización mediante otro botón.
  • Permitir ajustar la velocidad en la que se van mostrando las imágenes.

Una posible sugerencia de la interfaz de la aplicación sería la siguiente:

Elementos HTML de IONIC 4

Slides

La funcionalidad principal de este ejercicio nos la proporciona IONIC mediante el elemento <ion-slides></ion-slides>, donde ya se encuentra encapsulada la respuesta a los gestos de swipe y la reproducción automática, tal como se explica en la documentación.

Por ejemplo, para poder visualizar de manera secuencial las imágenes alicante1.jpg, alicante2.jpg y alicante3.jpg, ubicadas en la carpeta img, bastaría con insertarlas dentro de elementos <ion-slide></ion-slide>, y agruparlas todas con <ion-slides></ions-slides> de la siguiente forma:

<ion-slides>
  <ion-slide><img src="img/alicante1.jpg" /></ion-slide>
  <ion-slide><img src="img/alicante2.jpg" /></ion-slide>
  <ion-slide><img src="img/alicante3.jpg" /></ion-slide>
</ion-slides>

También podríamos haber utilizado el elemento <ion-img></ion-img>, que tiene un comportamiento casi idéntico al <img> de HTML. La diferencia es que esta última versión de IONIC optimiza especialmente aquellas páginas que tienen una lista muy grande de imágenes, ya que sólo se cargarían cuando fueran a estar visibles.

Tabs

El siguiente elemento clave que utilizaremos es <ion-tabs></ion-tabs>, ya que nos permitirá agrupar las fotos por categoría. Nos proporciona la funcionalidad necesaria para mostrar unas secciones de nuestro código HTML mientras se ocultan otras.

Conviene destacar además las numerosas opciones de personalización, tales como el color, que se pueden ajustar especificando el valor deseado en el atributo correspondiente (se puede consultar la documentación para más detalles). Por ejemplo, si queremos que se muestren dos pestañas con el fondo de color rojo, una de ellas etiquetada con el texto Madrid y el icono train, y la otra con París y airplane, procederíamos de la siguiente forma:

<ion-tabs>
  <ion-tab tab="madrid">
      ...
  </ion-tab>
  <ion-tab tab="paris">
      ...
  </ion-tab>
  <ion-tab-bar slot="bottom" color="danger">
    <ion-tab-button tab="madrid">
      <ion-icon name="train"></ion-icon>
      <ion-label>Madrid</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="paris">
      <ion-icon name="airplane"></ion-icon>
      <ion-label>París</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

En nuestro ejemplo, donde queremos agrupar las diapositivas con las imágenes en varias categorías, procederíamos de la siguiente forma:

<ion-tabs>
  <ion-tab tab="madrid">
    <ion-slides>
      <ion-slide><img src="img/madrid1.jpg" /></ion-slide>
      <ion-slide><img src="img/madrid2.jpg" /></ion-slide>
      <ion-slide><img src="img/madrid3.jpg" /></ion-slide>
    </ion-slides>
  </ion-tab>
  <ion-tab tab="paris">
    <ion-slides>
      <ion-slide><img src="img/paris1.jpg" /></ion-slide>
      <ion-slide><img src="img/paris2.jpg" /></ion-slide>
      <ion-slide><img src="img/paris3.jpg" /></ion-slide>
    </ion-slides>
  </ion-tab>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="madrid">
      <ion-icon name="train"></ion-icon>
      <ion-label>Madrid</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="paris">
      <ion-icon name="airplane"></ion-icon>
      <ion-label>París</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

Al hacer clic en la categoría Madrid, se ocultarían las diapositivas de la categoría París, y viceversa.

Además, hacemos uso de los elementos ion-label e ion-icon, que nos permiten establecer el texto y el icono que aparecen en la pestaña, respectivamente. El elemento ion-label acepta un texto libre, mientras que ion-icon permite elegir uno de entre todos los posibles iconos que nos proporciona IONIC.

Botones play/pause

Para iniciar y pausar la reproducción automática de las imágenes, insertaremos dos botones formados simplemente por los iconos  play y pause que nos proporciona IONIC:

<ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
<ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>

Y los insertaremos junto con el título en la parte superior de la pantalla:

<ion-header>
  <ion-toolbar>
    <ion-title>Gallery!</ion-title>
    <ion-buttons slot="primary">
      <ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
      <ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>
    </ion-buttons>
  </ion-toolbar>  
</ion-header>

Un poco más adelante veremos la funcionalidad JavaScript que se ejecutará al pulsar dichos botones.

Para ajustar la velocidad

Para poder ajustar el tiempo que permanece cada diapositiva en la pantalla, sugerimos utilizar un elemento range (<ion-range></ion-range>). Por ejemplo, para poder establecer un tiempo que oscile entre 0 y 5 segundos (5000 milisegundos), podríamos utilizar el siguiente código:

<ion-range min="0" max="5000">
  <ion-icon slot="start" size="small" name="speedometer"></ion-icon>
  <ion-icon slot="end" name="speedometer"></ion-icon>
</ion-range>

Con el objetivo de mejorar el impacto visual del control deslizante, en el ejemplo utilizamos el icono speedometer, que será más pequeño a la izquierda (slot="start" size="small") para indicar que las imágenes irán apareciendo lentamente, y más grande a la derecha (slot="end") para dar a entender que las diapositivas se mostrarán una tras otra más rápidamente.

Sugerimos insertar dicho control también en el elemento <ion-header></ion-header> de la aplicación, para que aparezca justo debajo de la barra que contiene el título y los botones:

<ion-header>
  <ion-toolbar>
    <ion-title>...</ion-title>
    <ion-buttons>
      ...
    </ion-buttons>
  </ion-toolbar>
  <ion-toolbar>
    <ion-item>
      <ion-range>
        ...
      </ion-range>
    </ion-item>
  </ion-toolbar>    
</ion-header>

El código JavaScript

Accediendo a cada elemento

Por simplificar un poco el código, utilizaremos tres funciones para acceder a cada uno de los elementos clave (range, tabs y slides):

function getDelay() {
    return(document.querySelector('ion-range'));
}

function getTabs() {
    return(document.querySelector('ion-tabs'));
}

function getSlides() {
    return(document.querySelectorAll('ion-slides'));
}

Al pulsar play/pause

Para iniciar la reproducción automática de diapositivas bastará con acceder al elemento <ion-slides></ion-slides> correspondiente y ejecutar el método startAutoplay(), tal como se indica en la documentación.

Puesto que tendremos varias pestañas (una por cada categoría) primero deberemos acceder al tab que se encuentre seleccionado (getTabs().getSelected()), que será devuelto en una promesa. Utilizaremos luego simplemente el atributo id para acceder a la categoría activa, iniciando (s.startAutoplay()) o parando (s.stopAutoplay()) la reproducción según el botón que se haya pulsado (play o pause):

function play() {
    getTabs().getSelected().then(function(tab) {
        document.getElementById(tab).startAutoplay();
    });
}

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

Al ajustar la velocidad de reproducción

Cada vez que movamos el control deslizante, se generará un evento ionChange, que nos indicará que tenemos que cambiar la velocidad con la que se muestran las diapositivas:

getDelay().addEventListener("ionChange", init);

...

function init() {
    getSlides().forEach(function(s) {
        s.options = {
            width: window.innerWidth,
            autoplay: {
                delay: 5000 - getDelay().value
            }
        };
    });
}

Puesto que la posición izquierda del control deslizante indicará menos velocidad (tiempo de espera mayor), y la posición derecha mayor velocidad (tiempo de espera menor), bastará con hacer una resta del tiempo máximo de visualización (5000 milisegundos) para calcular el tiempo (delay) que deberá permanecer cada imagen en pantalla:

autoplay: {
    delay: 5000 - getDelay().value
}

Inicializamos también la anchura de cada diapositiva indicando que ocupan toda la ventana:

s.options = {
    width: window.innerWidth,
    ...
};

Al cambiar de pestaña

Cada vez que cambiemos de pestaña, pararemos la reproducción:

getTabs().addEventListener("ionTabsWillChange", pause);

...

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

Velocidad de reproducción inicial

Utilizaremos los eventos onload y resize para inicializar la velocidad de reproducción de las diapositivas que hemos especificado en el control deslizante, así como la anchura de cada una para que ocupen toda la pantalla:

<body onload="onLoad()">

...

function onLoad() {
    window.addEventListener('resize', init);
    ...
    init();
}

...

function init() { 
    getSlides().forEach(function(s) {
        ...
    });
}

En resumen…

El fichero index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="gallery.js"></script>
  <title>Gallery!</title>
</head>
<body onload="onLoad()">
  <ion-app>
    <ion-header>
      <ion-toolbar>
        <ion-title>Gallery!</ion-title>
        <ion-buttons slot="primary">
          <ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
          <ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>
        </ion-buttons>
      </ion-toolbar>
      <ion-toolbar>
        <ion-item>
          <ion-range min="0" max="5000" value="4000">
            <ion-icon slot="start" size="small" name="speedometer"></ion-icon>
            <ion-icon slot="end" name="speedometer"></ion-icon>
          </ion-range>
        </ion-item>
      </ion-toolbar>    
    </ion-header>
    <ion-content>
      <ion-tabs>
        <ion-tab tab="alicante">
          <ion-slides id="alicante">
            <ion-slide><img src="img/alicante1.jpg" /></ion-slide>
            <ion-slide><img src="img/alicante2.jpg" /></ion-slide>
            <ion-slide><img src="img/alicante3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab tab="madrid">
          <ion-slides id="madrid">
            <ion-slide><img src="img/madrid1.jpg" /></ion-slide>
            <ion-slide><img src="img/madrid2.jpg" /></ion-slide>
            <ion-slide><img src="img/madrid3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab tab="paris">
          <ion-slides id="paris">
            <ion-slide><img src="img/paris1.jpg" /></ion-slide>
            <ion-slide><img src="img/paris2.jpg" /></ion-slide>
            <ion-slide><img src="img/paris3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab-bar slot="bottom">
          <ion-tab-button tab="alicante">
            <ion-icon name="walk"></ion-icon>
            <ion-label>Alicante</ion-label>
          </ion-tab-button>
          <ion-tab-button tab="madrid">
            <ion-icon name="train"></ion-icon>
            <ion-label>Madrid</ion-label>
          </ion-tab-button>
          <ion-tab-button tab="paris">
            <ion-icon name="airplane"></ion-icon>
            <ion-label>París</ion-label>
          </ion-tab-button>
        </ion-tab-bar>
      </ion-tabs>
    </ion-content>
  </ion-app>
</body>
</html>

El fichero gallery.js

function onLoad() {
    getDelay().addEventListener("ionChange", init);
    window.addEventListener('resize', init);
    getTabs().addEventListener("ionTabsWillChange", pause);
    init();
}

function getDelay() {
    return(document.querySelector('ion-range'));
}

function getTabs() {
    return(document.querySelector('ion-tabs'));
}

function getSlides() {
    return(document.querySelectorAll('ion-slides'));
}

function init() {
    getSlides().forEach(function(s) {
        s.options = {
            width: window.innerWidth,
            autoplay: {
                delay: 5000 - getDelay().value
            }
        };
    });
}

function play() {
    getTabs().getSelected().then(function(tab) {
        document.getElementById(tab).startAutoplay();
    });
}

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la galería de imágenes y probar la funcionalidad resultante utilizando el código especificado.

Compilando una App con PhoneGap Build

Adobe PhoneGap

Tal como ya sabemos, el uso de dispositivos móviles ha crecido de manera exponencial en los últimos años. Como consecuencia, el número de aplicaciones para dichos dispositivos se han incrementado en la misma medida. Esto ha motivado a su vez que se haya realizado un esfuerzo muy importante para desarrollar frameworks como IONIC que facilitan y mejoran tanto el proceso de programación de aplicaciones híbridas, como el resultado ofrecido por las mismas. Es aquí donde PhoneGap juega un papel importante.

Las aplicaciones híbridas nos proporcionan una gran ventaja respecto a las las nativas: con un sólo código fuente, podremos usar nuestra aplicación en cualquier dispositivo, independientemente del sistema operativo que tenga (iOS, Android, etc).

PhoneGap es un framework que brinda la posibilidad de desarrollar aplicaciones híbridas de manera gratuita utilizando tecnologías web muy simples tales como HTML, CSS y JavaScript. También podremos incluir IONIC en nuestros proyectos y compilar de la misma forma, obteniendo un resultado muy satisfactorio con un esfuerzo mínimo.

Además, dentro de las principales características de PhoneGap, nos encontramos con una serie de especificaciones para controlar los diferentes recursos del dispositivo, como por ejemplo:

  • Cámara
  • Acelerómetro
  • GPS
  • Notificaciones
  • Vibrador
  • Almacenamiento
  • Sistema de ficheros

Y por último queremos destacar que Adobe nos ofrece de forma gratuita el servicio online Phonegap Build. Se trata de un compilador al que podemos acceder vía web. Nos permite adjuntar un archivo a través de una cuenta en GitHub o un archivo .zip desde nuestro equipo:

Phonegap Build cuenta con varios tipos de planes, uno gratuito y otros de pago. En nuestro caso, podemos utilizar el servicio gratuito, ya que cubre perfectamente nuestras necesidades. La principal limitación que presenta es que sólo permite almacenar una aplicación privada, pero para nosotros será más que suficiente. También puedes encontrar más información aquí (en castellano) o en la página oficial (en inglés).

Desde mi punto de vista, PhoneGap Build nos proporciona una magnífica  herramienta para desarrollar aplicaciones multiplataformaya que nos brinda una manera de compilar nuestro código fuente de manera gratuita, sin necesidad de instalar nada en nuestros equipos. Incluso se puede utilizar desde cualquier ordenador por cualquier usuario, ya que no precisa disponer de software adicional ni permisos de administrador.

En resumen, PhoneGap nos brinda toda la funcionalidad del compilador, y no presenta ningún requisito. Sólo nos pide nuestros archivos HTML, CSS y JavaScript, y nos proporciona automáticamente el archivo APK con toda la funcionalidad nativa que necesitemos.

Cambios en el archivo index.html

En primer lugar, deberemos enlazar desde nuestro código HTML (index.html) el archivo JavaScript cordova.js (<script src="cordova.js"></script>) que nos permitirá acceder a la funcionalidad nativa desde nuestras propias funciones:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
  </body>
</html>

El archivo cordova.js se generará automáticamente por PhoneGap Build y se incluirá en nuestro proyecto. Nos permitirá acceder por ejemplo a recursos tales como la cámara, las notificaciones, etc., desde nuestras propias funciones JavaScript utilizando las especificaciones ampliamente documentadas de Cordova. En este punto convienen aclarar que PhoneGap es simplemente una distribución de Apache Cordova, y en la actualidad ambos nombres se utilizan para referirse al mismo framework.

Para poder apreciar la diferencia entre la ejecución en el navegador y la ejecución como app en el móvil, añadiremos por ejemplo la funcionalidad de vibración a nuestra calculadora. Además, para poder ajustar el nivel de vibración, vamos a añadir un control adicional:

<ion-list>
  ...
  <ion-item>
    <ion-range min="0" max="150" value="50" id="vibration">
      <ion-icon slot="start" name="trending-down"></ion-icon>
      <ion-icon slot="end" name="trending-up"></ion-icon>
    </ion-range>
  </ion-item>
</ion-list>

Podemos añadir dicho código por ejemplo justo después de los desplegables para ajustar el tamaño y tipo de los botones.

Utilizaremos el elemento <ion-range></ion-range> para especificar la duración de la vibración por cada pulsación de tecla. En el ejemplo proponemos utilizar un tiempo de vibración que varíe de 0 (min) a 150 (max) milisegundos, con un valor por defecto de 50 (value). Además añadiremos el icono trending-down al principio y trending-up al final del control, añadiendo los respectivos elementos <ion-icon></ion-icon>:

<ion-icon slot="start" name="trending-down"></ion-icon>
<ion-icon slot="end" name="trending-up"></ion-icon>

Se puede consultar la documentación oficial de IONIC para ampliar detalles sobre todas las opciones de configuración del elemento range, y los iconos disponibles.

Resumiendo, desde nuestro archivo principal index.html deberemos enlazar el fichero cordova.js, y añadir el control para el ajuste del tiempo de la vibración:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
    <ion-list>
      ...
      <ion-item>
        <ion-range min="0" max="150" value="50" id="vibration">
          <ion-icon slot="start" name="trending-down"></ion-icon>
          <ion-icon slot="end" name="trending-up"></ion-icon>
        </ion-range>
      </ion-item>
    </ion-list>  
  ...
  </body>
</html>

Cambios en el archivo calculator.js

En este ejercicio vamos a añadir una pequeña vibración cuando se pulse alguna tecla de la calculadora. Conseguiremos acceder a la vibración simplemente llamando a la función nativa desde nuestro código JavaScript. Por ejemplo, para provocar una vibración en nuestro dispositivo de 50 ms, bastaría con utilizar la siguiente línea de código:

navigator.vibrate(50);

Ahora deberemos añadir esa línea a nuestro código JavaScript. Además, para poder ajustar el valor de la vibración, en vez de poner un valor fijo, leeremos el valor seleccionado en el nuevo control implementado con el elemento <ion-range> e identificado con el id vibration. Por ejemplo, lo podemos hacer de la siguiente forma:

function setResult(value) {
    navigator.vibrate(document.getElementById("vibration").value);
    document.getElementById("result").innerHTML = value;
}

Dicho código provocará una vibración cada vez que se pulse cualquier tecla y se actualice el contenido de la pantalla de la calculadora.

Se puede encontrar la documentación completa relativa a la funcionalidad de vibración en la página correspondiente del plugin en Github.

El archivo config.xml

PhoneGap Build nos pide un archivo de configuración donde le podremos especificar por ejemplo el nombre de la aplicación, el icono a utilizar, o la funcionalidad nativa del móvil a la que nos gustaría acceder.

El contenido básico del archivo config.xml podría ser el siguiente:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.calculator"
    versionCode = "1"
    version     = "0.0.1" >

  <name>Calculator!</name>

  <description>
      A simple calculator.
  </description>

  <author href="https://fernandoruizrico.com" email="[email protected]">
      Fernando Ruiz Rico
  </author>

  <access origin="*" />
  <plugin name="cordova-plugin-whitelist" />
  <plugin name="cordova-plugin-vibration" />
</widget>

El elemento <widget> debe encontrarse en el nivel principal del archivo. Dentro de dicho elemento, podremos especificar el valor de los siguientes atributos:

  • id: El identificador único de nuestra aplicación. Se debe utilizar la notación de nombre de dominio inverso (por ejemplo, com.tuempresa.tuapp).
  • version: Código de versión de tres cifras (cambio mayor/cambio menor/corrección errores), como por ejemplo 0.0.1. Se incrementará la primera cifra cuando realicemos grandes cambios y mejoras. La segunda cuando hagamos pequeños cambios y correcciones de errores, y la tercera cuando simplemente se realice alguna corrección.
  • versionCode: Este atributo nos permite especificar el código de versión. Es opcional y sólo se utiliza cuando compilamos la aplicación para Android. Para más información puedes consultar la documentación oficial aquí.

Utilizando el elemento <name></name> podremos especificar el nombre de la aplicación que aparecerá debajo del icono. PhoneGap Build colocará de momento un icono por defecto, y ya más adelante (en otro ejercicio) proporcionaremos los detalles sobre cómo especificar y proporcionar el icono de la app.

El elemento <access /> nos permite especificar si damos permiso para acceder a archivos externos, como por ejemplo, para enlazar librerías directamente desde un servidor utilizando la conexión de Internet del móvil. Por ejemplo, nos permitirá acceder al servidor de IONIC desde nuestro archivo index.html, sin necesidad de instalar o descargar nada, tal como hemos hecho en la calculadora:

...
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>
...

De momento permitiremos acceso a cualquier servidor (<access origin="*" />), y más adelante veremos cómo incluir directamente en la app el código de IONIC para poder prescindir incluso de la conexión de Internet en aplicaciones que no requieran de la misma.

En las últimas versiones de Cordova, resulta necesario añadir el plugin  cordova-plugin-whitelist para que se puedan hacer efectivos los permisos especificados con la etiqueta anterior. Le indicaremos a PhoneGap Build que añada automáticamente el código del plugin mediante la siguiente línea:

<plugin name="cordova-plugin-whitelist" />

Además, utlizaremos el plugin cordova-plugin-vibration para hacer que el móvil vibre cada vez que se pulse una tecla. Para pedirle a PhoneGap Build que incluya en nuestra aplicación la funcionalidad de vibración, deberemos añadir la línea correspondiente al archivo config.xml:

<plugin name="cordova-plugin-vibration" />

Puedes encontrar aquí un resumen de la documentación original (en inglés) relativa al fichero config.xml, y aquí la documentación completa (también en inglés).

Sobre los dos plugins utilizados en este ejemplo, puedes obtener documentación adicional aquí: cordova-plugin-whitelist y cordova-plugin-vibration.

¿Cómo generar el archivo APK?

En este ejercicio seguiremos sin instalar nada en nuestros ordenadores. Para compilar nuestra aplicación y obtener el archivo APK, utilizaremos el servicio online gratuito PhoneGap Build, tal como hemos sugerido. Para ello, deberemos acceder a la página principal de dicho servicio y pulsar en la opción Sign in (en la esquina superior derecha).

Puesto que en los últimos meses han estado realizando algunos cambios en la página web de PhoneGap Build y en los servicios que ofrecen, a continuación indicamos un par de capturas antiguas. Si aparecieran de nuevo las opciones de pago, en nuestro caso deberemos escoger la opción gratuita (Free Plan):

Para poder compilar la aplicación deberemos autentificarnos utilizando usuario y contraseña. Para ello, se puede crear una cuenta directamente desde dicha página web, o también podemos acceder mediante una cuenta de Facebook o Google.

Una vez hayamos accedido, si hacemos clic en Apps -> private, veremos la siguiente pantalla:

Ahí podemos seleccionar si queremos generar la aplicación utilizando el código disponible desde un enlace de Github, o utilizando un archivo zip, que será nuestro caso. Para ello utilizaremos el botón Upload a .zip file.

Deberemos haber comprimido previamente todos los archivos de nuestra aplicación en un mismo fichero ZIP. Por ejemplo, para nuestro ejercicio de la calculadora con IONIC 4, el archivo comprimido debería contener tres ficheros: index.html, calculator.jsconfig.xml.

Si se ha subido correctamente el archivo, estaréis delante de la siguiente pantalla:

Bastará con hacer click en Ready to build, y esperar un poco, hasta que PhoneGap Build compile la aplicación y genere el archivo APK.

Si todo ha ido bien, podréis descargar e instalar directamente vuestra aplicación mediante un código QR, o haciendo click en el icono de Android:

Finalmente, conviene destacar que podemos repetir este proceso tantas veces como queramos. Podemos cambiar, subir, compilar e instalar de nuevo la aplicación utilizando el botón Update Code. Y todo eso de forma gratuita y sin instalar nada en nuestros equipos… no se puede pedir más.

En resumen…

El archivo index.html

Desde nuestro archivo principal index.html deberemos enlazar el fichero cordova.js, y añadir el control para el ajuste del tiempo de la vibración:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
    <ion-list>
      ...
      <ion-item>
        <ion-range min="0" max="150" value="50" id="vibration">
          <ion-icon slot="start" name="trending-down"></ion-icon>
          <ion-icon slot="end" name="trending-up"></ion-icon>
        </ion-range>
      </ion-item>
    </ion-list>  
  ...
  </body>
</html>

El archivo calculator.js

Desde nuestro propio archivo donde hayamos ubicado nuestras funciones de JavaScript (por ejemplo, calculator.js), deberemos recoger el valor seleccionado en el nuevo control, y llamar a la funcion vibrate para acceder a la funcionalidad nativa correspondiente:

...
function setResult(value) {
    navigator.vibrate(document.getElementById("vibration").value);
    document.getElementById("result").innerHTML = value;
}
...

El archivo config.xml

Para poder compilar la aplicación desde PhoneGap Build, debemos proporcionar un archivo de configuración config.xml, donde especificaremos el nombre de la aplicación y los plugins utilizados:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.calculator"
    versionCode = "1"
    version     = "0.0.1" >

  <name>Calculator!</name>

  <description>
      A simple calculator.
  </description>

  <author href="https://fernandoruizrico.com" email="[email protected]">
      Fernando Ruiz Rico
  </author>

  <access origin="*" />
  <plugin name="cordova-plugin-whitelist" />
  <plugin name="cordova-plugin-vibration" />
</widget>

El archivo .zip

A PhoneGap Build le deberemos proporcionar un fichero comprimido con formato ZIP que contenga los tres archivos especificados: index.html, calculator.js y config.xml.

El resultado

Calculadora con estilo personalizable utilizando IONIC 4

¿Cómo podemos utilizar IONIC 4?

Si deseamos utilizar IONIC 4, sólo necesitaremos enlazar los archivos JavaScript y CSS, tal como se especifica en la documentación y en github:

...
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>
...

De esta forma, quedarán definidos de forma automática los nuevos elementos, haciéndolos completamente accesibles al navegador para que nosotros sólo tengamos que escribir la etiqueta correspondiente en nuestro archivo HTML. A su vez además podremos interactuar con ellos utilizando código JavaScript.

Esta magnífica innovación que nos proporciona IONIC en su versión 4 simplifica muchísimo el acceso a sus archivos, ya que podemos utilizarlos sin necesidad de descargar ni instalar absolutamente nada.

Como se puede observar, estamos utilizamos la versión 4.9.1 de IONIC, que es la última que actualmente podemos encontrar en repositorio. Por otro lado, si deseáramos utilizar siempre la última versión, también podríamos acceder a dichos archivos utilizando la referencia «@ionic/core@latest». En tal caso, debemos tener en cuenta que con el paso del tiempo deberíamos comprobar si los últimos cambios pudieran afectar al código que escribimos inicialmente.

Más adelante describiremos el procedimiento para acceder a la funcionalidad de IONIC sin enlazar el código directamente desde Internet y así evitaremos que se descargue cada vez. Sin embargo, de momento podemos centrarnos en el código y el resultado, utilizando un simple editor de texto, sin necesidad de tener que instalar nada en nuestros equipos para poder utilizar IONIC.

Colocando los botones

En el ejemplo anterior hemos utilizado una tabla de HTML para distribuir los botones en filas y columnas. Sin embargo, las tablas se usan cada vez menos con este propósito. En su lugar se suele utilizar código CSS para ubicar o distribuir los diferentes elementos.

En este aspecto, IONIC nos proporciona una herramienta increíble conocida como grid, que tiene un comportamiento muy similar al de otras librerías de desarrollo web, como por ejemplo Bootstrap.

IONIC ha creado para nosotros tres nuevos elementos HTML que nos permiten establecer una cuadrícula muy fácilmente. Delimitamos los elementos que vamos a colocar mediante <ion-grid></ion-grid>  y luego establecemos las filas mediante <ion-row></ion-row> y las columnas con <ion-col></ion-col>.

Gracias a esto, podemos distribuir los botones de la calculadora utilizando el elemento grid de IONIC con un código casi idéntico al que utilizaríamos con una simple tabla de HTML, simplemente reemplazando las etiquetas de la siguiente forma:

  • <table></table> -> <ion-grid></ion-grid>
  • <tr></tr> -> <ion-row></ion-row>
  • <td></td> -> <ion-col></ion-col>

Y no sólo nos proporciona una forma sencilla de realizar dicha distribución, sino que nos aporta mucha más flexibilidad que las tablas de HTML, tal como se puede observar en la documentación oficial. Además, por defecto ocupará todo el ancho de la pantalla (el comportamiento esperado en cualquier dispositivo móvil) y cada fila podrá tener un número diferente de columnas, cuya anchura se distribuirá por defecto de manera uniforme.

Por ejemplo, para dibujar la siguiente tabla:

0
123+
456
789*
0/
AC.=

En el ejercicio anterior hemos utilizado el siguiente código HTML:

<table>
  <tr>
    <td colspan="4">0</td>
  </tr>
  <tr>
    <td>1</td>
    <td>2</td>
    <td>3</td>
    <td>+</td>
  </tr>
  <tr>
    <td>4</td>
    <td>5</td>
    <td>6</td>
    <td>-</td>
  </tr>
  <tr>
    <td>7</td>
    <td>8</td>
    <td>9</td>
    <td>*</td>
  </tr>
  <tr>
    <td colspan="3">0</td>
    <td>/</td>          
  </tr>
  <tr>
    <td colspan="2">AC</td>
    <td>.</td>
    <td>=</td>            
  </tr>
</table>

Si utilizamos los nuevos elementos que nos proporciona IONIC 4, obtendríamos el mismo resultado con el siguiente código:

<ion-grid>
  <ion-row>
    <ion-col>0</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>1</ion-col>
    <ion-col>2</ion-col>
    <ion-col>3</ion-col>
    <ion-col>+</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>4</ion-col>
    <ion-col>5</ion-col>
    <ion-col>6</ion-col>
    <ion-col>-</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>7</ion-col>
    <ion-col>8</ion-col>
    <ion-col>9</ion-col>
    <ion-col>*</ion-col>
  </ion-row>
  <ion-row>
    <ion-col size="9">0</ion-col>
    <ion-col>/</ion-col>
  </ion-row>
  <ion-row>
    <ion-col size="6">AC</ion-col>
    <ion-col>.</ion-col>
    <ion-col>=</ion-col>
  </ion-row>
</ion-grid>

Tenemos que destacar la forma que nos ofrece IONIC 4 para establecer el tamaño de una celda en concreto. Por ejemplo, en nuestro caso, para conseguir que la tecla del cero ocupe 3/4 de la pantalla, y que la tecla de borrado ocupe la mitad, ajustamos el valor del atributo size en el elemento ion-col:

...
<ion-row>
  <ion-col size="9">0</ion-col>
  <ion-col>/</ion-col>
</ion-row>
<ion-row>
  <ion-col size="6">AC</ion-col>
  <ion-col>.</ion-col>
  <ion-col>=</ion-col>
</ion-row>
...

IONIC establece por defecto que en la pantalla caben 12 columnas. Por lo tanto, al especificar <ion-col size="9">0</ion-col>, estamos diciéndole al navegador que esa celda ocupará 9 columnas de un total de 12, es decir, 3/4 de la pantalla. Y al especificar <ion-col size="6">AC</ion-col>, estamos diciéndole al navegador que esa celda ocupará 6 columnas de un total de 12, es decir, la mitad de la pantalla.

Observamos además que en la primera fila, donde mostramos el resultado, no hace falta especificar el tamaño, ya que por defecto, la celda se expande automáticamente para ocupar todo el ancho de la fila.

Veremos más adelante que el número de columnas por defecto se puede configurar cambiando el valor de la variable CSS --ion-grid-columns.

Los nuevos botones

Con IONIC 4 disponemos de un nuevo elemento <ion-button></ion-button>. Se utiliza de forma similar al <button></button> de HTML, pero como podemos leer en la documentación, ahora podemos cambiar el aspecto del botón mediante atributos específicos tales como expand, fill, size o color. Por ejemplo, podríamos definir el botón de borrado de la siguiente forma, sin necesidad de utilizar ningún otro código adicional:

<ion-button color="danger" expand="block" onclick="del()">AC</ion-button>

El navegador nos mostrará automáticamente un botón de color rojo que ocupará todo el ancho de la columna.

Como podemos observar, la sencillez y similitud respecto al código básico HTML son obvias. El mayor cambio en este nuevo elemento es que ahora no necesitamos ajustar ninguna propiedad CSS para obtener el aspecto deseado, tal como hacíamos antes:

.ac {
  color: red;
}
<button class="ac">AC</button>

Para cambiar el estilo

En este apartado comenzaremos a apreciar la verdadera magia de IONIC 4. Fijémos en la similitud entre este código:

<select>
    <option value="clear">Sin borde ni color</option>
    <option value="outline">Sólo borde</option>
    <option value="solid" selected>Con color de fondo</option>
</select>

y este otro:

<ion-select value="solid">
  <ion-select-option value="clear">Sin borde ni color</ion-select-option>
  <ion-select-option value="outline">Sólo borde</ion-select-option>
  <ion-select-option value="solid">Con color de fondo</ion-select-option>
</ion-select>

A simple vista podemos intuir que el resultado debería ser el mismo. Sin embargo, al pulsar sobre el segundo select, nos aparecerá un cuadro de diálogo con un diseño muy cuidado que además será diferente en móviles IOS y en Android. Además, por defecto dispondremos de un botón para aceptar y otro para cancelar.

En la documentación de IONIC podemos deleitarnos observando el resultado que llegaremos a obtener haciendo uso de este nuevo elemento. Encontraremos además en ese enlace todos los posibles atributos con sus posibles respectivos valores, que nos permitirán ajustar el comportamiento del mismo a nuestros gustos y necesidades.

Debemos matizar que para obtener una correcta visualización, deberemos incluir cada elemento dentro de una lista, tal como se observa en los ejemplos, y que utilizaremos el elemento <ion-label></ion-label> para establecer el texto descriptivo, que también disparará el evento onclick automáticamente al pulsar encima. Por ejemplo, en nuestro caso:

<ion-list>
  <ion-item>
    <ion-label>Tamaño botones</ion-label>
    <ion-select value="default" id="size">
      <ion-select-option value="small">Pequeños</ion-select-option>
      <ion-select-option value="default">Medianos</ion-select-option>
      <ion-select-option value="large">Grandes</ion-select-option>
    </ion-select>
  </ion-item>
  <ion-item>
    <ion-label>Tipo botones</ion-label>
    <ion-select value="solid" id="type">
      <ion-select-option value="clear">Sin borde ni color</ion-select-option>
      <ion-select-option value="outline">Sólo borde</ion-select-option>
      <ion-select-option value="solid">Con color de fondo</ion-select-option>
    </ion-select>
  </ion-item>
</ion-list>

¿Estilo IOS o Android?

La verdad es que resulta difícil contestar a esa pregunta, y por eso los desarrolladores de IONIC nos permiten elegir el aspecto que van a tener los diferentes elementos de nuestros formularios. Para ello han añadido el atributo mode a muchos elementos, de forma que podamos especificar los valores «ios» o «md».

En caso de que queramos cambiar el estilo de todos los elementos de la calculadora al mismo tiempo, IONIC también nos permite hacerlo modificando la url, tal como se especifica en la documentación:

  • index.html?ionic:mode=md
  • index.html?ionic:mode=ios

Para mejorar el diseño de la calculadora, añadiremos a la misma un encabezado (elemento <ion-header></ion-header>) que contendrá un título y dos botones con un atributo href que apuntarán a esas dos urls. Además, observaremos al probar la nueva calculadora que el propio estilo del encabezado que contiene esos dos botones también se actualizará:

<ion-header>
  <ion-toolbar>
    <ion-title>Calculator</ion-title>
    <ion-buttons slot="secondary">
      <ion-button href="index.html?ionic:mode=md">Android</ion-button>
    </ion-buttons>
    <ion-buttons slot="primary">
      <ion-button href="index.html?ionic:mode=ios">IOS</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

Cuando iniciemos la aplicación, ésta se mostrará automáticamente utilizando el estilo de la plataforma en la que se esté ejecutando.

¿Código JavaScript adicional?

Pues la verdad es que muy poco:

function onLoad() {
    document.addEventListener("ionChange", setStyle);
    setStyle();
}

function setStyle() {
    document.querySelectorAll("ion-content ion-button").forEach(function(b) {
        b.expand = "block";
        b.strong = "true";
        b.fill = document.getElementById("type").value;
        b.size = document.getElementById("size").value;
    });
}

Los elementos <ion-select></ion-select> generarán un evento ionChange automáticamente cada vez que cambie el valor seleccionado. Lo único que tenemos que hacer es estar atentos y volver a ajustar el diseño de la calculadora cuando se dispare dicho evento. Para que se ejecute una acción automáticamente al detectar cualquier cambio, utilizamos la siguiente línea:

document.addEventListener("ionChange", setStyle);

Una vez se ejecute la función setStyle(), lo único que tendremos que hacer es recorrer todos los botones y actualizar las propiedades que queramos. En nuestro caso podemos cambiar las siguientes propiedades:

  • expand: «block»
  • strong: «true»
  • fill: «clear», «outline» o «solid»
  • size: «small», «default» o «large»

Podemos volver a consultar la respectiva documentación si deseamos realizar cualquier modificación en el código.

En resumen…

El fichero index.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="calculator.js"></script>

  <title>Calculator!</title>
</head>

<body onload="onLoad()">
  <ion-app>
    <ion-header>
      <ion-toolbar>
        <ion-title>Calculator</ion-title>
        <ion-buttons slot="secondary">
          <ion-button href="index.html?ionic:mode=md">Android</ion-button>
        </ion-buttons>
        <ion-buttons slot="primary">
          <ion-button href="index.html?ionic:mode=ios">IOS</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <ion-list>
        <ion-item>
          <ion-label>Tamaño botones</ion-label>
          <ion-select value="default" id="size">
            <ion-select-option value="small">Pequeños</ion-select-option>
            <ion-select-option value="default">Medianos</ion-select-option>
            <ion-select-option value="large">Grandes</ion-select-option>
          </ion-select>
        </ion-item>
        <ion-item>
          <ion-label>Tipo botones</ion-label>
          <ion-select value="solid" id="type">
            <ion-select-option value="clear">Sin borde ni color</ion-select-option>
            <ion-select-option value="outline">Sólo borde</ion-select-option>
            <ion-select-option value="solid">Con color de fondo</ion-select-option>
          </ion-select>
        </ion-item>
      </ion-list>
      <ion-grid>
        <ion-row>
          <ion-col><ion-button color="dark"><span id="result">0</span></ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('1')">1</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('2')">2</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('3')">3</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('+')">+</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('4')">4</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('5')">5</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('6')">6</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('-')">-</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('7')">7</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('8')">8</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('9')">9</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('*')">*</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="9"><ion-button color="primary" onclick="add('0')">0</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('/')">/</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="6"><ion-button color="danger" onclick="del()">AC</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('.')">.</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="calc()">=</ion-button></ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>
  </ion-app>
</body>
</html>

El fichero calculator.js

function onLoad() {
    document.addEventListener("ionChange", setStyle);
    setStyle();
}

function setStyle() {
    document.querySelectorAll("ion-content ion-button").forEach(function(b) {
        b.expand = "block";
        b.strong = "true";
        b.fill = document.getElementById("type").value;
        b.size = document.getElementById("size").value;
    });
}

function setResult(value) {
    document.getElementById("result").innerHTML = value;
}

function getResult() {
    return(document.getElementById("result").innerHTML);
}

function add(key) {
    var result = getResult();
    if (result!="0" || isNaN(key)) setResult(result + key);
    else setResult(key);
}

function calc() {
    var result = eval(getResult());
    setResult(result);
}

function del() {
    setResult(0);
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la calculadora y probar la funcionalidad resultante utilizando el código especificado.

Calculadora básica con HTML+CSS+Javascript

Esqueleto HTML

La estructura básica de nuestro fichero principal será la siguiente:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <link href="calculator.css" rel="stylesheet">
    <script src="calculator.js"></script>
    <title>Calculator!</title>
  </head>
  <body>

  ...

  </body>
</html>

HTML5 doctype

La primera línea que aparece en el archivo ( <!doctype html> ) no es tanto una etiqueta HTML, sino una declaración del lenguaje y la versión que vamos a utilizar (HTML 5). Además, como no se trata de una etiqueta, no necesitamos cerrar la declaración:

<!doctype html>
<html lang="en">
  ...
</html>

Etiqueta para obtener una visualización responsive

El viewport es el área de una página web que visualiza un usuario. Puede variar dependiendo del dispositivo, y será menor por ejemplo en un teléfono móvil que en la pantalla de un ordenador.

Antes de que aparecieran las tabletas y los teléfonos móviles, las páginas web se diseñaban sólo para pantallas de ordenadores, y era muy común que tuvieran un diseño estático y un tamaño fijo.

Cuando comenzamos a utilizar los navegadores de los dispositivos móviles, las páginas de tamaño fijo eran demasiado grandes para ajustarse al viewport. Para solucionar esto, los navegadores modernos reducían el tamaño de todos los elementos de la página web para ajustarse a la pantalla, con lo que resultaba muy difícil leer el contenido de la misma.

Un elemento  <meta> de tipo viewport le dice al navegador cómo controlar las dimensiones de la página para hacer zoom automáticamente de forma adecuada, utilizando por ejemplo un mayor tamaño de letra y habilitando a su vez scroll vertical para poder acceder a todo el contenido.

En resumen, para asegurar una correcta visualización, tanto en dispositivos móviles como en ordenadores de escritorio, debemos añadir una etiqueta <meta name="viewport"> para especificar el tamaño y la escala del contenido de la pantalla:

<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

Más concretamente, con la cadena especificada dentro del atributo content del ejemplo, estaríamos diciéndole al WebView (el navegador interno del dispositivo) que aproveche al máximo el área visible de la pantalla. Y puesto que la visualización debería ser óptima, el usuario no necesitará realizar zoom (user-scalable=no).

Puedes encontrar más información aquí. Además, también puedes consultar algunas recomendaciones al respecto de los desarrolladores de Ionic, o de Safari o de Firefox. Además, debido a la constante actualización en el mercado de dispositivos móviles puede resultar necesario realizar algún ajuste, como por ejemplo, para obtener una mejor visualización en el iPhone X. También es interesante observar la cadena de configuración utilizada por  algunas librerías, como por ejemplo Bootstrap.

Colocando los botones en una tabla

Para distribuir los botones de la calculadora en varias filas y columnas utilizaremos una tabla. Por ejemplo, para obtener la siguiente distribución:

0
123+
456
789*
0/
AC.=

Podemos utilizar el siguiente código:

<table>
  <tr>
    <td colspan="4">0</td>
  </tr>
  <tr>
    <td>1</td>
    <td>2</td>
    <td>3</td>
    <td>+</td>
  </tr>
  <tr>
    <td>4</td>
    <td>5</td>
    <td>6</td>
    <td>-</td>
  </tr>
  <tr>
    <td>7</td>
    <td>8</td>
    <td>9</td>
    <td>*</td>
  </tr>
  <tr>
    <td colspan="3">0</td>
    <td>/</td>          
  </tr>
  <tr>
    <td colspan="2">AC</td>
    <td>.</td>
    <td>=</td>            
  </tr>
</table>

Como se puede observar, se utiliza el elemento <tr></tr> (table row) para establecer las filas de la tabla, y el elemento <td></td> (table data) para delimitar las celdas de cada fila.

Añadimos el atributo colspan="2" y colspan="3" para hacer que una determinada celda abarque 2 y 3 columnas respectivamente, y por lo tanto, el botón que contiene sea más grande que el resto.

Botones básicos para capturar las pulsaciones

Utilizaremos el elemento <button></button> para implementar las teclas de la calculadora, y los colocaremos dentro de cada celda de la tabla. Por ejemplo:

<tr>
  <td><button onclick="...">1</button></td>
  <td><button onclick="...">2</button></td>
  <td><button onclick="...">3</button></td>
  <td><button onclick="...">+</button></td>
</tr>

El atributo onclick nos servirá para ejecutar una función de JavaScript cada vez que el usuario pulse ese botón. Lo veremos con detalle un poco más adelante.

Utilizando archivos CSS y JavaScript externos

Para poder cambiar el aspecto de la calculadora, utilizaremos un fichero externo que contendrá algunas líneas de código CSS en un fichero externo, que incluiremos en el archivo principal (index.html).

Además, también utilizaremos diversas funciones y código JavaScript que también ubicaremos en un archivo externo.

Para incluir ambos archivos en el código HTML principal y poder utilizar el código que hayamos colocado allí, utilizaremos las siguientes etiquetas:

<link href="calculator.css" rel="stylesheet">
<script src="calculator.js"></script>

Modificando el aspecto de los botones

Para conseguir que los botones sean un poco más grandes y se puedan pulsar más fácilmente, añadiremos algunas propiedades CSS al archivo calculator.css:

table {
  width: 100%;
}

button {
  width: 100%;
  font-size: 150%;
}

.ac {
  color: red;
}

Mediante la propiedad width: 100%; haremos que la tabla ocupe todo el ancho de la pantalla, y que los botones ocupen todo el ancho de cada celda de la tabla.

Con la modificación font-size: 150%; incrementaremos el tamaño del texto que pongamos dentro de los botones.

Mediante  la definición de una clase, conseguiremos aplicar un estilo concreto (color del texto en rojo) al botón AC (All Clear):

.ac {
  color: red;
}
<button class="ac">AC</button>

Añadiendo la funcionalidad para realizar los cálculos

El código HTML ya incluye todos los elementos básicos para interactuar con el usuario, pero todavía no disponemos del código JavaScript necesario para capturar las pulsaciones de cada tecla, actualizar la pantalla de la calculadora y calcular el resultado.

Proponemos utilizar cinco funciones, cuyo propósito justificamos debajo del cuadro:

function setResult(value) {
    document.getElementById('result').innerHTML = value;
}

function getResult() {
    return(document.getElementById('result').innerHTML);
}

function add(key) { 
    var result = getResult();
    if (result!='0' || isNaN(key)) setResult(result + key);
    else setResult(key);
}

function calc() {
    var result = eval(getResult()); 
    setResult(result);
}

function del() { 
    setResult(0);
}
  • setResult(): Actualiza la pantalla de la calculadora poniendo el valor que se pase como parámetro.
  • getResult(): Recoge el valor del último resultado obtenido, o de la expresión matemática que se debe calcular, y que se está visualizando en la pantalla de la calculadora.
  • add(): Añade a la pantalla la tecla pulsada (por ejemplo, el dígito o la operación a realizar). Si la pantalla ya contiene algún dato o la tecla que se pulsa no es un dígito, el carácter de la tecla pulsada se añadirá a la pantalla. En caso contrario (por ejemplo, si la pantalla está a cero, y se pulsa otro dígito), el contenido de la pantalla se reemplazará con la tecla pulsada.
  • calc(): Realiza el cálculo de la expresión que se encuentre en la pantalla (utilizando la función eval()), y escribe el resultado.
  • del(): Pone a cero el contenido de la pantalla de la calculadora.

Las tres últimas funciones de la lista se ejecutarán desde el código HTML utilizando el atributo onclick:

<button onclick="add('1')">1</button>
...
<button onclick="calc()">=</button>
...
<button onclick="del()">AC</button>

En resumen…

El fichero index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <link href="calculator.css" rel="stylesheet">
    <script src="calculator.js"></script>    

    <title>Calculator!</title>
  </head>
  <body>
    <table>
      <tr>
        <td colspan="4"><button id="result">0</button></td>
      </tr>
      <tr>
        <td><button onclick="add('1')">1</button></td>
        <td><button onclick="add('2')">2</button></td>
        <td><button onclick="add('3')">3</button></td>
        <td><button onclick="add('+')">+</button></td>
      </tr>
      <tr>
        <td><button onclick="add('4')">4</button></td>
        <td><button onclick="add('5')">5</button></td>
        <td><button onclick="add('6')">6</button></td>
        <td><button onclick="add('-')">-</button></td>
      </tr>
      <tr>
        <td><button onclick="add('7')">7</button></td>
        <td><button onclick="add('8')">8</button></td>
        <td><button onclick="add('9')">9</button></td>
        <td><button onclick="add('*')">*</button></td>
      </tr>
      <tr>
        <td colspan="3"><button onclick="add('0')">0</button></td>
        <td colspan="3"><button onclick="add('/')">/</button></td>          
      </tr>
      <tr>
        <td colspan="2"><button onclick="del()" class="ac">AC</button></td>
        <td><button onclick="add('.')">.</button></td>
        <td><button onclick="calc()">=</button></td>            
      </tr>
    </table>
  </body>
</html>

El fichero calculator.css

table {
    width: 100%;
}

button {
    width: 100%;
    font-size: 150%;
}

.ac {
    color: red;
}

El fichero calculator.js

function setResult(value) {
    document.getElementById('result').innerHTML = value;
}

function getResult() {
    return(document.getElementById('result').innerHTML);
}

function add(key) { 
    var result = getResult();
    if (result!='0' || isNaN(key)) setResult(result + key);
    else setResult(key);
}

function calc() {
    var result = eval(getResult()); 
    setResult(result);
}

function del() { 
    setResult(0);
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la calculadora y probar la funcionalidad resultante utilizando el código especificado.

IONIC 4 ya está aquí

¿Por qué IONIC?

Ya desde la primera versión vinieron pisando fuerte, y apostaron por utilizar Angular, que ya había demostrado ser un framework muy recomendable, ya que proporcionaba una estabilidad, modularidad y escalabilidad únicas en el código generado. Gracias a ese alto potencial, los creadores de IONIC predijeron que esa fusión permitiría el desarrollo de aplicaciones híbridas que podrían llegar a competir incluso con las nativas (https://ionicframework.com/present-ionic/slides).

Angular hace posible que podamos desarrollar aplicaciones complejas con tecnología web. Esto se consigue gracias a que permite al programador crear nuevas etiquetas HTML, y componentes específicos incrustados en una arquitectura vista-controlador, muy adecuada en el desarrollo de aplicaciones para dispositivos móviles.

Además, desde un principio se ha mantenido la filosofía de no obligar al programador a adquirir conocimientos complejos ni específicos de cada plataforma, ni a tener que desarrollar un código diferente para acceder al hardware de los distintos dispositivos móviles:

  • Por un lado, se utiliza Angular para aprovechar las habilidades de muchos desarrolladores web que ya conocen los conceptos de componentes, directivas y servicios, que además son completamente compatibles con cualquier navegador, y permiten por lo tanto utilizar un sólo código fuente compatible con dispositivos muy diferentes.
  • Por otro lado, se utiliza Cordova para acceder al hardware de los dispositivos, permitiendo interactuar con el mismo utilizando código Javascript.

IONIC destacó principalmente porque aportaba una interfaz de usuario muy atractiva que combinada con Angular y Cordova, proporcionaba al programador un entorno único en su época para el desarrollo de aplicaciones híbridas:

Fuimos muchos los que descubrimos y utilizamos esas primeras versiones para desarrollar aplicaciones (https://showcase.ionicframework.com/apps/archive), y prueba de ello fue que la Play Store y la App Store comenzaron a recibir una gran cantidad de aplicaciones híbridas, que además era rápidas y estables, unas características que hace años sólo podían encontrarse en aplicaciones nativas.

Sin embargo, esas primeras versiones obligaban a los desarrolladores de IONIC a ir adaptando su código a medida que Angular iba evolucionando. Aunque eso ya ha cambiado…

¿Por qué IONIC 4?

Después de dos años de trabajo desde que nos deleitaran con la versión 3, el pasado 23 de enero, los desarrolladores de IONIC pusieron a nuestra disposición la versión 4, por lo que somos muy afortunados de poder disfrutar actualmente de todo su potencial. Además, ya podemos consultar la documentación actualizada (https://ionicframework.com/docs).

Los cambios respecto a la versión 3 son muy significativos (https://blog.ionicframework.com/introducing-ionic-4-ionic-for-everyone) y desde luego no dejan lugar a dudas para decantarnos por esta última versión de IONIC para desarrollar nuestros proyectos web, o aplicación móviles híbridas.

El objetivo original de IONIC era desarrollar una librería compatible con cualquier tecnología web, y que pudiera incluirse en cualquier proyecto independientemente de la librería o framework que ya estuviera utilizando el programador. Y esto no era posible, hasta ahora…

En la práctica, los últimos cambios hechos en la arquitectura de la librería, permiten que los componentes de IONIC se puedan utilizar con la misma simpleza que cualquier otra etiqueta HTML, pero nos proporcionan una funcionalidad ampliada. Por ejemplo, podríamos hacer uso de la etiqueta  <ion-button></ion-button> en cualquier framework, e incluso de manera independiente (sin ninguna arquitectura o código base específico), utilizando la sencilla sintaxis de Javascript. En esta última versión, el navegador puede reconocer los nuevos elementos HTML sin realizar ningún tipo de modificación en nuestro código.

Ya que tenemos el lujo de poder utilizar IONIC 4, no vamos a dejar escapar la posibilidad de disfrutar de la sencillez, facilidad de desarrollo y versatilidad que nos proporciona esta última versión, tal como apreciaremos en los próximos ejercicios.

Una aplicación para probar las librerías UIKit y AVFoundation con iOS

BaseViewController.swift

//
//  BaseViewController.swift
//  UIKit component handling
//

import UIKit

class BaseViewController: UIViewController {
    let componentName:String
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
    }
    
    init(_ componentName: String) {
        self.componentName = componentName
        super.init(nibName: nil, bundle: nil)
        self.navigationItem.prompt = componentName
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func appDelegate() -> AppDelegate? {
        guard let app = UIApplication.shared.delegate as? AppDelegate else { return nil }
        return app
    }
}

RootViewController.swift

//
//  ViewController.swift
//  UIKit component handling
//

import UIKit

class RootViewController: UITableViewController, UISearchBarDelegate {
    @IBOutlet var searchBar: UISearchBar!
    
    let componentList : [BaseViewController] = [
        LabelVC("UILabel"),
        ImageViewVC("UIImageView"),
        ToolBarVC("UIToolBar"),
        FirstNavigationBarVC("Navigation Bar"),
        PlayMusicalNotesVC("AVAudioPlayer"),
        WebServiceSchoolsVC("JSON web service"),
        ScrollViewVC("UIScrollView")
        /* ... */
    ]
    
    var filteredData : [BaseViewController]! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.filteredData = self.componentList
        
        self.searchBar.delegate = self
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.filteredData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
        
        cell.textLabel?.text = self.filteredData[indexPath.row].componentName
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        self.navigationController?.pushViewController(self.filteredData[indexPath.row], animated: true)
    }

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        self.filteredData = searchText.isEmpty ? self.componentList : self.componentList.filter({$0.componentName.contains(searchText)})
        
        tableView.reloadData()
    }
}

AppDelegate.swift

//
//  AppDelegate.swift
//  UIKit component handling
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var effectView: UIView!
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }
    
    func applicationWillResignActive(_ application: UIApplication) {
        makeEffectView()
    }
    
    func applicationDidEnterBackground(_ application: UIApplication) {
    }
    
    func applicationWillEnterForeground(_ application: UIApplication) {
    }
    
    func applicationDidBecomeActive(_ application: UIApplication) {
        removeEffectView()
    }
    
    func applicationWillTerminate(_ application: UIApplication) {
    }
}

extension AppDelegate {
    
    private func makeEffectView() {
        effectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffect.Style.light))
        effectView.frame = CGRect(x:0, y:0, width:UIScreen.main.bounds.size.width, height:UIScreen.main.bounds.size.height)
        self.window?.addSubview(effectView)
    }
    
    private func removeEffectView() {
        if effectView != nil {
            self.effectView.removeFromSuperview()
        }
    }
    
    // Add UIAlertController on UIViewController
    func showMessage(vc: UIViewController, title: String, message: String, actionTitle: String, actionStyle: UIAlertAction.Style) {
        // Create a UIAlertController.
        let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        
        // Add an Action of OK.
        alert.addAction(UIAlertAction(title: actionTitle, style: actionStyle, handler: { action in print("Action OK!") }))
        
        // Activate UIAlert.
        vc.present(alert, animated: true, completion: nil)
    }
}

LabelVC.swift

//
//  LabelVC.swift
//  UIKit component handling
//

import UIKit

class LabelVC: BaseViewController {

    lazy var label: UILabel = {
        // Define the size of the label.
        let width: CGFloat = 300
        let height: CGFloat = 100
        
        // Define coordinates to be placed.
        // (center of screen)
        let posX: CGFloat = self.view.bounds.width/2 - width/2
        let posY: CGFloat = self.view.bounds.height/2 - height/2
        
        // Label Create.
        let label: UILabel = UILabel(frame: CGRect(x: posX, y: posY, width: width, height: height))
        
        // Define background color.
        label.backgroundColor = .orange
        
        // Define text color.
        label.textColor = .white
        
        // Define text font.
        label.font = .systemFont(ofSize: 20, weight: .regular)
        
        // Define text of label.
        label.text = "Hello! :)"
        
        // Define count of line.
        // '0' is infinity
        label.numberOfLines = 0
        
        // Round UILabel frame.
        label.layer.masksToBounds = true
        
        // Radius of rounded corner.
        label.layer.cornerRadius = 20.0
        
        // Define shadow color.
        label.shadowColor = .gray
        
        // Define text Alignment.
        // options: .left, .right, .center, .justified, .natural
        label.textAlignment = .center
        
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add UILabel to view.
        self.view.addSubview(self.label)
    }
}

ImageViewVC.swift

//
//  ImageViewVC.swift
//  UIKit component handling
//

import UIKit

class ImageViewVC: BaseViewController {
    
    lazy var imageView: UIImageView = {
        // Set the size of UIImageView.
        let width: CGFloat = self.view.bounds.width/2
        let height: CGFloat = 150
        
        // Set x, y of UIImageView.
        let posX: CGFloat = (self.view.bounds.width - width)/2
        let posY: CGFloat = (self.view.bounds.height - height)/2
        
        // Create UIImageView.
        let imageView = UIImageView(frame: CGRect(x: posX, y: posY, width: width, height: height))
        
        // Create UIImage.
        let myImage = UIImage(named: "apple.png")!
        
        // Set the image to UIImageView.
        imageView.image = myImage
        
        return imageView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add UIImageView to view
        self.view.addSubview(self.imageView)
    }
}

ToolBarVC.swift

//
//  ToolBarVC.swift
//  UIKit component handling
//

import UIKit

class ToolBarVC: BaseViewController {

    private let toolBarHeight: CGFloat = 44
    
    lazy var toolBar: UIToolbar = {
        // Decide the size of the toolbar.
        let tb = UIToolbar(frame: CGRect(x: 0, y: self.view.bounds.size.height - toolBarHeight, width: self.view.bounds.size.width, height: 40.0))
        
        // Determine the position of the toolbar.
        tb.layer.position = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height-20.0)
        
        // Decide the color of the toolbar.
        tb.barStyle = .blackTranslucent
        tb.tintColor = .white
        tb.backgroundColor = .black
        
        return tb
    }()
    
    lazy var barButtonGreen: UIBarButtonItem = {
        // Generate button 1.
        return UIBarButtonItem(title: "Green", style:.plain, target: self, action: #selector(onClickBarButton))
    }()
    
    lazy var barButtonBlue: UIBarButtonItem = {
        // Generate button 2.
        return UIBarButtonItem(title: "Blue", style:.plain, target: self, action: #selector(onClickBarButton))
    }()
    
    lazy var barButtonRed: UIBarButtonItem = {
        // Generate button 3.
        return UIBarButtonItem(title: "Red", style:.plain, target: self, action: #selector(onClickBarButton))
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Place the button in the tool bar.
        self.toolBar.items = [self.barButtonGreen, self.barButtonBlue, self.barButtonRed]
        
        // Add UIToolBar on view
        self.view.addSubview(self.toolBar)
    }
    
    // Called when UIBarButtonItem is pressed.
    @objc private func onClickBarButton(sender: UIBarButtonItem) {
        switch sender.title {
            case "Green": self.view.backgroundColor = .green
            case "Blue": self.view.backgroundColor = .blue
            case "Red": self.view.backgroundColor = .red
            default: return
        }
    }
}

Navigation Bar

FirstNavigationBarVC.swift

//
//  FirstNavigationBarVC.swift
//  UIKit component handling
//

import UIKit

class FirstNavigationBarVC: BaseViewController {
    
    lazy var button: UIButton = {
        // Create Button
        let b = UIButton(frame: CGRect(x: 0, y: 0, width: 250, height: 50))
        b.backgroundColor = .orange
        b.layer.masksToBounds = true
        b.setTitle("Go to second view", for: .normal)
        b.layer.cornerRadius = 20.0
        b.layer.position = CGPoint(x: self.view.bounds.width/2, y:200)
        b.addTarget(self, action: #selector(goToNextView), for: .touchDown)
        
        return b
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Define the background color of View.
        self.view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        // Add button on view
        self.view.addSubview(self.button)
        
        // Create a BarButtonItem.
        let barButton_1 = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(goToPreviousView))
        
        let barButton_2 = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(goToNextView))
        let barButton_3 = UIBarButtonItem(barButtonSystemItem: .fastForward, target: self, action: #selector(goToNextView))
        
        // Set the title.
        self.navigationItem.title = "First View"

        // Place it on the left side of Bar.
        self.navigationItem.setLeftBarButton(barButton_1, animated: true)
        
        // Place multiple on the right side of Bar.
        self.navigationItem.setRightBarButtonItems([barButton_2, barButton_3], animated: true)
    }
    
    // Called when the left bar button is pressed
    @objc private func goToPreviousView(sender: UIBarButtonItem) {
        self.navigationController?.popViewController(animated: true)
    }
    
    // Called when the right bar buttons and the button on the screen are pressed
    @objc private func goToNextView(sender: UIButton) {
        let secondViewController = SecondNavigationBarVC("Second view")
        self.navigationController?.pushViewController(secondViewController, animated: true)
    }
}

SecondNavigationBarVC.swift

//
//  SecondNavigationBarVC.swift
//  UIKit component handling
//

import UIKit

class SecondNavigationBarVC: BaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Define the background color of View.
        self.view.backgroundColor = UIColor.green
    }
}

PlayMusicalNotesVC.swift

//
//  PlaySoundVC.swift
//  UIKit component handling
//

import UIKit
import AVFoundation

class PlayMusicalNotesVC: BaseViewController {
    
    var audioPlayer : AVAudioPlayer!
    
    let sounds = ["do", "re", "mi", "fa", "sol", "la", "si", "do2"]
    
    lazy var stackView: UIStackView = {

        let stackView = UIStackView()
        
        stackView.frame = CGRect(x: 0, y: 98, width: self.view.bounds.width, height: self.view.bounds.height - 98)
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        
        for sound in sounds {
            let button = UIButton()
            
            // Set the background color of the button.
            button.backgroundColor = .white
            button.layer.borderWidth = 1
            button.layer.borderColor = UIColor.black.cgColor
            
            // Set the title (normal).
            button.setTitle(sound, for: .normal)
            button.setTitleColor(.black, for: .normal)
            
            // Set the title (highlighted).
            button.setTitle(sound, for: .highlighted)
            button.setTitleColor(.red, for: .highlighted)
            
            // Add an event.
            button.addTarget(self, action: #selector(onClick), for: .touchDown)
            
            stackView.addArrangedSubview(button)
        }
            
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add UIStackView to view.
        self.view.addSubview(self.stackView)
    }
    
    // Button event.
    @objc private func onClick(_ sender: UIButton) {

        if let soundURL : URL = Bundle.main.url(forResource: sender.currentTitle, withExtension: "mp3") {
            print(soundURL)
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
                audioPlayer.play()
            } catch {
                print(error)
            }
        }
    }
}

WebServiceSchoolsVC

//
//  WebServiceSchoolsVC.swift
//  UIKit component handling
//

import UIKit

class Ciclo : Codable {
    var denominacion:String = ""
    var denominacion_val:String = ""
}

class WebServiceSchoolsVC: BaseViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
    
    // Define the array to use in the Table.
    var ciclos: [Ciclo] = []
    var ciclosFiltrados: [Ciclo] = []
    
    // Define the url to download the data
    let url = URL(string: "https://fernandoruizrico.com:8001/api/getListadoCiclos")
    
    lazy var searchBar: UISearchBar = {
        // Create a search bar.
        let searchBar = UISearchBar()
        searchBar.delegate = self
        searchBar.frame = CGRect(x: 0, y: 98, width: self.view.bounds.width, height: 100)
        
        // Add a shadow.
        searchBar.layer.shadowColor = UIColor.black.cgColor
        searchBar.layer.shadowOpacity = 0.5
        searchBar.layer.masksToBounds = false
        
        // Disable the bookmark button.
        searchBar.showsBookmarkButton = false
        
        // Set bar style to Default.
        searchBar.searchBarStyle = .default
        
        // Set descriptive text.
        searchBar.placeholder = "Buscar"
        
        // Set the color of the cursor and cancel button.
        searchBar.tintColor = .red
        
        // The search result display button is not displayed.
        searchBar.showsSearchResultsButton = false
        
        return searchBar
    }()
    
    lazy var tableView: UITableView = {

        let tableView = UITableView()
        
        tableView.frame = CGRect(x: 0, y: 200, width: self.view.bounds.width, height: self.view.bounds.height)
        
        // Register the Cell name.
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        
        // Set the DataSource.
        tableView.dataSource = self
        
        // Set Delegate.
        tableView.delegate = self
        
        return tableView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        URLSession.shared.dataTask(with: url!) { (data, response, error) in
            do {
                // Decode data
                self.ciclos = try JSONDecoder().decode([Ciclo].self, from: data!)
                self.ciclosFiltrados = self.ciclos
                
                DispatchQueue.main.async {
                    // Add UITableView on view
                    self.view.addSubview(self.searchBar)
                    self.view.addSubview(self.tableView)
                }
            } catch let error {
                print("Error: \(error)")
            }
        }.resume()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return ciclosFiltrados.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        cell.textLabel?.text = self.ciclosFiltrados[indexPath.row].denominacion
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        // Create a UIAlertController.
        let alert: UIAlertController = UIAlertController(title: "Ciclo", message: self.ciclosFiltrados[indexPath.row].denominacion+"\n"+self.ciclosFiltrados[indexPath.row].denominacion_val, preferredStyle: .alert)
        
        // Add an Action of OK.
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        
        present(alert, animated: true, completion: nil)
    }
    
    // Called whenever text is changed
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        self.ciclosFiltrados = searchText.isEmpty ? self.ciclos : self.ciclos.filter({$0.denominacion.contains(searchText)})
        self.tableView.reloadData()
    }
}

ScrollViewVC.swift

//
//  ScrollViewVC.swift
//  UIKit component handling
//

import UIKit

class ScrollViewVC: BaseViewController {
    
    lazy var scrollView: UIScrollView = {
        let scrollView: UIScrollView = UIScrollView()
        
        // Generate ScrollView.
        scrollView.frame = self.view.frame
        
        // Disable ScrollView bounces
        scrollView.bounces = false
        
        // Set the image in UIImage.
        let image = UIImage(named: "vertical.jpeg")!
        
        // Create a UIImageView.
        let imageView = UIImageView()
        
        // Set myImage to the image of imageView.
        imageView.image = image
        
        // Set the value of frame size
        imageView.frame.size = image.size
        
        // Set the aspect ratio of the image.
        imageView.contentMode = .scaleAspectFill
        
        // Add imageView to ScrollView.
        scrollView.addSubview(imageView)
        
        // Set contentSize to ScrollView.
        scrollView.contentSize = imageView.frame.size
        
        return scrollView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add UIScrollView on view
        self.view.addSubview(self.scrollView)
    }
}

Swift – Ejercicios propuestos (V)

Diseñar un encriptador en modo consola utilizando una clase que tenga una propiedad para guardar la equivalencia entre las letras sin encriptar y las letras encriptadas. Para ello se deberá utilizar un array de estructuras. Además, dicha clase deberá contener una función para encriptar y otra para desencriptar. Se deberá diseñar una clase para probarlo. Las letras a encriptar y su equivalente se podrán añadir directamente desde código, y la cadena de texto a encriptar se deberá pedir por teclado, mostrando el resultado por pantalla.

...

Diseñar un encriptador en modo gráfico utilizando una clase que permita añadir las letras a encriptar y su equivalente, guardando dicho listado de manera persistente. Además, la aplicación deberá tener dos pantallas, de forma que en la primera se puedan encriptar o desencriptar expresiones según la equivalencia que se encuentre guardada en la aplicación. En la segunda pantalla se deberá poder modificar el listado de las letras que se deben encriptar, permitiendo añadir o quitar letras, volviendo a guardar en cada modificación dicho listado para que se mantenga entre las distintas ejecuciones de la aplicación. Para ello se puede utilizar un array de estructuras que se guardará utilizando la librería ‘Disk’ que se puede instalar utilizando ‘CocoaPods’.

...

Desarrollar una aplicación para mostrar el uso y la personalización de los diversos componentes de la interfaz gráfica de la librería UIKit. Se deberán consultar las diferentes opciones que nos proporciona la librería y escribir el código swift necesario para cambiar el valor de las propiedades más significativas de cada uno de los componentes y visualizar los resultados:

UILabel, UIButton, UIBarButtonItem, UITextField (isSecureTextEntry), UITextView, UIFont, UIAlertController (UIAlertAction, alerts, action sheets), UIPickerView, UIImageView (UIImage, transform, CGAffineTransform, scale, rotate, reverse), UIScrollView, UITableView (UITableViewCell, cells, sections), UIPageControl, UISlider, UISwitch, UIDatePicker, UIActivityIndicatorView, UISearchBar, UIWindow, UIToolBar, UIProgressView, UISegmentedControl, UIStepper, UNUserNotificationCenter, WKWebView, UIStackView, etc.

Añadir a la aplicación anterior un teclado sencillo para reproducir las notas musicales básicas utilizando el componente UIStackView y el reproductor AVAudioPlayer de la librería AVFoundation. Tenéis un ejemplo completo en el último post, y los archivos de audio con las notas musicales en Google Drive:

...

Añadir a la aplicación anterior un apartado para reproducir diversos sonidos similares a los de la aplicación Instant Buttons. Se podrán utilizar una serie de botones distribuidos en forma de rejilla utilizando el componente UIStackView:

...

Añadir a la aplicación anterior un apartado para obtener de un servicio web un listado con todos los ciclos formativos que se imparten en la Comunidad Valenciana. Dicho listado se deberá mostrar utilizando un componente UITableView, y se deberán poder realizar búsquedas utilizando un componente UISearchBar. Además, se deberá proporcionar un mecanismo para poder mostrar los nombres de los ciclos formativos en castellano o valenciano (se puede utilizar por ejemplo un «action sheet» o también se pueden añadir botones a la barra de navegación, o se puede colocar una barra con dos botones en la parte inferior de la pantalla, etc.). Podéis encontrar un ejemplo completo sobre cómo personalizar la barra de navegación aquí, o cómo crear una barra de botones aquí.


Añadir a la aplicación anterior un apartado para obtener de un servicio web un listado con todos los institutos públicos o concertados de la Comunidad Valenciana donde se imparten ciclos formativos. Dicho listado se deberá mostrar utilizando un componente UITableView, y se deberán poder realizar búsquedas utilizando un componente UISearchBar. Además, al pulsar sobre un instituto en particular, se deberá mostrar un alert con toda la información relativa a dicho instituto:


Realizar una modificación en el apartado anterior para que se muestren todos los ciclos del centro educativo que se seleccione, pasando a una vista nueva (se puede consultar este enlace para ver un ejemplo de cómo añadir y navegar a una nueva vista). Para ello en la nueva vista se deberá realizar una consulta diferente utilizando el «id» del instituto seleccionado. Por ejemplo, para obtener los ciclos formativos que se imparten en el instituto IES San Vicente:

... {"id":47,"denominacion":"IES San Vicente","codigo_centro":"03008423","provincia":"Alacant","localidad":"Sant Vicent Del Raspeig","tipo_via":"Cl","domicilio":"Lillo Juan","numero":"128","cp":"3690","telefono":"965936505","fax":"965936506","email":"[email protected]","director":"Joaquín Pastor Pina","firmaLocalidad":"S. Vicent Raspeig","gestion_id":10,"gestion":{"id":10,"descripcion":"Público"}} ...

https://fernandoruizrico.com:8001/api/getListadoCiclos/47
...

Añadir a la búsqueda de centros educativos un componente «UIPickerView» para poder elegir qué tipo de búsqueda se pretende realizar (por nombre del instituto, por provincia, por localidad, etc.). Se deberá utilizar una enumeración para establecer los campos por los que se puede filtrar:

...

Añadir a la aplicación anterior un apartado para obtener datos sobre el tiempo atmosférico de una ciudad concreta que se introducirá por teclado. Se pueden realizar consultas a la api «http://api.openweathermap.org». A continuación se muestra una llamada y el resultado que se obtiene en formato JSON. Para obtener dicho resultado sólo es necesario proporcionar un identificador de usuario (appid) que se obtiene gratuitamente en el siguiente enlace «https://openweathermap.org/appid«:

https://api.openweathermap.org/data/2.5/weather?q=Alicante&appid=xxxxxxxx&lang=es


{"coord":{"lon":-74.23,"lat":10.65},"weather":[{"id":800,"main":"Clear","description":"cielo claro","icon":"01n"}],"base":"stations","main":{"temp":300.82,"pressure":1009,"humidity":83,"temp_min":300.15,"temp_max":301.15},"visibility":10000,"wind":{"speed":3.6,"deg":10},"clouds":{"all":0},"dt":1562913434,"sys":{"type":1,"id":8584,"message":0.0066,"country":"CO","sunrise":1562928074,"sunset":1562973803},"timezone":-18000,"id":3682292,"name":"Alicante","cod":200}

Añadir a la aplicación anterior un apartado para crear una lista de tareas utilizando un componente UITableView y la librería Disk para guardar el listado en el móvil, permitiendo por lo menos añadir y borrar tareas:


Swift – Ejercicios propuestos (IV)

Utiliza el método filter para obtener todos los nombre de personas que se encuentran en un array llamado nombres y que comiencen por la letra «A»:

let nombres = ["Ana", "Juan", "Pepe", "Anastasio", "Ángel"]
let nombresConA = nombres.filter { $0.first == "A" || $0.first == "Á" }
print("Los siguientes nombres comienzan por 'A': \(nombresConA)")

Utilizando el método reduce, calcula la suma de todos los enteros de un array:

let numeros = [1, 2, 3, 4, 6, 8, 9, 3, 12, 11]
let suma = numeros.reduce(0, {$0 + $1})
print("La suma de los números \(numeros) es \(suma)")

Utilizando el método map calcula el número real que se obtiene al dividir el numerador entre el denominador de diversas fracciones que se encuentran almacenadas en un array de tuplas, e imprime el resultado:

let fracciones = [(1,2), (2, 3), (5, 1), (4, 7)]
let reales = fracciones.map {Double($0.0) / Double($0.1)}
print("De las fracciones \(fracciones) se obtienen los números reales \(reales)")

Utilizando el método sorted, ordena ascendentemente todos los enteros de un array:

let numeros = [1, 6, 3, 34, 6, 8, 9, 3, 12, 11]
let numerosOrdenados = numeros.sorted(by: <)
print(numerosOrdenados)

Definir una clase libro que tenga dos propiedades: titulo y paginas. A continuación crear un array de libros con diferentes títulos y páginas, y ordenar los libros por el título de manera ascendente utilizando el método sorted. Finalmente se deberá mostrar el resultado por pantalla:

class Libro {
    var titulo = ""
    var paginas = 0
    init(_ titulo:String, _ paginas:Int) {
        self.titulo = titulo
        self.paginas = paginas
    }
}

var libros = [Libro("Juego de tronos", 250), Libro("El principito", 100), Libro("Blancanieves", 130)]
let librosOrdenados = libros.sorted(by: {$0.titulo < $1.titulo})
print("Libros ordenados por titulo:")
for libro in librosOrdenados {
    print(libro.titulo)
}

Utiliza el método filter para crear un array llamado multiplos que contenga todos los múltiplos de 3 de un array de enteros que se llame numeros e imprímelo para comprobar el resultado:

...

Utilizando el método reduce, encuentra el número más grande de entre los enteros de un array que se llame numeros e imprímelo:

...

Utilizando el método reduce, une todas las cadenas de un array en una sola añadiendo espacios entre cada una de ellas, e imprime el resultado:

...

Utilizando el método filter sobre un array que contiene los nombres de varias personas, muestra sólo aquellos cuyo nombre tiene más de 5 letras:

... 

Utilizando el método reduce, calcula la suma de todos los enteros de un array, y a continuación calcula la media e imprime el resultado:

...

Utilizando el método map convierte a minúsculas todas las cadenas almacenadas en un array, e imprime el resultado:

...

Utilizando el método map obtén un array con el número de letras que tienen cada una de las cadenas de otro array, e imprime el resultado:

...

Utilizando el método sorted, ordena descendentemente todos los enteros de un array:

...

Utilizando el método sorted, ordena ascendentemente todos los nombres de personas que se encuentran almacenados en un array:

...

Utilizando el código de ejercicios anteriores, crear un array de libros con diferentes autores, títulos y páginas, y ordenar los libros por el número de páginas de manera ascendente y descendente utilizando el método sorted, mostrando finalmente el resultado:

...

Utilizando el código de ejercicios anteriores, crear un array de álbumes de fotos, y ordenarlos de manera ascendente por número de páginas:

...

Utilizando el código de ejercicios anteriores, crear un array de vehículos y ordenarlos de manera ascendente según su velocidad:

...

Swift – Closures

Introducción

Los closures son bloques de código que pueden ser pasados por parámetro a otras funciones a lo largo del código. Una de las ventajas que proveen es que son capaces de capturar y guardar referencias de las constantes y variables del contexto en que son definidos.

Las funciones, vistas en una entrada anterior, son casos especiales de closures, que tienen la característica de estar definidos bajo un nombre. Sin embargo, en algunos casos es necesario escribir algoritmos más cortos y específicos que no van a ser reutilizados o invocados varias veces sino que solo tienen sentido en un ámbito en particular. El caso más claro es el de funciones que aceptan funciones por parámetro, y que al momento de invocarlos se les pasa closures, sin nombre ni una declaración completa, sino simplemente el código de su implementación.

Las expresiones de cerraduras proveen muchas optimizaciones en su sintaxis permitiendo simplificarlas sin perder el sentido y la claridad.

Para hacer una analogía con otros lenguajes, una cerradura es similar a una expresión lambda o bloques de Objective C.

Sintaxis

La forma general de una cerradura es la siguiente:

{ (parametros) -> tipo de retorno in
    sentencias
}

Como se puede observar, su sintaxis es la de una función con la diferencia de que no posee un nombre que la defina y que agrega la palabra in para separar su declaración de su implementación. Los parámetros pueden ser in-out pero no pueden tener valores por defecto.

Supongamos que tenemos la siguiente función:

func calcular(a:Int, b:Int, operacion:(Int,Int) -> Int) {
    print(“El resultado es \(operacion(a,b))”)
}

Para poder invocarla, necesitamos pasar dos números enteros y una función que reciba dos enteros y devuelva otro. Una posibilidad de hacerlo es definir una función que cumpla con el tipo del parámetro operación y pasarla por parámetro:

func sumar(a:Int, b:Int) -> Int {
    return a+b
}

calcular(a: 6, b: 10, operacion: sumar)

//Devuelve:
//El resultado es 16

De esta forma, podemos ejecutar una función u otra simplemente especificando el nombre de la función como último parámetro:

func sumar(a:Int, b:Int) -> Int {
    return a+b
}

func multiplicar(a:Int, b:Int) -> Int {
    return a*b
}

func calcular(a:Int, b:Int, operacion:(Int,Int) -> Int) {
    print("El resultado es \(operacion(a,b))")
}

calcular(a: 6, b: 10, operacion: sumar)
calcular(a: 6, b: 10, operacion: multiplicar)

// El resultado es 16
// El resultado es 60

Sin embargo, hacerlo de esta manera implica generar código que posiblemente no se vuelva a reutilizar y requiere de varias líneas para simplemente realizar una operación aritmética entre dos números.

En estos casos, es donde los closures vienen a ayudarnos a definir código mas específico y, al mismo tiempo, nos permite ahorrarnos algunas líneas. Una forma de hacer lo mismo usando closures podría ser:

func calcular(a:Int, b:Int, operacion:(Int, Int) -> Int) {
    print("El resultado es \(operacion(a,b))")
}

calcular(a: 6, b: 10, operacion: {(numero1:Int, numero2:Int) -> Int in return numero1 + numero2 })

calcular(a: 6, b: 10, operacion: {(numero1:Int, numero2:Int) -> Int in return numero1 * numero2 })

Simplificando la sintaxis

Lo primero que se puede simplificar al momento de usar un closure en la invocación de una función son los tipos de dato de sus parámetros y valor de retorno. En este sentido, Swift es capaz de inferir que el closure que estamos pasando debe cumplir con la forma (Int,Int) -> Int, por lo tanto no es necesario dejar explícitos los tipos de dato ni la flecha:

calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    return numero1 + numero2
})

calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    return numero1 * numero2
})

Además, los closures de una sola línea pueden omitir la palabra return ya que se sabe que la operación que se va a realizar dentro de ella se va a usar para devolver el dato que se espera:

calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    numero1 + numero2
})

calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    numero1 * numero2
})

Se dice que los closures son de una línea, o single-expression, cuando contienen una única operación independientemente de que hagamos cortes de línea por un tema de claridad al momento de leer el código.

Otra opción que podemos usar son los nombres de argumento corto que provee Swift para identificar a los argumentos, usando los nombres $0, $1, $2, etc, para hacer referencia al primer argumento, segundo, tercero, etc. Al mismo tiempo, se elimina la necesidad de usar la palabra reservada in ya que no se requiere separar la zona de argumentos de la de implementación:

calcular(a: 6, b: 10, operacion: { $0 + $1 })

calcular(a: 6, b: 10, operacion: { $0 * $1 })

Existe una forma aún más corta de escribir el closure anterior y es haciendo uso de los métodos de operador. Toda clase o struct puede definir su propia implementación de los operadores existentes haciendo uso de una técnica llamada sobrecarga. En el caso del tipo de dato Int (que internamente es un struct) tiene definido al operador + como una función del tipo (Int, Int) -> Int que es justamente lo que espera el parámetro calcular. Por lo tanto, la invocación anterior puede hacerse de la siguiente manera:

calcular(a: 6, b: 10, operacion: +)

calcular(a: 6, b: 10, operacion: *)

Trailing closures

Cada vez que escribimos una función que recibe un closure como parámetro, es recomendable dejar ese parámetro último en la lista para poder hacer uso de los trailing closures. El concepto de trailing hace alusión a que el closure puede escribirse por fuera del listado de parámetros, fuera de los paréntesis y a continuación de estos, para poder escribir un código más claro y sin la necesidad de especificar el nombre del argumento. En el caso de que la función no reciba otro parámetro adicional más que el closure, se pueden omitir los paréntesis:

func funcionCualquiera(closure: () -> Void) {

}

//llamando a la función sin usar trailing closure
funcionCualquiera(closure: {
    //Codigo
})

//llamando a la función con trailing closure
funcionCualquiera() {
    //Codigo
}

//llamando a la función con closure sin los paréntesis
funcionCualquiera {
    //Codigo
}

Volviendo al ejemplo anterior, usando lo aprendido se debería escribir así:

calcular(a: 6, b: 10) { $0 + $1 }

calcular(a: 6, b: 10) { $0 * $1 }

Capturar valores usando closures

Como dijimos al principio, los closures pueden capturar valores del ámbito donde están definidos y hacer uso de esos valores incluso cuando este contexto ya no existe.

El ejemplo más claro es el caso de las funciones anidadas, donde la función interior captura las variables que pertenecen a la función que la engloba para realizar sus cálculos, incluso cuando solo se hace uso de la función interna.

Supongamos que tenemos una función que posee un atributo y una función anidada, y que al llamarla la misma devuelve a dicha función, de manera de poder ser utilizada fuera de su ámbito.

func hacerIncremento(en cantidad:Int) -> () -> Int {
    var total = 0
    func incrementar() -> Int {
        total += cantidad
        return total
    }
    return incrementar
}

Como se puede observar, al invocar a hacerIncremento(en:) lo que obtenemos es una referencia a la función incrementar(_:), en lugar de un simple entero, la cual aumenta el valor del atributo total que pertenece a la función que la engloba. Cabe destacar que los atributos total y cantidad, si bien son usados dentro de la función incrementar(_:) no pertecen a ella, sino que ésta captura sus valores al momento de ejecutarse.

let incrementarPorDiez = hacerIncremento(en:10)

print(incrementarPorDiez()) //Devuelve 10
print(incrementarPorDiez()) //Devuelve 20
print(incrementarPorDiez()) //Devuelve 30

Aquí hay un caso curioso. Como podemos ver, incrementarPorDiez es una constante con lo cual no debería poder cambiar su valor y sin embargo estamos observando que cada vez que lo ejecutamos aumenta en 10 su valor de retorno. Esto es posible porque los closures son tipos por referencia. Esto implica que cada vez que asignamos una constante o variable a un closure, en realidad lo que estamos haciendo es asignar la dirección de memoria donde ese closure existe a esa variable o constante, en lugar de hacer una copia fiel del mismo.

En otras palabras, cuando ejecutamos esta línea:

let incrementarPorDiez = hacerIncremento(en:10)

Swift determina un espacio en memoria para que exista la función hacerIncremento(en:) y devuelve la dirección de memoria que es alojada en incrementarPorDiez. Si hiciéramos otra constante apuntando a otra versión distinta de hacerIncremento(en:) obtendríamos lo siguiente:

let incrementarPorCuatro = hacerIncremento(en:4)

print(incrementarPorCuatro()) //Devuelve 4
print(incrementarPorCuatro()) //Devuelve 8
print(incrementarPorCuatro()) //Devuelve 12

Como es un incrementador nuevo, la variable total es nueva también y arranca desde el principio, con lo cual, a estas alturas tenemos 2 incrementadores distintos en memoria. Ahora bien, si queremos asignar a una constante el primer incrementador, sucede lo siguiente:

let otroIncrementadorPorDiez = incrementarPorDiez

print(otroIncrementadorPorDiez()) //Devuelve 40

Como vemos, no arranca de 0 sino de 30, que es donde había quedado el incrementador por 10. Esto sucede porque en la constante otroIncrementadorPorDiez lo que se está almacenando es la misma dirección de memoria a la que apunta incrementarPorDiez, con lo cual el atributo total al que están incrementando es el mismo.

Ejemplos completos

func calcular(a:Int, b:Int, operacion:(Int,Int)->Int){
    print("El resultado es \(operacion(a,b))")
}

func sumar(a:Int, b:Int) -> Int{
    return a+b
}

calcular(a: 6, b: 10, operacion: sumar)

//Devuelve:
//El resultado es 16

calcular(a: 6, b: 10, operacion: {(numero1:Int, numero2: Int) -> Int in
                                    return numero1 + numero2
                                })

calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    return numero1 + numero2
})


calcular(a: 6, b: 10, operacion: {(numero1, numero2) in
    numero1 + numero2
})

calcular(a: 6, b: 10, operacion: { $0 + $1 })

calcular(a: 4, b: 5, operacion: +)



// Trailing Closures
func funcionCualquiera(closure: () -> Void) {
    
}

//llamando a la función sin usar trailing Closure
funcionCualquiera(closure: {
    //Codigo
})

//llamando a la función con trailing Closure
funcionCualquiera() {
    //Codigo
}

calcular(a: 6, b: 10) { $0 + $1 }

func hacerIncremento(en cantidad:Int) -> () -> Int {
    var total = 0
    func incrementar() -> Int {
        total += cantidad
        return total
    }
    return incrementar
}

let incrementarPorDiez = hacerIncremento(en:10)
incrementarPorDiez()
//Devuelve 10
incrementarPorDiez()
//Devuelve 20
incrementarPorDiez()
//Devuelve 30

let incrementarPorCuatro = hacerIncremento(en: 4)
incrementarPorCuatro()
//Devuelve 4
incrementarPorCuatro()
//Devuelve 8
incrementarPorCuatro()
//Devuelve 12

let otroIncrementadorPorDiez = incrementarPorDiez
otroIncrementadorPorDiez()
//Devuelve 40