Ejercicios

En este ejercicio vamos a crear una pequeña aplicación de gestión de tareas pendientes, en la que se puedan listar tareas y añadir tareas nuevas. Cada tarea tiene un id (entero, autonumérico), un titulo (cadena), un vencimiento (fecha) y una una descripcion (cadena). Las columnas están por este orden en la tabla. La base de datos ya está creada, y tú debes copiarla a tu proyecto como se explica a continuación.

Infraestructura básica (1 punto)

Configurar el proyecto

  • Crear un proyecto llamado TareasSQLite de tipo Master-Detail Application

  • En la carpeta archivos SQLite de las plantillas hay unos cuantos recursos que debes copiar al proyecto

    • Copia en el proyecto la base de datos tareas.db. NO LO HAGAS ARRASTRANDO, usa el menú File > Add files to TareasSQLite... y selecciona el archivo tareas.db. En el cuadro de diálogo de copia, pulsa sobre el boton options de la parte inferior y asegúrate de que la casilla de Copy items if needed está marcada. En caso contrario estás haciendo solo una referencia al archivo original, que se pierde si mueves el proyecto, y tampoco se sube al repositorio.

    • Crea en el proyecto un DBManager.swift con el siguiente código, es muy parecido al que tienes en los apuntes

import Foundation

class DBManager {
    var db : OpaquePointer? = nil

    init(db nombreDB : String, reload: Bool) {
        if let dbCopiaURL = copyDB(conNombre: nombreDB, reload: reload) {
            if sqlite3_open(dbCopiaURL.path, &(self.db)) == SQLITE_OK {
                print("Base de datos \(dbCopiaURL) abierta OK")
            }
            else {
                let error = String(cString:sqlite3_errmsg(db))
                print("Error al intentar abrir la BD: \(error) ")
            }
        }
        else {
            print("El archivo no se encuentra")
        }
    }

    deinit {
        sqlite3_close(self.db)
    }

    //Copia la base de datos desde el bundle al directorio Documents, para que se pueda modificar
    //si el parámetro "machaca" es true, copia la BD aunque ya esté en Documents.
    //En una app normal esto no lo haríamos cada vez que arranquemos, ya que se machacaría la BD
    func copyDB(conNombre nombre : String, reload machaca : Bool)->URL? {
        let fileManager = FileManager.default
        let docsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let dbCopiaURL = docsURL.appendingPathComponent(nombre)
        let existe = fileManager.fileExists(atPath: dbCopiaURL.path)
        if  existe && !machaca {
            return dbCopiaURL
        }  
        else {
            if let dbOriginalURL = Bundle.main.url(forResource: nombre, withExtension: "") {
                if (existe) {
                    try! fileManager.removeItem(at: dbCopiaURL)
                }
                if (try? fileManager.copyItem(at: dbOriginalURL, to: dbCopiaURL)) != nil {
                    return dbCopiaURL
                }
                else {
                    return nil
                }
            }
            else {
                return nil
            }
        }
    }    
}

De momento, dará errores de compilación porque todavía no has incluido SQLite en el proyecto. Ahora hay que configurar el proyecto para poder usar SQLite. Añade la librería libsqlite3.tbd y crea el Bridging Header según se explica en el apartado de configuración de SQlite de los apuntes.

Para comprobar que funciona, introduce el siguiente código en el método viewDidLoad del MasterViewController

let manager = DBManager(db:"tareas.db", reload:false)

Si todo es correcto, en el log debe aparecer el mensaje "Base de datos url_enormemente_larga_de_la_BD abierta OK". Una vez que sepas que funciona, quita la línea que has insertado en el viewDidLoad para que no interfiera con el resto del ejercicio.

Nótese que como primer parámetro se pasa el nombre de la BD, y como segundo un booleano indicando si la copia de la BD de Documents se va a sobreescribir cada vez que se arranque la aplicación (útil cuando en desarrollo estamos cambiando “desde fuera” la BD para hacer pruebas)

Si por algún motivo cambias manualmente la estructura o el contenido de la base de datos, recuerda poner el parámetro reload a true la primera vez que ejecutes la aplicación tras la modificación

Código base

  • Crea una struct Swift llamada Tarea en un archivo Tarea.swift y añádele como propiedades:

    • id de tipo Int

    • titulo de tipo String

    • vencimiento de tipo Date

    • descripcion de tipo String

  • Crea un archivo TareasManager.swift donde se defina una clase del mismo nombre, y que sea una subclase de DBManager. En los siguientes apartados implementaremos aquí las operaciones con la tabla de tareas.

Funcionalidad 1: Listar tareas (3 puntos)

Implementar el listado en sí

En TareasManager Implementa un método listarTareas que debe hacer un SELECT de la tabla tareas ordenado por fecha de vencimiento y devolver un array de objetos Tarea . Será muy similar al código que sirve para listar personas en los apuntes.

Recuerda que en la tabla tareas las columnas son los mismos campos que en la struct Tarea: id, titulo, vencimiento y descripcion.

Para ir paso a paso, puedes implementar primero en TareasManager una versión inicial de listarTareas que solo saque de la BD el campo titulo (columna 1, de tipo cadena), y ponga el resto de campos a valores fijos y arbitrarios.

Una vez lo tengas puedes probarlo en el MasterViewController. Añade la siguiente propiedad para almacenar una referencia al TareasManager

var tm : TareasManager! = nil

y ahora en el viewDidLoad añade:

self.tm = TareasManager(db: "tareas.db", reload: false)
let lista = self.tm.listarTareas()
for t in lista {
    print("\(t.titulo)");
}

Deberían salir los títulos de 3 tareas distintas. Si esto va, ya sabes que la parte básica te funciona y ahora puedes añadir en listarTareas el resto de campos: id, descripcion y vencimiento. Este último campo ten en cuenta que usa “tiempo UNIX”: número de segundos transcurridos desde el 1/1/1970.

Mostrar las tareas en la tabla

Vamos a modificar el código del viewDidLoad para integrarlo mejor con la plantilla que ha generado Xcode. Fijate que lo que nosotros definimos como lista la plantilla lo define en la propiedad objects de la clase MasterViewController, y que es un array de objetos cualesquiera ([Any]()). Cámbialo por un array de tareas, [Tarea]()), y cambia el código del apartado anterior para que use self.objects en vez de lista. Quedará como:

self.tm = TareasManager(db: "tareas.db", reload: false)
self.objects = self.tm.listarTareas()

Para que las tareas aparezcan correctamente el interfaz gráfico puedes cambiar las línea en el método tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) que dicen

let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description

por otras que usen la clase Tarea y referencien su propiedad titulo:

let tarea = objects[indexPath.row]
cell.textLabel!.text = tarea.titulo

Para evitar errores de compilación, en el método insertNewObject cambia la primera línea (objects.insert...) por:

objects.insert(Tarea(id:0, titulo:"Prueba", descripcion:"", vencimiento:Date()), at: 0)

Ahora puedes probar la app y los títulos de las tareas deberían aparecer en la lista (aunque si pinchas en alguna de ellas dará error en tiempo de ejecución, ya que la plantilla asume que son NSDate y no objetos Tarea)

Para poder ver algún detalle más en la lista, por ejemplo la fecha de vencimiento, abre el storyboard y selecciona la celda de tabla que aparece en la segunda pantalla de la aplicación. En las propiedades, cambiar el Style de Basic a Subtitle, por ejemplo (aunque también valdrían los otros dos estilos).

En el código de tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) debes incluir código que

  1. convierta object a Tarea

  2. acceda a su propiedad vencimiento, que será una fecha, y la convierta a texto con DateFormatter (mira las transparencias)

  3. Asigne esta fecha en formato texto a cell.detailTextLabel.text.

Finalmente, para que funcione la pantalla secundaria de detalle hay que:

  • En el método prepare del MasterViewController cambiar la línea

let object = objects[indexPath.row] as! NSDate

eliminando la conversión a NSDate, quedará simplemente como

let object = objects[indexPath.row]
  • En la línea 31 de la clase DetailViewController cambiar el tipo de DetailItem de NSDate a Tarea

  • En la línea 20 de la misma clase cambiar detail.description por detail.descripcion que es un campo que sí tienen los objetos Tarea

Funcionalidad 2: Insertar nueva tarea (3 puntos)

Implementar un método func insertar(tarea : Tarea)->Bool en la clase TareasManager, que inserte una nueva tarea en la BD y devuelva true si todo ha ido bien y false en caso contrario.

Al campo id no es necesario darle valor al insertar un registro ya que es autonumérico.

Primero comprueba que funciona correctamente, insertando una tarea con datos fijos desde el viewDidLoad del MasterViewController. (hazlo antes del que las lista, para que la nueva esté incluida también en la lista de tareas)

let nueva = Tarea()
nueva.titulo = "Tarea nueva";
nueva.descripcion = "nueva descripcion";
//24*60*60 segundos posterior a la fecha actual -> mañana a la misma hora
nueva.vencimiento = Date(timeIntervalSinceNow:24*60*60);
self.tm.insertar(tarea:nueva)

Una vez comprobado puedes eliminar el código anterior. Para añadir la funcionalidad en la interfaz de forma sencilla podemos usar un UIAlertAction. Copia este método en el MasterViewController:

@objc func nuevaTarea() {
    let alert = UIAlertController(title: "Nueva tarea",
                                 message: "Introduce los datos",
                                 preferredStyle: .alert)
    let crear = UIAlertAction(title: "Crear", style: .default) {
        action in
        let tit = alert.textFields![0].text!
        if let desc = alert.textFields![1].text! {
            if let diasVenc = Double(alert.textFields![2].text!) {
               let venc = Date(timeIntervalSinceNow:24*60*60*diasVenc)
               let t = Tarea(id:0, titulo: tit, descripcion: desc, vencimiento: venc)
               if (self.tm.insertar(tarea: t)) {
                    self.objects.insert(t, at: 0)
                    let indexPath = IndexPath(row: 0, section: 0)
                    self.tableView.insertRows(at: [indexPath], with: .automatic)
               }
            }
        }
    }
    let cancelar = UIAlertAction(title: "Cancelar", style: .cancel) {
        action in
        print("Cancelada creación de tarea")
    }
    alert.addAction(crear)
    alert.addAction(cancelar)
    alert.addTextField() { $0.placeholder = "Título"}
    alert.addTextField() { $0.placeholder = "Descripción"}
    alert.addTextField() { $0.placeholder = "Vencimiento (días)"}
    self.present(alert, animated: true)
}

Ahora hay que vincular el botón + de la interfaz con este método. En el viewDidLoad busca la línea

let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_)))

y déjala como

let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(nuevaTarea))

Ahora al pulsar el botón + debería aparecer un alert para escribir los datos de la tarea. En esta versión simplificada no se puede poner una fecha de vencimiento día-mes-año, sino solo un número de días a partir de la fecha actual.

Last updated