Ejercicio de SQLite: app de tareas pendientes (5 puntos)¶
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 archivotareas.db
. En el cuadro de diálogo de copia, pulsa sobre el botonoptions
de la parte inferior y asegúrate de que la casilla deCopy 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
- Copia en el proyecto la base de datos
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 "Configurar el proyecto" 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
atrue
la primera vez que ejecutes la aplicación tras la modificación
Código base¶
-
Crea una
struct
Swift llamadaTarea
en un archivoTarea.swift
y añádele como propiedades:id
de tipoInt
titulo
de tipoString
vencimiento
de tipoDate
descripcion
de tipoString
-
Crea un archivo
TareasManager.swift
donde se defina una clase del mismo nombre, y que sea una subclase deDBManager
. 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
- convierta
object
a Tarea - acceda a su propiedad
vencimiento
, que será una fecha, y la convierta a texto conDateFormatter
(mira las transparencias) - 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
delMasterViewController
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 deDetailItem
deNSDate
aTarea
- En la línea 20 de la misma clase cambiar
detail.description
pordetail.descripcion
que es un campo que sí tienen los objetosTarea
Funcionalidad 2: Insertar nueva tarea (2 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
Sería mucho más elegante tener una pantalla aparte para introducir los datos de la tarea, pero lo que nos importa en esta sesión es trabajar con SQLite y "perderíamos" demasiado tiempo en crear la interfaz.
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.