Advertisement
  1. Code
  2. Mobile Development
  3. iOS Development

Swift desde Cero: Control de Acceso y Observadores de Propiedad

Scroll to top
Read Time: 13 min
This post is part of a series called Swift From Scratch.
Swift From Scratch: Delegation and Properties
Swift From Scratch: Initialization and Initializer Delegation

() 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 UITableViewDelegatetableView(_: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 loadItemsloadCheckedItems. 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 clase ViewController.

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 loadItemsloadCheckedItems 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 loadItemsloadCheckedItems. 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 itemcheckedItems 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.