Para este nuevo proyecto estaremos trabajando de la siguiente manera para la entrega del prework:
- Haz un fork de este repositorio.
- Clona el repositorio a tu escritorio o carpeta de preferencia dentro de tu computadora.
- Entra a la carpeta Week5/Prework.
- Crea una carpeta con tu apellido paterno y nombre siguiendo la nomenclatura UpperCamelCase como se muestra en el siguiente ejemplo: GaltJohn. (Por cierto,
Quien es John Galt?
). - Responde el prework dentro de tu carpeta en un archivo llamado README.md
- Aunado al archivo README.md donde responderás la parte teórica, adjunta el archivo .swift o el playground con el código contestado.
- Cuando hayas terminado de contestarlo, haz un commit y push a tu repositorio.
- Finalmente, haz un pull request donde me notifiques que haz terminado el prework.
Para este proyecto crearemos una app que administre una lista de artículos y todas las operaciones relacionadas con la edición de artículos dentro de una lista. Si bien es un proyecto sumamente sencillo, es crucial entenderlo muy bien, pues los UITableViewControllers
son el elemento más popular de UIKit. Algunas de las operaciones que se pueden realizar sobre un UITableViewController
son las siguientes:
- Desplegar la lista.
- Agregar elementos a la lista.
- Editar los elementos de la lista.
- Borrar elementos de la lista.
- Guardar los elementos de la lista en el dispositivo.
Aunado a lo anterior, en este proyecto trabajaremos con una lista de pendientes (to do). Cada pendiente tendrá una fecha límite y unas notas. Cada pendiente podrá ser marcado como completado, pero se mantendrá en la lista hasta que el usuario lo elimine. Por esta razón, este proyecto deberá seguir las siguientes guías de diseño:
- Cuando la app se abra, ésta desplegará la lista de los pendientes.
- Dentro de la vista principal, controles para agregar y borrar pendientes de la lista.
- Al dar tap en un pendiente, la app mostrará información más detallada sobre cada pendiente.
- En la vista de detalle, el usuario podrá editar el pendiente seleccionado.
- La app guarda la última versión de la lista.
- UIKit: entender
UITableViewController
,UITableViewCell
- Swift: entender las diferencias entre una clase y una estructura, entender los protocolos y ejemplos de algunos de ellos.
- Computer Science: arquitectura Modelo Vista Controlador, ciclo de vida de una app.
- Soft skills: fomentar la creatividad y el trabajo en equipo mediante squads.
Este tercer proyecto se realizará en tripletas, te recomendamos que eligan una computadora sobre la cual estarán programando y que ocupen las otras dos computadoras para investigar y leer material complementario.
Para este tercer proyecto, es necesario que una integrante de la tripleta cree un fork del repositorio y cree una carpeta con el nombre de su tripleta. Cada tripleta tendrá un nombre asignado por un algoritmo. Si quieren trabajar de manera remota puden forkear de nuevo el repositorio forkeado por la integrante de su tripleta o que la integrante que lo forkeo las haga colaboradoras.
Para la entrega del proyecto, deberán crear un pull request para que se considerado como válido y poner un mensaje relevante con la entrega. Aunado a lo anterior, para que el proyecto sea considerado como válido, todas las integrantes deberán codificar o crear una parte de la interfaz del proyecto.
El algoritmo anterior, aunado a darles con quienes estarán trabajando y el nombre de su tripleta, también les asignará una parte del proyecto. Es decir, cada integrante de la tripleta será responsable de una parte del proyecto. Por ejemplo:
Danny Taggard: UI.
John Galt: UITableViewController.
Howard Roark: UITableViewCell y operaciones sobre la lista.
Al terminar la sesión, deberán hacer otro pull request donde reflejarán el trabajo que hicieron durante la sesión. Para que el pull request sea válido, deberán subirlo antes de las 14:05 pm. Si quieren seguir trabajando después, adelante, pero solo se contará como válido el pull request que envién antes de terminar la clase.
- Haz un fork del este repositorio.
- Clona el repositorio que forkeaste dentro del escritorio.
- Crea una carpeta con el nombre de tu tripleta.
- Agrega un archivo README.md con el nombre de las integrantes de tu tripleta y su responsabilidad dentro del proyecto.
- Creen un proyecto de Xcode llamado ToDoApp con la plantilla
Single View Application
y guárdenlo dentro de la carpeta que crearon.- Asegúrense de que seleccionen para la opción de
User Interface
la opción deStoryboard
. - Asegúrense de que la casilla
Create Git repository on my Mac
esté deseleccionada
- Asegúrense de que seleccionen para la opción de
-
Agrega un
UITableViewController
embebido en unUINavigationController
y modifica lo que sea necesario para que tu primer vista se vea así: -
Asígnale un identificador a la celda del
UITableViewController
.
- Crea un modelo para un objeto llamado
ToDo
y define las siguientes propiedades:- Título, indicador para saber si ya se completó el ToDo, fecha, notas.
- Ojo, no todos los ToDo tendrán notas. ¿Qué significa esto para la definición de mi modelo?
- Dentro del modelo
ToDo
, escribe un método estático (static
) que obtenga la información guardada en disco y regrese un arreglo deToDo
's si es que los logró encontrar. (considera usar opcionales para el arreglo que tiene que regresar esta función. El código de esta función la escribirás más adelante, por lo que regresanil
por el momento. - Dentro del modelo
ToDo
, escribe otro método estático encargado de generar un arreglo deToDo
con información de prueba (mock-up data).- Como mockup data, para el atributo de fecha, asigna lo siguiente: (para los demás parámetros asígnale los datos que quieras)
ToDo(title: "Un título", isComplete: false, dueDate: Date(), notes: "...")
- Una vez que tengas diseñana la interfaz del punto uno de la sección de UI, necesitarás crear una clase para controlar la interfaz. Llámala
ToDoTableViewController
y esta clase debe heredar de la claseUITableViewController
. Puedes crearla desde cero o como una CocoaTouchClass. Recuerda,UITableViewController
pertenece aUIKit
.- No olvides de ligar la interfaz con la clase que acabas de crear.
- Inicializa un arreglo vacío de
ToDo
(completa el punto uno de la sección Modelo) o continua con el siguiente punto si tu compañera de equipo aún no termina la sección de Modelo. - Sobre carga los métodos
numberOfRowsInSection
ycellForRowAt
y defínelos correctamente para que se despliegue la información del arreglo detodos
. - Sobrecarga el método
viewDidLoad()
.- Recuerda llamar a
super.ViewDidLoad()
.
- Recuerda llamar a
- Dentro de
viewDidLoad()
asigna al arreglo vacío deToDo
's la información de los métodos estáticos del punto 2 y 3 de la sección de Modelo. En caso de que el método del punto 2 de esa misma sección regresenil
, asíganle el mock up data al arreglo del método del punto 3.
- Arrastra un
UIBarButtonIttem
al navigation bar y cambia elSystem Item
aAdd
. - Cambia el título del navigation bar a:
My To-Do`s
- Arrastra un
UINavigationController
a la escena. - Arrastra del botón
Add
alUINavigationController
y selecciona Present Modally.
- Agrega el código necesario para borrar elementos del
UITableView
. - Agrega un
editButtonItem
a la propiedadleftBarButtonItem
del navigation bar.
-
Para el
UITableView
de hasta la derecha, cambia el contenido deDynamic Prototypes
aStatic Content
y cambia el estilo aGrouped
. -
Agrega dos bar button items, uno llamado
Cancel
y otro llamadoSave
. -
Realiza el punto 1 del Controlador.
-
Vincula estos dos items a la función
unwind
delToDoTableViewController
. -
Agrega un identificador al segue del botón
Save
. -
Asegúrate que el table view tenga tres secciones, cada sección con solo una celda.
-
Actualiza el header de la sección 1 a Basic Info.
-
Arrastra un TextField y un Button.
-
Para el Button, y usando constraints:
- Céntralo verticalmente dentro del contenedor.
- Alinea el margen izquierdo (leading) al margen izquierdo (leading) de la celda.
- Width: 36
- Height: 36
- Elimina el título del botón.
- Descarga SFSymbols que es una biblioteca de imágenes de objetos. https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
- Observa la propiedad State config en el inspector de atributos:
- Default: escoge una imagen con una palomita (checkmark).
- Selected: escoge una imagen con un checkmark con el fondo relleno.
-
Para el TextField, y usando constraints:
- Céntralo verticalmente en el contenedor.
- Alinea el margen izquierdo (leading) al margen izquierdo (leading) de la celda con una separacion de 8 pixeles.
- Alinea el margen derecho (trailing) al margen derecho (trailing) de la celda.
- Placeholder: Recuérdame ...
Finalmente, termina por construir la siguiente interfaz
Algunos tips:
- Para la segunda y tercer sección:
- Cambia el tamaño de la celda a 200:
- Selecciona la celda > Size Inspector > Deselecciona Automatic > Escribe 200.
- Los elementos que ves tienen, de igual manera, constraints:
- Para la segunda sección: dos labels y un DatePicker.
- Para la tercer sección: solamente un UITextView.
- Cambia el tamaño de la celda a 200:
-
Agrega un método de
unwind
para poder regresar alUITableView
principal. -
Crea una clase que herede del
UITableViewController
y asígnala al storyboard del table view estático, llama a esta claseToDoViewController
. -
Posteriormente, crea outlets para todos los elementos de la interfaz y uno más para el butón Save.
-
Escribe un método que active o desactive al Save button dependiendo de si existe texto dentro del TextField.
-
Crea una acción para el textfield:
-
Crea otra acción para el textfield, pero ahora cuando ocurra el evento Primary Action Triggered
-
Y dentro de esa función escribe lo siguiente:
-
titleTextField.resignFirstResponder() //yo nombré a mi outlet titleTextField, pero sustitúyelo por el nombre que tu le diste.
-
Esta función se encarga de que cuando el usuario le de click al botón return del teclado, el teclado se ocultará.
-
Ahora, agrega un IBAction más para que el botón de isComplete cambie su estado. Es decir, que cuando el usuario le de click al botón para indicar si el ToDo está completo, éste botón tiene que cambiar su imagen. Para lo anterior, agrega el siguiente código a la función que creaste.
-
isCompleteButton.isSelected = !isCompleteButton.isSelected
-
-
Completa el paso 1 del Modelo.
-
Agrega una función encargada de recibir un objeto de tipo Date como parámetro y actualiza el label para que muestre la fecha indicada por el usuario en el DatePicker usando el dateformatter que creaste dentro del modelo. Recuerda como llamar propiedades estáticas definidas dentro de una estructura.
- DateFormatter tiene un método llamado
string(from: Date())
encargado de regresar un String a partir de un Date. Llama a éste método y asígnalo al label.
- DateFormatter tiene un método llamado
-
Recuerda llamar a la función que acabas de crear, también, desde del
viewDidLoad()
. -
Agrega un @IBAction para el date picker y llama a la función del punto 9.
-
La altura de la celda del DatePicker (Section 1, Row 0) debería depender de si el usuario le da click o no, si le da click, ésta se debe expandir y si le da click de nuevo, ésta debe colapsar. Pero antes, agrega una variable booleana que indique si el DatePicker está escondido.
-
Dentro de
ToDoViewController
sobrecarga el métodotableView(_ tableView:, heightForRowAt indexPath: )
.-
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let normalCellHeight = CGFloat(44) let largeCellHeight = CGFloat(200) switch indexPath { case [1,0]: return isPickerHidden ? normalCellHeight:largeCellHeight case [2,0]: return largeCellHeight default: return normalCellHeight } }
-
-
Ahora, sobre carga el método
didSelectRowAt
:-
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch (indexPath) { case [1,0]: isPickerHidden = !isPickerHidden print(isPickerHidden) dueDateLabel.textColor = isPickerHidden ? .black : tableView.tintColor tableView.beginUpdates() tableView.endUpdates() default: break } }
-
-
El texto para el label de la fecha debere reflejar el valor escogido por el usuario en el DatePicker. Pero primero necesitas una manera de convertir, a partir de un objeto Date, un String. Agrega una constante estática a ToDo que sea de tipo DateFormatter.
static let dueDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short return formatter }()
Ahora que tienes ya casi toda la interfaz lista, es hora de guardar un ToDo. Hay que hacer básicamente dos cosas:
-
Obtener los datos de la interfaz para mandarlo al
ToDoTableViewController
mediante un segue.-
Dentro de
ToDoViewController
sobre carga el métodoprepare(for segue: )
y asegúrate que el identificador del segue sea el correcto:override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) guard segue.identifier == "SaveUnwind" else {return} let title = titleTextField.text! let isComplete = isCompleteButton.isSelected let dueDate = datePicker.date let notes = notesTextView.text }
-
Crea una propiedad llamada
todo
de tipoToDo?
(esta variable debe ser opcional).
-
-
Insertar el ToDo al tableview del
ToDoTableViewController
.- Dentro de la función
unwind
agrega el código necesario para aceptar eltodo
deToDoViewController
.
- Dentro de la función
-
Arrastra la celda del
ToDoTableViewController
hacía el NavigationController y selecciona PresentModally. -
Agrega un identificador al segue anterior ->
ShowDetails
.
-
Al hacer lo anterior, necesitarán pasar la información del modelo de datos de la celda que seleccionaron al
ToDoViewController
. Para hacer lo anterior, recuerden implementar la funciónprepare(for segue: )
desdeToDoTableViewController
y crear una propiedadtodo
dentro deToDoViewController
.override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard segue.identifier == "ShowDetails" else {return} let navController = segue.destination as! UINavigationController let todoViewController = navController.topViewController as! ToDoViewController guard let newIndexPath = tableView.indexPathForSelectedRow else {return} let selectedTodo = todos[newIndexPath.row] todoViewController.todo = selectedTodo }
-
El table view que creamos puede fungir para editar el ToDo que seleccionemos o para agregar un nuevo ToDo. Cuando demos click en el botón
+
la variabletodo
estará vacía, pero si damos click en la celda, la variabletodo
no será vacía. Agrega las siguientes líneas dentro delviewDidLoad
if let todo = todo { navigationItem.title = "To-Do" titleTextField.text = todo.title isCompleteButton.isSelected = todo.isComplete datePicker.date = todo.dueDate notesTextView.text = todo.notes } else { datePicker.date = Date().addingTimeInterval(24*60*60) }
-
Finalmente, ¿qué pasa si quiero actualizar un ToDo? Tenemos que reemplazar el modelo de datos antiguo del table view y sustituirlo por el nuevo modelo de datos con la información actualizada. Dentro del método
unwind
delToDoTableViewController
agrega lo siguiente:@IBAction func unwindToTodoTableVC(_ segue: UIStoryboardSegue) { guard segue.identifier == "SaveUnwind" else {return} let sourceViewController = segue.source as! ToDoViewController if let todo = sourceViewController.todo { if let selectedIndexPath = tableView.indexPathForSelectedRow{ todos[selectedIndexPath.row] = todo tableView.reloadRows(at: [selectedIndexPath], with: .none) } else { let newIndexPath = IndexPath(row: todos.count, section: 0) todos.append(todo) tableView.insertRows(at: [newIndexPath], with: .automatic) } } }
Por último, necesitamos que nuestra app guarde la información que el usuario ingresó al disco del dispositivo. Para esto, usaremos un protocolo llamado Codable
. Este protocolo nos ayudará a que las instancias de nuestra estructura ToDo
pueda ser codificada y descodificada.
Para esta app, vamos a guardar la información dentro del dispositivo en un directorio llamado Document's directory. A este directorio solo puede entrar tu app, por lo que es un lugar seguro para guardar la información.
-
Haz que tu estructura
ToDo
conforme al protocoloCodable
. -
Agrega las siguientes constantes a la definición de
ToDo
.static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! static let ArchiveUrl = DocumentsDirectory.appendingPathComponent("todos").appendingPathExtension("plist")
-
Ahora que agregamos las propiedades anteriores, es posible agregar lógica al método
loadToDos()
.static func loadToDos() -> [ToDo]? { guard let codedTodos = try? Data(contentsOf: ArchiveUrl) else {return nil} let propertyListDecoder = PropertyListDecoder() return try? propertyListDecoder.decode(Array<ToDo>.self, from: codedTodos) }
-
Ahora, y antes de comenzar a leer información, hay que tener un método para guardar la información. Creen la siguiente función.
static func saveToDos(_ todos: [ToDo]) {
let propertyListEncoder = PropertyListEncoder()
let codedTodos = try? propertyListEncoder.encode(todos)
try? codedTodos?.write(to: ArchiveUrl, options: .noFileProtection)
}
- ¿Ahora, cuando sería el momento apropiado para guardar la información al disco? Primero, cuando el usuario quite un elemento del arreglo, es decir, cuando borre un todo del table view. O, cuando el usuario cree o actualice un todo. Manda a llamar a la función
saveToDos
donde sea necesario.
!Felicidades! Lograron completar el tercer proyecto. ¿Quieren avanzar un poco más?:
- Implementen una funcionalidad de búsqueda de ToDo's.
- Implementen una funcionalidad de filtrado por ToDo's completados.
- Introducción al Desarrollo de Apps. https://books.apple.com/mx/book/introducci%C3%B3n-al-desarrollo-de-apps-con-swift/id1216831475
- App Development with Swift. https://books.apple.com/us/book/app-development-with-swift/id1219117996
- The Swift Programming Language Guide. https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html
- Apple’s World Wide Developers Conference Videos. https://developer.apple.com/videos/
- Human Interface Guidelines: iOS. https://developer.apple.com/design/human-interface-guidelines/ios/overview/themes/
- Apple Developer. https://developer.apple.com/
Otros sitios recomendados:
- Paul Hudson. Swift in Sixty Seconds. https://www.hackingwithswift.com/sixty