Swift desde Cero: Control de Acceso y Observadores de Propiedad
() translation by (you can also view the original English article)
En el tutorial anterior, agregamos la habilidad de crear elementos por hacer. Mientras que esta adición ha hecho a la aplicación un poco más útil, también sería conveniente agregar la habilidad de marcar elementos como hechos y borrar elementos. Eso es en lo que nos enfocaremos en ese tutorial.
Prerrequisitos
Si quisieras seguir a la par conmigo, entonces asegúrate de que tienes Xcode 6.3 o superior instalado en tu máquina. Al momento de la escritura, Xcode 6.3 está en beta y disponible desde el iOS Dev Center de Apple para desarrolladores iOS registrados.
La razón para requerir Xcode 6.3 o superior es poder sacar ventaja de Swift 1.2, el cuál Apple presentó en Febrero. Swift 1.2 introduce un número de grandiosas adiciones de las que sacaremos ventaja en el resto de esta serie.
1. Borrando Elementos
Para borrar elementos, necesitamos implementar dos métodos adicionales del protocolo UITableViewDataSource
. Primero necesitamos decirle a la vista de tabla cuáles filas pueden ser editadas implementado el método tableView(_:canEditRowAtIndexPath:)
. Como puedes ver en el pedazo de código de abajo, la implementación es sencilla. Le decimos a la vista de tabla que cada fila es editable devolviendo true
.
1 |
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { |
2 |
return true |
3 |
}
|
El segundo método en el que estamos interesados es tableView(_:commitEditingStyle:forRowAtIndexPath:)
La implementación es un poco más compleja pero lo suficientemente fácil de comprender.
1 |
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { |
2 |
if editingStyle == .Delete { |
3 |
// Fetch Item
|
4 |
let item = self.items[indexPath.row] |
5 |
|
6 |
// Update Items
|
7 |
self.items.removeAtIndex(indexPath.row) |
8 |
|
9 |
// Update Table View
|
10 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right) |
11 |
}
|
12 |
}
|
Comenzamos revisando el valor de editingStyle
, una enumeración de tipo UITableViewCellEditingStyle
. Solo borramos un elemento si el valor de editingStyle
es igual a UITableViewCellEditingStyle.Delete
.
Swift es más inteligente que esa idea. Debido a que sabe que editingStyle
es de tipo UITableViewCellEditingStyle
, podemos omitir UITableViewCellEditingStyle
, el nombre de la enumeración, y escribir .Delete
, el valor miembro de la enumeración en la que estamos interesados. Si eres nuevo en las enumeraciones en Swift, entonces te recomiendo leer este consejo rápido sobre enumeraciones en Swift.
Después, recolectamos el elemento correspondiente de la propiedad items
y almacenamos temporalmente su valor en una constante llamada item
. Actualizamos los datos fuente de la vista de tabla, items
, invocando removeAtIndex(index: Int)
sobre la propiedad items
, pasando el índice correcto.
Finalmente, actualizamos la vista de tabla invocando deleteRowsAtIndexPaths(_:withRowAnimation:)
sobre tableView
, pasando un arreglo con indexPath
y .Right
para especificar el tipo de animación. Como vimos anteriormente, podemos omitir el nombre de la enumeración, UITableViewRowAnimation
, ya que Swift sabe que el tipo del segundo argumento es UITableViewRowAnimation
.
El usuario debería poder borrar elementos de la lista. Construye y ejecuta la aplicación para probar esto.
2. Marcando Elementos
Para marcar un elemento como terminado, vamos a agregar una marca de verificación a la fila correspondiente. Esto implica que necesitamos dar seguimiento a los elementos que el usuario ha marcado como terminados. Para ese propósito, declararemos una nueva propiedad que administra esto por nosotros. Declara una propiedad variable, checkedItems
, de tipo [String]
e inicializala con un arreglo vacío.
1 |
var checkedItems: [String] = [] |
En tableView(_:cellForRowAtIndexPath:)
, revisamos si chekedItems
contiene el elemento respectivo usando la función contains
, una función global definida en la Librería Estándar Swift. Pasamos chekedItems
como el primer argumento e item
como el segundo argumento. La función devuelve true
si checkedItems
contiene item
.
1 |
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
2 |
// Fetch Item
|
3 |
let item = self.items[indexPath.row] |
4 |
|
5 |
// Dequeue Table View Cell
|
6 |
let tableViewCell = tableView.dequeueReusableCellWithIdentifier("TableViewCell", forIndexPath: indexPath) as! UITableViewCell |
7 |
|
8 |
// Configure Table View Cell
|
9 |
tableViewCell.textLabel?.text = item |
10 |
|
11 |
if contains(self.checkedItems, item) { |
12 |
tableViewCell.accessoryType = .Checkmark |
13 |
} else { |
14 |
tableViewCell.accessoryType = .None |
15 |
}
|
16 |
|
17 |
return tableViewCell |
18 |
}
|
Si item
es encontrado en chekedItems
, establecemos la propiedad accessoryType
de la celda a .Checkmark
, un valor miembro de la enumeración UITableViewCellAccessoryType
. Si item
no es encontrado, caemos de vuelta a .None
como el tipo de accesorio de la celda.
El siguiente paso es agregar la habilidad de marcar un elemento como terminado implementando un método del protocolo UITableViewDelegate
, tableView(_:didSelectRowAtIndexPath:)
. En este método delegado, primero llamamos a deselectRowAtIndexPath(_:animated:)
sobre tableView
para deseleccionar la fila que tocó el usuario.
1 |
// MARK: Table View Delegate Methods
|
2 |
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
3 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = self.items[indexPath.row] |
7 |
|
8 |
// Fetch Table View Cell
|
9 |
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath) |
10 |
|
11 |
// Find Index of Item
|
12 |
let index = find(self.checkedItems, item) |
13 |
|
14 |
if let index = index { |
15 |
self.checkedItems.removeAtIndex(index) |
16 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None |
17 |
|
18 |
} else { |
19 |
self.checkedItems.append(item) |
20 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark |
21 |
}
|
22 |
}
|
Después recolectamos el elemento correspondiente de items
y una referencia a la celda que corresponde con la fila tocada. Usamos la función find
, definida en la Librería Estándar Swift, para obtener el índice de item
en chekedItems
. La función find
devuelve un opcional Int
. Si chekedItems
contiene item
, lo removemos de chekedItems
y establecemos el tipo de accesorio de la celda a .None
. Si chekedItems
no contiene item
, lo agregamos a chekedItems
y establecemos el tipo de accesorio de la celda a .Checkmark
.
Con estas adiciones, el usuario ahora puede marcar elementos como terminados. Construye y ejecuta la aplicación para asegurar que todo está funcionando como se espera.
3. Guardando Estado
La aplicación actualmente no guarda el estado entre lanzamientos. Para solucionar esto, vamos a almacenar los arreglos items
y checkedItems
en la base de datos de usuarios por defecto de la aplicación.
Paso 1: Estado de Carga
Comienza creando dos métodos de ayuda loadItems
y loadCheckedItems
. Nota la palabra clave private
prefijando cada método de ayuda. La palabra clave private
le dice a Swift que estos métodos son solo accesibles desde dentro de este archivo fuente.
1 |
// MARK: Private Helpers
|
2 |
private func loadItems() { |
3 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
4 |
|
5 |
if let items = userDefaults.objectForKey("items") as? [String] { |
6 |
self.items = items |
7 |
}
|
8 |
}
|
9 |
|
10 |
private func loadCheckedItems() { |
11 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
12 |
|
13 |
if let checkedItems = userDefaults.objectForKey("checkedItems") as? [String] { |
14 |
self.checkedItems = checkedItems |
15 |
}
|
16 |
}
|
La palabra clave private
es parte del control de acceso de Swift. Como el nombre implica, el control de acceso define qué código tiene acceso a qué código. Los niveles de acceso aplican a los métodos, funciones, tipos, etc. Apple simplemente se refiere a entidades. Hay tres niveles de acceso, público, interno y privado.
- Público: Entidades marcadas como públicas son accesibles por entidades definidas en el mismo módulo así como otros módulos. Este nivel de acceso es ideal para exponer la interfaz de un framework.
- Interno: Este es el nivel de acceso por defecto. En otras palabras, si no se especifica un nivel de acceso, este nivel de acceso aplica. Una entidad con un nivel de acceso interno es solo accesible por entidades definidas en el mismo módulo.
- Privado: Una entidad declarada como privada es solo accesible por entidades definidas en el mismo archivo fuente. Por ejemplo, los métodos privados de ayuda definidos en la clase
ViewController
son solo accesibles por la claseViewController
.
La implementación de los métodos de ayuda es simple si estás familiarizado con la clase NSUserDefaults
. Para facilidad de uso, almacenamos una referencia al objeto por defecto estándar de usuario en una constante llamada userDefaults
. En el caso de loadItems
, pedimos a userDefaults
el objeto asociado con la llave "items"
y la degradamos a un arreglo opcional de cadenas de texto. Desenvolvemos de manera segura el opcional, lo que significa que almacenamos el valor en la constante items
si el opcional no es nil
, y asignamos el valor de la propiedad items
.
Si la declaración if
luce confusa, entonces echa un vistazo a una versión más simple del método loadItems
en el siguiente ejemplo. El resultado es idéntico, la única diferencia es la concisión.
1 |
private func loadItems() { |
2 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
3 |
let storedItems = userDefaults.objectForKey("items") as? [String] |
4 |
|
5 |
if let items = storedItems { |
6 |
self.items = items |
7 |
}
|
8 |
}
|
La implementación de loadChekedItems
es idéntica excepto para la llave usada para cargar el objeto almacenado en la base de datos por defecto del usuario. Pongamos loadItems
y loadCheckedItems
en uso actualizando el método viewDidLoad
.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Set Title
|
5 |
self.title = "To Do" |
6 |
|
7 |
// Load State
|
8 |
self.loadItems() |
9 |
self.loadCheckedItems() |
10 |
|
11 |
// Register Class for Cell Reuse
|
12 |
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell") |
13 |
}
|
Paso 2: Guardando Estado
Para guardar estado, implementamos dos métodos privados de ayuda más, saveItems
y saveCheckedItems
. La lógica es similar a esa de loadItems
y loadCheckedItems
. La diferencia es que almacenamos datos en la base de datos por defecto de usuario. Asegúrate de que las llaves usadas en la clase setObject(_:forKey:)
concuerden con aquellas usadas en loadItems
y loadCheckedItems
.
1 |
private func saveItems() { |
2 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
3 |
|
4 |
// Update User Defaults
|
5 |
userDefaults.setObject(self.items, forKey: "items") |
6 |
userDefaults.synchronize() |
7 |
}
|
8 |
|
9 |
private func saveCheckedItems() { |
10 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
11 |
|
12 |
// Update User Defaults
|
13 |
userDefaults.setObject(self.checkedItems, forKey: "checkedItems") |
14 |
userDefaults.synchronize() |
15 |
}
|
La llamada synchronize
no es estrictamente necesaria. El sistema operativo se asegurará de que los datos que almacenas en la base de datos de usuario por defecto es escrita a disco en algún punto. Invocando synchronize
, sin embargo, le dices explícitamente al sistema operativo que escriba cualquier cambio pendiente a disco. Esto es útil durante el desarrollo, debido a que el sistema operativo no escribirá tus cambios al disco si matas la aplicación. Podría parecer como si algo no funcionara apropiadamente.
Necesitamos invocar saveItems
y SaveChekedItems
en un número de lugares. Para comenzar, llama a saveItems
cuando un nuevo elemento es agregado a la lista. Hacemos esto en el método delegado del protocolo AddItemViewControllerDelegate
.
1 |
// MARK: Add Item View Controller Delegate Methods
|
2 |
func controller(controller: AddItemViewController, didAddItem: String) { |
3 |
// Update Data Source
|
4 |
self.items.append(didAddItem) |
5 |
|
6 |
// Save State
|
7 |
self.saveItems() |
8 |
|
9 |
// Reload Table View
|
10 |
self.tableView.reloadData() |
11 |
|
12 |
// Dismiss Add Item View Controller
|
13 |
self.dismissViewControllerAnimated(true, completion: nil) |
14 |
}
|
Cuando el estado de un elemento cambia en el tableView(_:didSelectRowAtIndexPath:)
, actualizamos checkedItems
. Es una buena idea también invocar saveCheckedItems
en ese punto.
1 |
// MARK: Table View Delegate Methods
|
2 |
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
3 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = self.items[indexPath.row] |
7 |
|
8 |
// Fetch Table View Cell
|
9 |
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath) |
10 |
|
11 |
// Find Index of Item
|
12 |
let index = find(self.checkedItems, item) |
13 |
|
14 |
if let index = index { |
15 |
self.checkedItems.removeAtIndex(index) |
16 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None |
17 |
|
18 |
} else { |
19 |
self.checkedItems.append(item) |
20 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark |
21 |
}
|
22 |
|
23 |
// Save State
|
24 |
self.saveCheckedItems() |
25 |
}
|
Cuando un elemento es borrado, ambos item
y checkedItems
son actualizados. Para guardar este cambio, llamamos tanto a saveItems
como a saveCheckedItems
.
1 |
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { |
2 |
if editingStyle == .Delete { |
3 |
// Fetch Item
|
4 |
let item = self.items[indexPath.row] |
5 |
|
6 |
// Update Items
|
7 |
self.items.removeAtIndex(indexPath.row) |
8 |
|
9 |
if contains(self.checkedItems, item) { |
10 |
self.checkedItems.removeAtIndex(indexPath.row) |
11 |
}
|
12 |
|
13 |
// Save State
|
14 |
self.saveItems() |
15 |
self.saveCheckedItems() |
16 |
|
17 |
// Update Table View
|
18 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Right) |
19 |
}
|
20 |
}
|
Eso es. Construye y ejecuta la aplicación para probar tu trabajo. Juega con la aplicación y fuerza su cierre. Cuando lanzas la aplicación de nuevo, el último estado conocido debería estar cargado y visible.
4. Observadores de Propiedad
La experiencia de usuario de la aplicación es un poco carente de momento. Cuando todo elemento es borrado o cuando la aplicación es lanzada por primera vez, el usuario ve una vista de tabla vacía. Esto no es grandioso. Podemos resolver esto mostrando un mensaje cuando no hay elementos. Esto también me dará la oportunidad de mostrarte otra característica de Swift, observadores de propiedad.
Paso 1: Agregando una Etiqueta
Comencemos agregando una etiqueta a la interfaz de usuario para mostrar el mensaje. Declara un outlet llamado messageLabel
de tipo UILabel
en la clase ViewController
, abre Main.storyboard, y agrega una etiqueta a la vista del controlador de vista.
1 |
@IBOutlet var messageLabel: UILabel! |
Agrega las restricciones de retícula necesarias a la etiqueta y conéctala con el outlet messageLabel
del controlador de vista en el Inspector de Conexiones. Establece el texto de la etiqueta a No tienes nada por hacer. y centra el texto de la etiqueta en el Inspector de Atributos.



Paso 2: Implementando un Observador de Propiedad
La etiqueta de mensaje debería solo ser visible si items
no contiene elementos. Cuando eso suceda, deberíamos también ocultar la vista de tabla. Podríamos resolver este problema agregando varias revisiones en la clase ViewController
, peor una aproximación más conveniente y elegante es usar un observador de propiedad.
Como el nombre implica, los observadores de propiedad observan una propiedad. Un observador de propiedad es invocado siempre que una propiedad cambia, incluso cuando el nuevo valor es el mismo que el valor antiguo. Hay dos tipos de observadores de propiedad.
-
willSet
: invocado antes de que el valor haya cambiado. -
didSet
: invocado después de que el valor haya cambiado
Para nuestro propósito, implementaremos el observador didSet
para la propiedad items
. Echa un vistazo a la sintaxis del siguiente pedazo de código.
1 |
var items: [String] = [] { |
2 |
didSet { |
3 |
let hasItems = items.count > 0 |
4 |
self.tableView.hidden = !hasItems |
5 |
self.messageLabel.hidden = hasItems |
6 |
}
|
7 |
}
|
La construcción podría verse un poco extraña al inicio así que déjame explicar qué está sucediendo. Cuando el observador didSet
es invocado, después de que la propiedad items
es cambiada, revisamos si la propiedad items
contiene cualquier elemento. Basado en el valor de la constante hasItems
, actualizamos la interfaz de usuario. Es tan simple como eso.
Al observador didSet
se le pasa un parámetro constante que contiene el valor del valor antiguo de la propiedad. Es omitido en el ejemplo de arriba, porque no lo necesitamos en nuestra implementación. El siguiente ejemplo muestra cómo podría ser usado.
1 |
var items: [String] = [] { |
2 |
didSet(oldValue) { |
3 |
if oldValue != items { |
4 |
let hasItems = items.count > 0 |
5 |
self.tableView.hidden = !hasItems |
6 |
self.messageLabel.hidden = hasItems |
7 |
}
|
8 |
}
|
9 |
}
|
El parámetro oldValue
en el ejemplo no tiene un tipo explícito, debido a que Swift sabe el tipo de la propiedad items
. En el ejemplo, solo actualizamos la interfaz de usuario di el valor antiguo difiere del nuevo valor.
Un observador willSet
funciona de manera similar. La diferencia principal es que el parámetro pasado al observador willSet
es una constante conteniendo el nuevo valor de la propiedad. Cuando uses observadores de propiedad, ten en cuenta que no son invocados cuando la instancia es iniciada.
Construye y ejecuta la aplicación para asegurar que todo está enganchado correctamente. Aunque la aplicación no es perfecta y podríamos usar unas cuantas características más, has creado tu primera aplicación iOS usando Swift.
Conclusión
A lo largo del curso de las tres últimas lecciones de esta serie, creaste una aplicación iOS funcional usando características orientadas a objetos de Swift. Si tienes alguna experiencia programando y desarrollando aplicaciones, entonces deberías haber notado que el modelo actual de datos tiene unas cuantos defectos, por decirlo ligeramente. Almacenar elementos como cadenas y crear un arreglo separado para almacenar un estado de elemento no es una buena idea si estás construyendo una aplicación apropiada. Una mejor aproximación sería crear una clase separada ToDo
para modelar elementos y almacenarlos en la caja de arena de la aplicación. Esa será nuestra siguiente meta para la siguiente entrega de esta serie.