iOS desde cero con Swift: creación de una aplicación de lista de compras 2
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En la lección anterior, sentamos las bases para la aplicación de la lista de compras. En la primera parte de esta lección, refinamos aún más la aplicación al permitir al usuario editar y eliminar elementos de la lista. Más tarde, agregamos la capacidad de seleccionar elementos de la lista para crear la lista de compras.
1. Eliminar elementos
Eliminar elementos de la lista es una adición importante en términos de experiencia del usuario y usabilidad general. Agregar esta habilidad implica:
- eliminar el elemento de la propiedad
itemsdel controlador de vista - actualizar la tabla
- guardando los cambios en el disco
Veamos cómo funciona esto en
la práctica. Primero debemos agregar un botón de edición a la barra de
navegación. En
el método viewDidLoad() del controlador de vista, cree una instancia
de UIBarButtonItem y asígnela a la propiedad rightBarButtonItem de la
propiedad navigationItem del controlador de vista.
Como
hicimos en la lección anterior, creamos un elemento de botón de barra
invocando init(barButtonSystemItem:target:action:), pasando .Edit,
un valor de miembro de UIBarButtonSystemItem, self como el objetivo y
"editItems:" como selector .
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Register Class
|
5 |
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) |
6 |
|
7 |
// Create Add Button
|
8 |
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "addItem:") |
9 |
|
10 |
// Create Edit Button
|
11 |
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Edit, target: self, action: "editItems:") |
12 |
}
|
La implementación de editItems(_:) es solo una línea de código, como puede ver a continuación. Cada vez que el usuario toca el botón de edición, la vista de tabla se activa o desactiva el modo de edición. Hacemos esto usando un pequeño truco. Le
preguntamos a la tabla si está o no en modo de edición, que devuelve un
valor de Bool, e inversamente el valor devuelto (true se convierte en
false y viceversa). El método que llamamos en la
vista de tabla es setEditing(_:animated:), un setter especializado
que acepta un parámetro de animación.
1 |
func editItems(sender: UIBarButtonItem) { |
2 |
tableView.setEditing(!tableView.editing, animated: true) |
3 |
}
|
Si ejecuta
la aplicación de la lista de compras en el simulador y toca el botón de
edición, debería ver que la vista de tabla está activada y desactivada
en el modo de edición.






Dos métodos del protocolo UITableViewDataSource son importantes para permitir la edición en una vista de tabla:
tableView(_:canEditRowAtIndexPath:)tableView(_:commitEditingStyle:forRowAtIndexPath:)
Si el
usuario toca el botón de edición, la vista de tabla pregunta a su origen
de datos qué filas son editables enviando un fuente de datos a
tableView(_:canEditRowAtIndexPath:). Si se
devuelve true para una ruta de índice en particular, la vista de
tabla indica a la celda de vista de tabla correspondiente que necesita
para alternar dentro o fuera del modo de edición, dependiendo del modo
de edición de la vista de tabla. Esto se traduce en la celda de vista de
tabla que muestra u oculta sus controles de edición. Implemente
el método tableView(_:canEditRowAtIndexPath:) como se muestra a
continuación para ver cómo funciona esto en la práctica.
1 |
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { |
2 |
if indexPath.row == 1 { |
3 |
return false |
4 |
}
|
5 |
|
6 |
return true |
7 |
}
|
La
implementación anterior de tableView(_:canEditRowAtIndexPath:) permite al usuario editar cada fila en la vista de tabla con la
excepción de la segunda fila. Ejecute la aplicación en el simulador y pruébela.



Para la aplicación de lista de compras, el usuario debe poder editar cada fila en la vista de tabla. Esto significa que tableView(_:canEditRowAtIndexPath:) siempre debe devolver true.
1 |
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { |
2 |
return true |
3 |
}
|
Puede haber notado que no ocurre nada cuando intenta eliminar una fila en la vista de tabla. El rompecabezas no ha terminado todavía. Cada
vez que el usuario toca el botón Eliminar de una fila, la vista de
tabla envía a su fuente de datos un mensaje de tableView(_:commitEditingStyle:forRowAtIndexPath:). El
segundo argumento del método correspondiente indica qué tipo de acción
ha realizado el usuario, insertando o borrando una fila. Para la
aplicación de la lista de compras, solo implementaremos el soporte para
eliminar filas de la vista de tabla.
Eliminar filas de una vista de tabla implica:
- eliminar el elemento correspondiente de la propiedad
itemsdel controlador de vista - actualizando la vista de tabla eliminando la fila correspondiente
Examinemos la implementación de
tableView(_:commitEditingStyle:forRowAtIndexPath:). El método
comienza comprobando si el estilo de edición es igual a
.Delete (un valor de miembro de UITableViewCellEditingStyle), porque
solo queremos permitir que el usuario elimine filas de la vista de
tabla.
Si
el estilo de edición es igual a .Delete, el elemento correspondiente
se elimina de la propiedad items, la fila correspondiente se
elimina de la vista de tabla y la lista actualizada de elementos se
guarda en el disco.
1 |
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { |
2 |
if editingStyle == .Delete { |
3 |
// Delete Item from Items
|
4 |
items.removeAtIndex(indexPath.row) |
5 |
|
6 |
// Update Table View
|
7 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right) |
8 |
|
9 |
// Save Changes
|
10 |
saveItems() |
11 |
}
|
12 |
}
|
Ejecute la aplicación en el simulador y elimine algunos elementos. No olvide cerrar y volver a iniciar la aplicación para verificar que los elementos se eliminen permanentemente de la lista.
2. Edición de artículos
Podríamos reutilizar la clase
AddItemViewController para editar elementos. Sin
embargo, debido a que el uso de un solo controlador de vista para
agregar y editar elementos a menudo puede complicar demasiado la
implementación, generalmente termino creando una clase separada para
editar los elementos. Esto puede dar lugar inicialmente a que nos
repitamos, pero nos dará más flexibilidad en el futuro.
Paso 1: Crear el controlador de vista de edición
Cree una nueva subclase UIViewController y
asígnele el nombre EditItemViewController. La interfaz de las clases
AddItemViewController y EditItemViewController es muy similar.
1 |
import UIKit |
2 |
|
3 |
protocol EditItemViewControllerDelegate { |
4 |
func controller(controller: EditItemViewController, didUpdateItem item: Item) |
5 |
}
|
6 |
|
7 |
class EditItemViewController: UIViewController { |
8 |
|
9 |
@IBOutlet var nameTextField: UITextField! |
10 |
@IBOutlet var priceTextField: UITextField! |
11 |
|
12 |
var item: Item! |
13 |
|
14 |
var delegate: EditItemViewControllerDelegate? |
15 |
|
16 |
...
|
17 |
|
18 |
}
|
Las
diferencias son la adición de una propiedad, item, para almacenar
una referencia al elemento que se está editando y la definición del
método del protocolo EditItemViewControllerDelegate. Tenga en cuenta que el item es de tipo Item!, un accesorio desenvuelto forzado opcional. Debido
a que el controlador de vista de elemento de edición no sirve sin un
elemento para editar, esperamos que la propiedad del item siempre
tenga un valor.
Abra Main.storyboard, arrastre una
instancia de UIViewController desde la Biblioteca de objetos, establezca
su clase en EditItemViewController y cree una secuencia de muestra
manual desde el controlador de vista de lista hasta el controlador de
vista de edición de elementos. Establezca el identificador de segue en EditItemViewController.



Arrastre
los campos de texto de la Biblioteca de objetos a la vista del
controlador de vista y colóquelos como se muestra en la figura a
continuación. Seleccione el campo de texto superior, abra el Inspector de atributos e ingrese Nombre en el campo Marcador de posición. Seleccione
el campo de texto de la parte inferior y, en el Inspector de atributos,
establezca su texto de marcador de posición en Precio y establezca
Teclado en el Panel numérico. Seleccione el objeto
controlador de vista, abra el Inspector de conexiones y conecte las
salidas nameTextField y priceTextField con el campo de texto
correspondiente en la interfaz de usuario.



En el método viewDidLoad() del controlador de vista, cree el botón guardar como lo hicimos en la clase AddItemViewController.
1 |
// MARK: -
|
2 |
// MARK: View Life Cycle
|
3 |
override func viewDidLoad() { |
4 |
super.viewDidLoad() |
5 |
|
6 |
// Create Save Button
|
7 |
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Save, target: self, action: "save:") |
8 |
}
|
La implementación de la acción save(_:) es muy similar a la que implementamos en la clase AddItemViewController. Sin embargo, hay algunas diferencias sutiles que quiero señalar.
1 |
// MARK: -
|
2 |
// MARK: Actions
|
3 |
func save(sender: UIBarButtonItem) { |
4 |
if let name = nameTextField.text, let priceAsString = priceTextField.text, let price = Float(priceAsString) { |
5 |
// Update Item
|
6 |
item.name = name |
7 |
item.price = price |
8 |
|
9 |
// Notify Delegate
|
10 |
delegate?.controller(self, didUpdateItem: item) |
11 |
|
12 |
// Pop View Controller
|
13 |
navigationController?.popViewControllerAnimated(true) |
14 |
}
|
15 |
}
|
En
lugar de pasar los valores de nombre y precio al delegado, actualizamos
directamente el elemento y pasamos el elemento actualizado al delegado
del controlador de vista. Como el controlador de vista es un controlador de vista infantil de un
controlador de navegación, descartamos el controlador de vista
sacándolo de la pila de navegación.
Paso 2: Mostrar el controlador de vista Editar elemento
En unos minutos, implementaremos la funcionalidad que permite al usuario seleccionar elementos del controlador de vista de lista para agregarlos a la lista de compras. El usuario podrá hacer esto tocando una fila en la vista de lista. La pregunta es cómo el usuario podrá editar un elemento si toca reservar una fila para agregar un artículo a la lista de compras.
El
marco UIKit proporciona un botón de divulgación de detalles para este
caso de uso. Un botón de divulgación de detalles se coloca a la derecha
de una celda de vista de tabla. Para
agregar un botón de divulgación de detalles a una celda de vista de
tabla, necesitamos revisar tableView(_:cellForRowAtIndexPath:) en el
controlador de vista de lista y enmendarlo como se muestra a
continuación.
1 |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
2 |
// Dequeue Reusable Cell
|
3 |
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = items[indexPath.row] |
7 |
|
8 |
// Configure Table View Cell
|
9 |
cell.textLabel?.text = item.name |
10 |
cell.accessoryType = .DetailDisclosureButton |
11 |
|
12 |
return cell |
13 |
}
|
Cada celda de vista de tabla tiene una propiedad accessoryType. En
tableView(_:cellForRowAtIndexPath:), lo configuramos en
.DetailDisclosureButton, un valor miembro de
UITableViewCellAccessoryType. Puede haber notado que a los ingenieros de Apple no les gustan los nombres cortos.
¿Cómo notifica la vista de tabla a su delegado cuando se toca el botón de divulgación de detalles? Como
era de esperar, el protocolo UITableViewDelegate define el método tableView(_:accessoryButtonTappedForRowWithIndexPath:) para este
propósito. Eche un vistazo a su implementación.
1 |
// MARK: -
|
2 |
// MARK: Table View Delegate Methods
|
3 |
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) { |
4 |
// Fetch Item
|
5 |
let item = items[indexPath.row] |
6 |
|
7 |
// Update Selection
|
8 |
selection = item |
9 |
|
10 |
// Perform Segue
|
11 |
performSegueWithIdentifier("EditItemViewController", sender: self) |
12 |
}
|
Buscamos
el elemento correcto de la propiedad items y lo almacenamos en
selection, una propiedad del controlador de vista de lista que
declararemos en un momento. Luego realizamos la segue con el identificador EditItemViewController.
Antes
de actualizar la implementación de prepareForSegue(_:sender:), necesitamos declarar la propiedad para almacenar el elemento
seleccionado. También debemos adaptar la clase ListViewController al protocolo EditItemViewControllerDelegate.
1 |
import UIKit |
2 |
|
3 |
class ListViewController: UITableViewController, AddItemViewControllerDelegate, EditItemViewControllerDelegate { |
4 |
|
5 |
let CellIdentifier = "Cell Identifier" |
6 |
|
7 |
var items = [Item]() |
8 |
var selection: Item? |
9 |
|
10 |
...
|
11 |
|
12 |
}
|
La implementación actualizada de prepareForSegue(_:sender:) es bastante simple, como puede ver a continuación. Obtenemos una referencia al controlador de vista de elemento de edición y establecemos sus propiedades delegate y item. Debido
a que la propiedad selection es del tipo Item?, lo vinculamos a la
constante item para desenvolverlo de manera segura.
1 |
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { |
2 |
if segue.identifier == "AddItemViewController" { |
3 |
...
|
4 |
|
5 |
} else if segue.identifier == "EditItemViewController" { |
6 |
if let editItemViewController = segue.destinationViewController as? EditItemViewController, let item = selection { |
7 |
editItemViewController.delegate = self |
8 |
editItemViewController.item = item |
9 |
}
|
10 |
}
|
11 |
}
|
La implementación de EditItemViewController está casi completa. En su método viewDidLoad(), llenamos los campos de texto con los datos de la propiedad item. Debido
a que la propiedad text de un campo de texto es de tipo String?, utilizamos la interpolación de cadenas para crear una cadena a partir
del valor Float de la propiedad price del elemento. La interpolación de cadenas es bastante poderosa en Swift.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Create Save Button
|
5 |
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Save, target: self, action: "save:") |
6 |
|
7 |
// Populate Text Fields
|
8 |
nameTextField.text = item.name |
9 |
priceTextField.text = "\(item.price)" |
10 |
}
|
Paso 3: adopción del protocolo de delegado
Adoptar
el protocolo EditItemViewControllerDelegate significa implementar el
método controller(_:didUpdateItem:) como se muestra a continuación. Puede sorprenderle que no actualicemos el origen de datos de la vista de
tabla. Todo lo que hacemos en el método de delegado es volver a cargar
una fila de la vista de tabla.
1 |
// MARK: -
|
2 |
// MARK: Edit Item View Controller Delegate Methods
|
3 |
func controller(controller: EditItemViewController, didUpdateItem item: Item) { |
4 |
// Fetch Index for Item
|
5 |
if let index = items.indexOf(item) { |
6 |
// Update Table View
|
7 |
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade) |
8 |
}
|
9 |
|
10 |
// Save Items
|
11 |
saveItems() |
12 |
}
|
La razón
por la que no necesitamos actualizar el origen de datos de la vista de
tabla, la matriz items, es que el elemento actualizado se pasó
por referencia al controlador de vista de elemento de edición. En otras palabras, el objeto que el controlador de vista de elemento de
edición ha actualizado es el mismo objeto que está contenido en la
matriz items. Este es uno de los buenos beneficios de trabajar
con instancias de clase. Se pasan por referencia.
No olvides guardar la lista de elementos para asegurarte de que los cambios se escriben en el disco. Ejecute la aplicación para probar la funcionalidad de edición.
3. Crear el controlador de vista de lista de compras
Antes de explorar la
fuente de datos del controlador de vista de la lista de compras, creemos
algunos andamios para trabajar. Cree una nueva subclase
UITableViewController y asígnele el nombre
ShoppingListViewController.
Abra ShoppingListViewController.swift y
agregue dos propiedades de tipo [Item]:
-
items, que contendrán la lista completa de artículos -
shoppingList, que solo contendrá los artículos de la lista de compras
1 |
import UIKit |
2 |
|
3 |
class ShoppingListViewController: UITableViewController { |
4 |
|
5 |
var items = [Item]() |
6 |
var shoppingList = [Item]() |
7 |
|
8 |
...
|
9 |
|
10 |
}
|
La idea es
cargar la lista de elementos cada vez que se realiza un cambio en la
lista, analizar la lista de elementos y extraer solo los elementos que
tienen su propiedad inShoppingList establecida en true. Esos elementos luego se agregan a la lista shoppingList.
Otra opción sería almacenar la lista de compras en un archivo separado. La desventaja de este enfoque sería que tenemos que mantener los elementos en el controlador de vista de lista y los elementos en la lista de compras sincronizados. Esto es pedir problemas si me preguntas.
Paso 1: Agregar observadores de propiedades
Los observadores de propiedad son una
excelente manera de responder a los cambios en el valor de una
propiedad. Veamos
cómo podemos aprovechar los observadores de propiedades para actualizar
la propiedad shoppingList cada vez que cambia el valor de items.
1 |
var items = [Item]() { |
2 |
didSet { |
3 |
buildShoppingList() |
4 |
}
|
5 |
}
|
La sintaxis es bastante fácil de entender. Los observadores de propiedad están envueltos en un cierre. Hay dos tipos de observadores de propiedades:
-
willSet, que se invoca antes de que se establezca el nuevo valor de la propiedad -
didSet, que se invoca después de establecer el nuevo valor de la propiedad
En el ejemplo anterior, invocamos buildShoppingList() cada vez que se asigna un nuevo valor a items. Esto es lo
que parece la implementación de buildShoppingList().
1 |
// MARK: -
|
2 |
// MARK: Helper Methods
|
3 |
func buildShoppingList() { |
4 |
shoppingList = items.filter({ (item) -> Bool in |
5 |
return item.inShoppingList |
6 |
})
|
7 |
}
|
Filtramos
los elementos de la matriz items, incluidos solo los elementos
para los que inShoppingList se establece a true. El resultado se asigna a shoppingList.
También creamos un observador de propiedad didSet para la propiedad shoppingList. En
este observador de propiedades, actualizamos la vista de tabla para
reflejar el contenido de shoppingList, el origen de datos de la vista de
tabla.
1 |
var shoppingList = [Item]() { |
2 |
didSet { |
3 |
tableView.reloadData() |
4 |
}
|
5 |
}
|
Paso 2: mostrar la lista de compras
Implementar los métodos del protocolo UITableViewDataSource ya debería
ser un juego de niños. Eche un vistazo a la implementación a
continuación.
1 |
// MARK: -
|
2 |
// MARK: Table View Data Source Methods
|
3 |
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { |
4 |
return 1 |
5 |
}
|
6 |
|
7 |
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
8 |
return shoppingList.count |
9 |
}
|
10 |
|
11 |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
12 |
// Dequeue Reusable Cell
|
13 |
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) |
14 |
|
15 |
// Fetch Item
|
16 |
let item = shoppingList[indexPath.row] |
17 |
|
18 |
// Configure Table View Cell
|
19 |
cell.textLabel?.text = item.name |
20 |
|
21 |
return cell |
22 |
}
|
No olvide
declarar el identificador de reutilización de celda y registrar la clase
UITableViewCell como lo hicimos en la clase ListViewController.
1 |
import UIKit |
2 |
|
3 |
class ShoppingListViewController: UITableViewController { |
4 |
|
5 |
let CellIdentifier = "Cell Identifier" |
6 |
|
7 |
...
|
8 |
|
9 |
}
|
1 |
// MARK: -
|
2 |
// MARK: View Life Cycle
|
3 |
override func viewDidLoad() { |
4 |
super.viewDidLoad() |
5 |
|
6 |
// Register Class
|
7 |
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) |
8 |
}
|
Paso 3: carga de elementos
Como
expliqué anteriormente, la principal ventaja de utilizar la lista de
elementos para almacenar y crear la lista de compras es que la
aplicación almacena cada elemento en un solo lugar. Esto hace que la
actualización de elementos en la lista y en la lista de compras no sea
un dolor de cabeza. Los métodos loadItems() y pathForItems() son
idénticos a los que implementamos en la clase ListViewController.
1 |
private func loadItems() { |
2 |
if let filePath = pathForItems() where NSFileManager.defaultManager().fileExistsAtPath(filePath) { |
3 |
if let archivedItems = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Item] { |
4 |
items = archivedItems |
5 |
}
|
6 |
}
|
7 |
}
|
1 |
private func pathForItems() -> String? { |
2 |
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) |
3 |
|
4 |
if let documents = paths.first, let documentsURL = NSURL(string: documents) { |
5 |
return documentsURL.URLByAppendingPathComponent("items").path |
6 |
}
|
7 |
|
8 |
return nil |
9 |
}
|
Cuando te encuentres duplicando el código, deberías escuchar un timbre de alarma sonando. El código de duplicación no es problemático mientras está implementando una nueva característica. Después, sin embargo, debería considerar la refacturación del código para minimizar la cantidad de duplicaciones en la base de códigos de la aplicación. Este es un concepto muy importante en el desarrollo de software y a menudo se lo conoce como DRY, Do not Repeat Yourself. Chris Peters escribió un excelente artículo sobre Envato Tuts+ sobre programación DRY.
Antes de usar la clase ShoppingListViewController,
debemos actualizar el método viewDidLoad() de la clase. Además
de configurar el título del controlador de vista, también cargamos la
lista de elementos, que automáticamente rellena el arreglo shoppingList
como vimos anteriormente.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Set Title
|
5 |
title = "Shopping List" |
6 |
|
7 |
// Load Items
|
8 |
loadItems() |
9 |
|
10 |
// Register Class
|
11 |
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) |
12 |
}
|
El último paso es inicializar el controlador de vista de la lista de compras actualizando el guión gráfico. Esto
implica agregar una instancia de UITableViewController, configurar su
clase en ShoppingListViewController, incrustarla en un controlador de
navegación y crear una transición de relación entre el controlador de la
barra de pestañas y el controlador de navegación. Seleccione la vista de tabla del controlador de vista de lista de compras y establezca el número de celdas de prototipo en 0.



Ejecute la aplicación para ver si todo funciona correctamente. Por supuesto, la lista de compras está vacía y no tenemos forma de agregar artículos a la lista de compras. Vamos a remediar eso en el siguiente paso.
4. Agregar elementos a la lista de compras
Como
escribí anteriormente, la idea es agregar un artículo a la lista de
compras cuando se toca en el controlador de vista de lista. Para
mejorar la experiencia del usuario, si un artículo está presente en la
lista de compras, mostramos una marca de verificación verde a la
izquierda del nombre del artículo. Si
se toca un elemento que ya está en la lista de compras, se elimina de
la lista de compras y la marca de verificación verde desaparece. Esto
significa que tenemos que echar un vistazo al método tableView(_:didSelectRowAtIndexPath:) del protocolo UITableViewDelegate.
Antes
de implementar tableView(_:didSelectRowAtIndexPath:), descargue los
archivos fuente de esta lección. En la carpeta denominada Resources,
busque los archivos llamados checkmark.png y checkmark@2x.png. Agregue
ambos archivos al proyecto, porque los necesitamos en unos momentos.
En
la primera línea de tableView(_:didSelectRowAtIndexPath:), enviamos a
la vista de tabla un mensaje de deselectRowAtIndexPath(_:animated:) para anular la selección de la fila que tocó el usuario. Cada vez que se
toca una fila, solo se debe resaltar por un instante, por lo tanto,
esta adición. A
continuación, buscamos el artículo que corresponde con la selección del
usuario y actualizamos la propiedad inShoppingList del artículo (true pasa a ser false y viceversa).
1 |
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
2 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
3 |
|
4 |
// Fetch Item
|
5 |
let item = items[indexPath.row] |
6 |
|
7 |
// Update Item
|
8 |
item.inShoppingList = !item.inShoppingList |
9 |
|
10 |
// Update Cell
|
11 |
let cell = tableView.cellForRowAtIndexPath(indexPath) |
12 |
|
13 |
if item.inShoppingList { |
14 |
cell?.imageView?.image = UIImage(named: "checkmark") |
15 |
} else { |
16 |
cell?.imageView?.image = nil |
17 |
}
|
18 |
|
19 |
// Save Items
|
20 |
saveItems() |
21 |
}
|
Según el valor de la propiedad inShoppingList del elemento, mostramos u ocultamos la marca de verificación verde. Mostramos
la marca de verificación configurando la propiedad image de la
propiedad imageView de la celda de la vista de tabla. Una celda de vista de tabla incluye una vista de imagen a la izquierda
(una instancia de la clase UIImageView). Al establecer la propiedad image de la vista de imagen en nil, la
vista de la imagen está en blanco y no muestra ninguna imagen.
La
implementación de tableView(_:didSelectRowAtIndexPath:) finaliza
guardando la lista de elementos en el disco para asegurarse de que los
cambios sean permanentes.
¿Cómo sabe la lista de compras cuando el usuario toca un elemento en el controlador de vista de lista? El controlador de vista de lista de compras no actualizará automáticamente su vista de tabla si se realizó un cambio en la lista de elementos en el controlador de vista de lista. Para evitar el acoplamiento cerrado, no queremos que el controlador de vista de lista y el controlador de vista de lista de compras se comuniquen directamente entre sí.
Una solución a este problema es el uso de notificaciones. Cada vez que el controlador de vista de lista hace un cambio en la lista de elementos, publica una notificación con un nombre específico para el centro de notificaciones, un objeto que administra las notificaciones. Los objetos que están interesados en ciertas notificaciones pueden agregarse como observadores de esas notificaciones, lo que significa que pueden responder cuando esas notificaciones se envían al centro de notificaciones.
¿Cómo funciona todo esto? Hay tres pasos involucrados:
- El controlador de vista de lista de compras comienza
diciéndole al
centro de notificaciones que está interesado en recibir notificaciones
con un nombre de
ShoppingListDidChangeNotification - el controlador de vista de lista publica una notificación al centro de notificaciones cada vez que actualiza la lista de elementos
- cuando el controlador de vista de lista de compras recibe la notificación del centro de notificaciones, actualiza su fuente de datos y la vista de tabla en respuesta
Antes de implementar los tres pasos que
acabo de describir, es una
buena idea echar un vistazo más de cerca a la clase
NSNotificationCenter.
En pocas palabras, NSNotificationCenter gestiona la
transmisión de notificaciones. Los objetos en una aplicación pueden
registrarse en un centro de
notificaciones para recibir notificaciones usando addObserver(_:selector:name:object:) donde:
- el primer argumento es el objeto que recibirá las notificaciones (el observador)
-
selectores la acción que se invoca al observador cuando recibe la notificación -
namees el nombre de la notificación -
objectes el objeto que desencadena el envío de la notificación
Si el último argumento se establece en nil, el
observador recibe cada notificación con el nombre especificado.
Paso 1: recibir notificaciones
Vuelva
a visitar el método viewDidLoad() de la clase
ShoppingListViewController y agregue la instancia del controlador de
vista como un observador para recibir notificaciones con un nombre de
ShoppingListDidChangeNotification.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Set Title
|
5 |
title = "Shopping List" |
6 |
|
7 |
// Load Items
|
8 |
loadItems() |
9 |
|
10 |
// Register Class
|
11 |
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) |
12 |
|
13 |
// Add Observer
|
14 |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateShoppingList:", name: "ShoppingListDidChangeNotification", object: nil) |
15 |
}
|
La acción que se desencadena cuando el controlador de vista recibe una notificación con ese nombre es updateShoppingList(_:). El último parámetro, object, es nil, porque no importa qué objeto envió la notificación.
Paso 2: responder a las notificaciones
El método que se activa cuando el
observador recibe una notificación tiene un formato específico, como
puede ver a continuación. Acepta un argumento, el objeto de notificación
que es de tipo NSNotificación.
El
objeto de notificación mantiene una referencia al objeto que publicó la
notificación y también puede contener un diccionario con información
adicional. La implementación del método updateShoppingList(_:) es
bastante simple. Llamamos loadItems() al controlador de vista, lo que
significa que la lista de elementos se carga desde el disco. El resto
sucede automáticamente gracias a los observadores de propiedades que
implementamos anteriormente.
1 |
// MARK: -
|
2 |
// MARK: Notification Handling
|
3 |
func updateShoppingList(notification: NSNotification) { |
4 |
loadItems() |
5 |
}
|
Paso 3: envío de notificaciones
La tercera pieza del rompecabezas es publicar una notificación cada vez
que el controlador de vista de lista cambie la lista de elementos.
Podemos hacer esto en el método saveItems() de la clase
ListViewController.
1 |
private func saveItems() { |
2 |
if let filePath = pathForItems() { |
3 |
NSKeyedArchiver.archiveRootObject(items, toFile: filePath) |
4 |
|
5 |
// Post Notification
|
6 |
NSNotificationCenter.defaultCenter().postNotificationName("ShoppingListDidChangeNotification", object: self) |
7 |
}
|
8 |
}
|
Primero
solicitamos una referencia al centro de notificación predeterminado
llamando a defaultCenter() en la clase NSNotificationCenter. A
continuación, llamamos a postNotificationName(_:object:) en el
centro de notificaciones predeterminado, pasando el nombre de la
notificación, ShoppingListDidChangeNotification y el objeto que publica
la notificación.
Antes de compilar el proyecto,
asegúrese de modificar tableView(_:cellForRowAtIndexPath:) en
ListViewController.swift como se muestra a continuación para mostrar una
marca de verificación verde para los elementos que ya están presentes
en la lista de compras.
1 |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
2 |
// Dequeue Reusable Cell
|
3 |
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = items[indexPath.row] |
7 |
|
8 |
// Configure Table View Cell
|
9 |
cell.textLabel?.text = item.name |
10 |
cell.accessoryType = .DetailDisclosureButton |
11 |
|
12 |
if item.inShoppingList { |
13 |
cell.imageView?.image = UIImage(named: "checkmark") |
14 |
} else { |
15 |
cell.imageView?.image = nil |
16 |
}
|
17 |
|
18 |
return cell |
19 |
}
|
Ejecute la aplicación de la lista de compras para darle un giro. ¿Ha notado que las ediciones de un artículo se reflejan automáticamente en la lista de compras?



¿Listo para publicación?
Estupendo. ¿Dónde está el botón para publicar la aplicación de la lista de compras en la App Store? Todavía no hemos terminado. A pesar de que hemos sentado las bases de la aplicación de la lista de compras, no está lista para su publicación. También hay algunas cosas a considerar.
Escalabilidad
La aplicación de lista de compras es una implementación modesta de una lista de compras. Si la aplicación contiene cientos o miles de elementos, sería pertinente agregar funciones de búsqueda y secciones para ordenar los elementos alfabéticamente (como hicimos anteriormente en esta serie). Es importante darse cuenta de que la lista de elementos se escribe en el disco en su totalidad cada vez que se actualiza un elemento. Esto no es un problema cuando la lista es pequeña, pero lo será si la lista crece con el tiempo a cientos o miles de elementos.
Relaciones
Además, los usuarios pueden querer guardar más de una lista de compras. ¿Cómo lidiarías con eso? Una opción es almacenar cada lista de compras en un archivo separado, pero ¿cómo trataría los cambios realizados en los artículos? ¿Vas a actualizar cada lista de compras que contiene el artículo? Cuando empiece a tratar con relaciones, es mejor elegir un almacén de datos SQLite.
Core Data es un gran compañero si eliges seguir por ese camino. Es un marco potente y presenta muchas características que hacen que gran parte del código de nuestra aplicación de listas de compras quede obsoleto. Es cierto que Core Data trae consigo un poco más de gastos indirectos, por lo que es fundamental considerar primero si Core Data es adecuado para su aplicación, en otras palabras, si vale la pena la sobrecarga.
Características adicionales
También hay un gran potencial para funciones adicionales. La propiedad de precio de los artículos permanece sin usar en la implementación actual de la aplicación de la lista de compras. Además, sería bueno si el usuario pudiera marcar un ítem de la lista de compras tocando el ítem. Como puede ver, la implementación actual de la aplicación de la lista de compras es solo un comienzo modesto.
Conclusión
Aunque la aplicación de la lista de compras no está lista para la App Store, no puede negar que funciona según lo planeado y le ha mostrado varios aspectos nuevos del desarrollo de Cocoa, como notificaciones e implementación de un protocolo de delegado personalizado.
Ahora sabe qué esperar del iOS SDK y cómo es el desarrollo de iOS. Depende de usted decidir si desea continuar su viaje y convertirse en un desarrollador experto de iOS. Si eliges continuar con el desarrollo de iOS, entonces te proporcionaré algunos recursos geniales en la próxima y última entrega de esta serie.
Si tiene preguntas o comentarios, puede dejarlos en los comentarios a continuación o comunicarse conmigo en Twitter.



