iOS desde cero con Swift: creación de una aplicación de lista de compras 1
() translation by (you can also view the original English article)
En
las próximas dos lecciones, pondremos en práctica lo que aprendimos en
esta serie al crear una aplicación de lista de compras. En
el camino, también aprenderá una serie de conceptos y patrones nuevos,
como crear una clase de modelo personalizada e implementar un patrón de
delegacion personalizado. Tenemos mucho camino por recorrer, así que
comencemos.
Contornos
La aplicación de la lista de compras que vamos a crear tiene dos funciones, administrar una lista de artículos y crear una lista de compras seleccionando elementos de la lista.
Desarrollaremos la aplicación con un controlador de barra de pestañas para hacer que el cambio entre las dos vistas sea rápido y directo. En esta lección, nos enfocamos en la primera característica. En la próxima lección, terminamos esta característica y ampliamos la lista de compras, la segunda característica de la aplicación.
Aunque la aplicación de la lista de compras no es complicada desde la perspectiva del usuario, hay varias decisiones que deben tomarse durante su desarrollo. ¿Qué tipo de tienda usamos para almacenar la lista de artículos? ¿Puede el usuario agregar, editar y eliminar elementos? Estas son preguntas que abordamos en las próximas dos lecciones.
En esta lección, también le muestro cómo sembrar la aplicación de lista de compras con datos ficticios para darles a los usuarios nuevos algo para empezar. Sembrar una aplicación con datos a menudo es una buena idea para ayudar a los nuevos usuarios a ponerse al día rápidamente.
1. Creando el Proyecto
Inicie Xcode y cree un nuevo proyecto basado en la plantilla Single View Application en la sección iOS > Application.



Nombre Shopping List del proyecto e ingrese el nombre e identificador de la organización. Establezca Language a Swift y Devices a iPhone. Asegúrese de que las casillas de verificación en la parte inferior estén desmarcadas. Dile a Xcode dónde guardar el proyecto y haz clic en Create.



2. Creando el Controlador List View
Como era
de esperar, el controlador de vista de lista va a ser una subclase de
UITableViewController
. Cree una nueva clase seleccionando New > File... en el menú File. Selecciona la Clase Cocoa Touch desde la
sección iOS > Source.



Denomine a la clase ListViewController
y
conviértala en una subclase de UITableViewController
. Deje
la casilla de verificación. También create XIB file sin marcar y
asegúrese de que Languages esté configurado a Swift. Dile a Xcode
dónde quieres guardar la clase y haz clic en Create.



Abra Main.storyboard, seleccione el controlador de vista que ya está presente y elimínelo. Arrastre
una instancia de UITabBarController
desde la Object Library y
elimine los dos controladores de vista que están vinculados al
controlador de la barra de pestañas. Arrastre
un UITableViewController
desde la Object Library, establezca su
clase en ListViewController
en el Identity Inspector y cree una
transición de relación desde el controlador de la barra de pestañas al
controlador de vista de lista.
Seleccione el controlador de la barra de pestañas, abra el Attributes Inspector y conviértalo en el controlador de vista inicial del guion gráfico marcando la casilla de verificación Is Initial View Controller.



El controlador de vista de lista debe ser el controlador de vista raíz de un controlador de navegación. Seleccione el controlador de vista de lista y elija Embed In > Navigation Controller en el menú Editor.



Seleccione la vista de tabla del controlador de vista de lista y establezca Prototype Cells en el Attributes Inspector en 0.



Ejecute la aplicación en el simulador para ver si todo está configurado correctamente. Debería ver una vista de tabla vacía con una barra de navegación en la parte superior y una barra de pestañas en la parte inferior.
3. Creación de la clase Item Model
¿Cómo vamos a trabajar con los artículos en la aplicación de la lista de compras? En otras palabras, ¿qué tipo de objeto usamos para almacenar las propiedades de un artículo, como su nombre, precio y una cadena que identifica de manera única cada elemento?
La opción más obvia es almacenar las propiedades del elemento en un diccionario. Aunque esto funcionaría muy bien, limitaría y ralentizaría severamente a medida que la aplicación ganara en complejidad.
Para la aplicación de la lista de compras, vamos a crear una clase de modelo personalizada. Requiere un poco más de trabajo para configurar, pero facilitará el desarrollo mucho más adelante.
Cree una nueva clase, Item
, y conviértala en una
subclase de NSObject
. Dile a Xcode dónde guardar la clase y haz clic en
Create.
Propiedades
Abra Item.swift y declare cuatro propiedades:
-
uuid
de tipoString
para identificar de forma única cada elemento -
name
de tipoString
-
price
de tipoFloat
-
inShoppingList
de tipoBool
para indicar si el artículo está presente en la lista de compras
Es
esencial que la clase Item
se ajuste al protocolo NSCoding
. La razón de
esto se aclarará en unos momentos. Eche un vistazo a lo que tenemos
hasta ahora. Los comentarios son omitidos.
1 |
import UIKit |
2 |
|
3 |
class Item: NSObject { |
4 |
|
5 |
var uuid: String = NSUUID().UUIDString |
6 |
var name: String = "" |
7 |
var price: Float = 0.0 |
8 |
var inShoppingList = false |
9 |
|
10 |
}
|
Cada propiedad debe tener un valor inicial. Configuramos name
en una cadena vacía, el price
en 0.0
, y en inShoppingList
en false
. Para establecer el valor inicial de uuid
, usamos
una clase que no hemos visto antes, NSUUID
. Esta clase nos ayuda a crear
una cadena única o UUID. Inicializamos una instancia de la clase y le
pedimos el UUID
como una cadena invocando a UUIDString()
.
Pruébelo
agregando el siguiente fragmento de código al método viewDidLoad()
de
la clase ListViewController
.
1 |
let item = Item() |
2 |
print(item.uuid) |
Ejecute la aplicación y eche un vistazo a la salida en la consola de Xcode para ver cómo se ve el UUID resultante. Debería ver algo como esto:
1 |
C6B81D40-0528-4D2C-BB58-6EF78D3D3DEF |
Archivando
Una
estrategia para guardar objetos personalizados en el disco, como las
instancias de la clase Item
, es a través de un proceso conocido como
archividando. Usaremos NSKeyedArchiver
y NSKeyedUnarchiver
para archivar y
desarchivar instancias de la clase Item
.
El prefijo de clase, NS
, indica
que ambas clases están definidas en el marco Foundation. La clase
NSKeyedArchiver
toma un conjunto de objetos y los almacena en el disco
como datos binarios. Un
beneficio adicional de este enfoque es que los archivos binarios
generalmente son más pequeños que los archivos de texto sin formato que
contienen la misma información.
Si
queremos usar NSKeyedArchiver
y NSKeyedUnarchiver
para archivar y
desarchivar instancias de la clase Item
, Item
debe adoptar el protocolo
NSCoding
. Comencemos actualizando la clase Item
para decirle al
compilador que Item
adopta el protocolo NSCoding
.
1 |
import UIKit |
2 |
|
3 |
class Item: NSObject, NSCoding { |
4 |
|
5 |
...
|
6 |
|
7 |
}
|
Recuerde
de la lección sobre el marco de Foundation, el protocolo NSCoding
declara dos métodos que una clase debe implementar para permitir que las
instancias de la clase sean codificadas y decodificadas. Veamos cómo
funciona esto.
Codificación
Si
crea clases personalizadas, entonces usted es responsable de
especificar cómo deben codificarse las instancias de esa clase,
convertidas en datos binarios. En
encodeWithCoder(_:)
, la clase que se ajusta al protocolo NSCoding
especifica cómo se deben codificar las instancias de la clase. Eche un
vistazo a la implementación a continuación. Las claves que usamos no son
tan importantes, pero generalmente debes usar los nombres de las
propiedades para mayor claridad.
1 |
func encodeWithCoder(coder: NSCoder) { |
2 |
coder.encodeObject(uuid, forKey: "uuid") |
3 |
coder.encodeObject(name, forKey: "name") |
4 |
coder.encodeFloat(price, forKey: "price") |
5 |
coder.encodeBool(inShoppingList, forKey: "inShoppingList") |
6 |
}
|
Descodificación
Siempre que un objeto codificado deba
convertirse a una instancia de la clase respectiva, se invoca a init(coder:)
. Las mismas claves que usamos en encodeWithCoder(_:)
se usan en init(coder:)
. Esto es muy
importante.
1 |
required init?(coder decoder: NSCoder) { |
2 |
super.init() |
3 |
|
4 |
if let archivedUuid = decoder.decodeObjectForKey("uuid") as? String { |
5 |
uuid = archivedUuid |
6 |
}
|
7 |
|
8 |
if let archivedName = decoder.decodeObjectForKey("name") as? String { |
9 |
name = archivedName |
10 |
}
|
11 |
|
12 |
price = decoder.decodeFloatForKey("price") |
13 |
inShoppingList = decoder.decodeBoolForKey("inShoppingList") |
14 |
}
|
Tenga en cuenta que usamos la palabra clave
required
. Recuerde que cubrimos la palabra clave required
anteriormente en esta serie. Como decodeObjectForKey(_:)
devuelve un
objeto de tipo AnyObject?
, lo convertimos en un objeto String
.
Nunca
debe llamar directamente a init(coder:)
y encodeWithCoder(_:)
. Solo son llamados por el sistema operativo. Al
conformar la clase Item
con el protocolo NSCoding
, solo le decimos al
sistema operativo cómo codificar y decodificar las instancias de la
clase.
Creando instancias
Para
facilitar la creación de nuevas instancias de la clase Item
, creamos un
inicializador personalizado que acepta un nombre y un precio. Esto es
opcional, pero facilitará el desarrollo, como verá más adelante en esta
lección.
Abra Item.swift y agregue el siguiente inicializador. Recuerde
que los inicializadores no tienen la palabra clave func
delante de su
nombre. Primero invocamos el inicializador de la superclase, NSObject
. Luego establecemos las propiedades name
y price
de la instancia Item
.
1 |
init(name: String, price: Float) { |
2 |
super.init() |
3 |
|
4 |
self.name = name |
5 |
self.price = price |
6 |
}
|
Usted
se estará preguntando por qué usamos self.name
en init(name:price:)
y
name
en init(coder:)
para establecer la propiedad name
. En ambos contextos, self
referencia la instancia Item
con la que
estamos interactuando. En Swift, puede acceder a una propiedad sin
utilizar la palabra clave self
. Sin
embargo, en init(name:price:)
uno de los parámetros tiene un nombre
que es idéntico a una de las propiedades de la clase Item
. Para evitar
confusiones, utilizamos la palabra clave self
. En resumen, puede omitir
la palabra clave self
para acceder a una propiedad a menos que haya
motivos de confusión.
4. Cargando y guardando artículos
La
persistencia de los datos va a ser clave en nuestra aplicación de la
lista de compras, así que echemos un vistazo a cómo vamos a implementar
la persistencia de datos. Abra ListViewController.swift y declare
una almacenada propiedad variable, items
, elementos de tipo [Item]
. Tenga en
cuenta que el valor inicial de items
es una matriz
vacía.
1 |
import UIKit |
2 |
|
3 |
class ListViewController: UITableViewController { |
4 |
|
5 |
var items = [Item]() |
6 |
|
7 |
...
|
8 |
|
9 |
}
|
Los elementos
que se muestran en la vista de tabla del controlador de vista se
almacenarán en items
. Es importante que items
sea una
matriz mutable, por eso, la palabra clave var
. ¿Por qué? Añadiremos
la posibilidad de agregar nuevos elementos un poco más adelante en esta
lección.
En
el inicializador de la clase, cargamos la lista de elementos del disco y
la almacenamos en la propiedad items
que declaramos hace unos
momentos.
1 |
// MARK: -
|
2 |
// MARK: Initialization
|
3 |
required init?(coder decoder: NSCoder) { |
4 |
super.init(coder: decoder) |
5 |
|
6 |
// Load Items
|
7 |
loadItems() |
8 |
}
|
}El
método loadItems()
del controlador de vista no es más que un método
auxiliar para mantener el método init?(coder:)
conciso y
legible. Echemos un vistazo a la implementación de loadItems()
.
Cargando artículos
El método loadItems()
comienza con la búsqueda de la ruta del
archivo en el que se almacena la lista de elementos. Hacemos esto
llamando a pathForItems()
, otro método de ayuda que veremos en unos
momentos. Como pathForItems()
devuelve una opción de tipo String?
, vinculamos el resultado a una constante, filePath
. Discutimos el enlace
opcional anteriormente en esta serie.
1 |
// MARK: -
|
2 |
// MARK: Helper Methods
|
3 |
private func loadItems() { |
4 |
if let filePath = pathForItems() where NSFileManager.defaultManager().fileExistsAtPath(filePath) { |
5 |
if let archivedItems = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Item] { |
6 |
items = archivedItems |
7 |
}
|
8 |
}
|
9 |
}
|
Lo que no cubrimos aún
es la palabra clave where
en una declaración if
. Al usar la palabra
clave where
, agregamos una restricción adicional a la condición de la
instrucción if
. En loadItems()
, nos aseguramos de que pathForItems()
devuelva un String
. Usando una cláusula where
, también verificamos que
el valor de filePath
corresponde a un archivo en el disco. Usamos la
clase NSFileManager
para esto.
NSFileManager
es una clase con la que aún
no hemos trabajado. Proporciona una API fácil de usar para trabajar con
el sistema de archivos. Obtenemos una referencia a una instancia de la
clase pidiéndole el administrador predeterminado.
Luego
invocamos fileExistsAtPath(_:)
en el administrador predeterminado,
pasando la ruta del archivo que obtuvimos en la primera línea de
loadItems()
. Si
existe un archivo en la ubicación especificada por la ruta del archivo,
cargamos el contenido del archivo en la propiedad items
. Si
no existe ningún archivo en esa ubicación, la propiedad items
conserva su valor inicial, una matriz vacía.
La carga del contenido del
archivo se realiza a través de la clase NSKeyedUnarchiver
. Puede
leer los datos binarios contenidos en el archivo y convertirlo en un
gráfico de objetos, una matriz de instancias Item
. Este proceso
será más claro cuando analicemos el método saveItems()
en un
minuto.
Echemos un vistazo a pathForItems()
, el método de ayuda que
invocamos anteriormente. Primero buscamos la ruta del directorio Documents en la zona de pruebas de la aplicación. Este paso debería ser
familiar por ahora.
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.plist").path |
6 |
}
|
7 |
|
8 |
return nil |
9 |
}
|
El método devuelve la ruta al archivo
que contiene la lista de elementos de la aplicación. Hacemos
esto buscando la ruta del directorio Documents de la zona sandbox y agregando "items"
a ella.
La
ventaja de utilizar URLByAppendingPathComponent(_:)
es que la
inserción de separadores de ruta se realiza para nosotros siempre que
sea necesario. En otras palabras, el sistema se asegura de que recibamos
una URL de archivo válida. Tenga en cuenta que invocamos a path()
en
la instancia de NSURL
resultante para asegurarnos de que devolvemos un
objeto String
.
Guardar elementos
Aunque
no vamos a guardar elementos hasta más adelante en esta lección, es una
buena idea implementarlo mientras estamos en ello. La implementación de
saveItems()
es muy concisa gracias al método de ayuda pathForItems()
.
Primero
buscamos la ruta al archivo que contiene la lista de elementos de la
aplicación y luego escribimos el contenido de la propiedad items
en esa ubicación. Fácil. ¿no?
1 |
private func saveItems() { |
2 |
if let filePath = pathForItems() { |
3 |
NSKeyedArchiver.archiveRootObject(items, toFile: filePath) |
4 |
}
|
5 |
}
|
El proceso de escribir un gráfico de objetos en
el disco se conoce como archivo. Usamos la clase NSKeyedArchiver
para
lograr esto llamando a archiveRootObject(_:toFile:)
en NSKeyedArchiver
.
Durante
este proceso, cada objeto en el gráfico del objeto se envía un mensaje
de encodeWithCoder(_:)
para convertirlo en datos binarios. Recuerde
que rara vez es necesario llamar directamente a encodeWithCoder(_:)
.
Para
verificar que la carga de la lista de elementos del disco funciona,
agregue una declaración de impresión al método viewDidLoad()
de la
clase ListViewController
. Ejecute la aplicación en el simulador y
verifique si todo está funcionando.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
print(items) |
5 |
}
|
Si echas un
vistazo a la salida en la consola de Xcode, verás que la propiedad items
es igual a una matriz vacía. Eso es lo que esperamos en este punto. Lo
importante es que items
no son iguales a nil
. En el siguiente
paso, le daremos al usuario algunos elementos para trabajar, un proceso
conocido como seeding (siembra).
5. Seeding el almacén de datos
Sembrar una aplicación con datos a menudo puede significar la diferencia entre un usuario comprometido y un usuario que abandona la aplicación después de usarla durante menos de un minuto. La creación de una aplicación con datos ficticios no solo ayuda a los usuarios a ponerse al día, sino que también muestra a los nuevos usuarios cómo se ve y se siente la aplicación con los datos que contiene.
Sembrar la aplicación de la lista de compras con una lista inicial de artículos no es difícil. Debido a que no queremos crear elementos duplicados, lo verificamos durante el lanzamiento de la aplicación si el data store ya ha sido sembrado con datos. Si aún no se ha sembrado el almacén de datos, cargamos una lista con datos iniciales y usamos esa lista para crear el almacén de datos de la aplicación.
La
lógica para sembrar el almacén de datos se puede invocar desde varias
ubicaciones en una aplicación, pero es importante pensar en el futuro. Podríamos
poner la lógica para sembrar el almacén de datos en la clase
ListViewController
, pero ¿qué ocurre si, en una versión futura de la
aplicación, otros controladores de vista también tienen acceso a la
lista de elementos? Un mejor lugar para sembrar el almacén de datos está
en la clase AppDelegate
. Veamos cómo funciona esto.
Abra
AppDelegate.swift y modifique la implementación de application(_:didFinishLaunchingWithOptions:)
para que se vea como la que se muestra a
continuación.
1 |
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { |
2 |
// Seed Items
|
3 |
seedItems() |
4 |
|
5 |
return true |
6 |
}
|
La única diferencia con la implementación anterior es que
primero invocamos seedItems()
. Es
importante que el almacenamiento de datos se realice antes de la
inicialización de cualquiera de los controladores de vista, ya que el
almacén de datos debe ser sembrado antes de que cualquiera de los
controladores de vista cargue la lista de elementos.
La implementación de
seedItems()
no es complicada. Comenzamos
almacenando una referencia al objeto predeterminado de usuario
compartido y luego verificamos si la base de datos predeterminada del
usuario tiene una entrada para una clave con el nombre
"UserDefaultsSeedItems"
y si esta entrada es booleana con un valor true
.
1 |
// MARK: -
|
2 |
// MARK: Helper Methods
|
3 |
private func seedItems() { |
4 |
let ud = NSUserDefaults.standardUserDefaults() |
5 |
|
6 |
if !ud.boolForKey("UserDefaultsSeedItems") { |
7 |
if let filePath = NSBundle.mainBundle().pathForResource("seed", ofType: "plist"), let seedItems = NSArray(contentsOfFile: filePath) { |
8 |
// Items
|
9 |
var items = [Item]() |
10 |
|
11 |
// Create List of Items
|
12 |
for seedItem in seedItems { |
13 |
if let name = seedItem["name"] as? String, let price = seedItem["price"] as? Float { |
14 |
// Create Item
|
15 |
let item = Item(name: name, price: price) |
16 |
|
17 |
// Add Item
|
18 |
items.append(item) |
19 |
}
|
20 |
}
|
21 |
|
22 |
if let itemsPath = pathForItems() { |
23 |
// Write to File
|
24 |
if NSKeyedArchiver.archiveRootObject(items, toFile: itemsPath) { |
25 |
ud.setBool(true, forKey: "UserDefaultsSeedItems") |
26 |
}
|
27 |
}
|
28 |
}
|
29 |
}
|
30 |
}
|
La clave puede ser lo que quiera siempre que sea coherente al nombrar las teclas que utiliza. La clave en la base de datos predeterminada del usuario nos dice si la aplicación ya se ha sembrado o no con datos. Esto es importante ya que solo queremos sembrar la aplicación una vez.
Si aún no se ha sembrado la aplicación, cargamos una lista de propiedades del paquete de aplicaciones, seed.plist. Este archivo contiene una matriz de diccionarios con cada diccionario que representa un elemento con un nombre y un precio.
Antes
de iterar a través de la matriz seedItems
, creamos una matriz mutable
para almacenar las instancias Item
que estamos a punto de crear. Para
cada diccionario en la matriz seedItems
, creamos una instancia de Item
invocando el inicializador que declaramos anteriormente en esta lección. Cada elemento se agrega a la matriz items
.
Finalmente,
creamos la ruta al archivo en el que almacenaremos la lista de
elementos y escribimos el contenido de la matriz items
en el
disco como vimos en el método saveItems()
de ListViewController
.
El
método archiveRootObject(_:toFile:)
devuelve true
si la
operación finalizó correctamente y solo entonces actualizamos la base de
datos predeterminada del usuario estableciendo el valor booleano para
la clave "UserDefaultsSeedItems"
en true
. La próxima vez que se
inicie la aplicación, el data store no se volverá a
sembrar.
Probablemente hayas notado que utilizamos otro método auxiliar
en seedItems()
, pathForItems()
. Su implementación es idéntica a la de
la clase ListViewController
.
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 |
}
|
Antes de ejecutar la aplicación, asegúrese de copiar la lista de propiedades, seed.plist, a su proyecto. No importa dónde lo almacene, siempre que esté incluido en el paquete de la aplicación.
Ejecute la aplicación e inspeccione el resultado en la consola para ver si el data store fue sembrado exitosamente con el contenido de seed.plist. Tenga en cuenta que sembrar un almacén de datos con datos o actualizar una base de datos lleva tiempo. Si la operación lleva demasiado tiempo, el sistema puede matar su aplicación antes de que haya tenido la oportunidad de finalizar el lanzamiento. Apple se refiere a este evento como el watchdog mata su aplicación.
Su aplicación tiene una cantidad limitada de tiempo para su lanzamiento. Si no se ejecuta dentro de ese margen de tiempo, el sistema operativo mata su aplicación. Esto significa que debe considerar cuidadosamente cuándo y dónde lleva a cabo ciertas operaciones, como sembrar el almacén de datos de su aplicación.
6. Mostrar la lista de artículos
Ahora tenemos una lista de
elementos para trabajar. Mostrar los elementos en la vista de tabla del
controlador de vista de lista no es difícil. Eche un vistazo a la
implementación de los tres métodos del protocolo UITableViewDataSource
que se muestra a continuación. Las implementaciones deberían ser
familiares si ha leído el tutorial sobre las vistas de
tabla.
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 items.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 = items[indexPath.row] |
17 |
|
18 |
// Configure Table View Cell
|
19 |
cell.textLabel?.text = item.name |
20 |
|
21 |
return cell |
22 |
}
|
Hay
dos detalles que debemos tener en cuenta antes de ejecutar la
aplicación, declarar el CellIdentifier
constante y decirle a la vista de
tabla qué clase usar para crear celdas de vista de
tabla.
1 |
import UIKit |
2 |
|
3 |
class ListViewController: UITableViewController { |
4 |
|
5 |
let CellIdentifier = "Cell Identifier" |
6 |
|
7 |
...
|
8 |
|
9 |
}
|
Mientras está en ello, establezca la propiedad title
del controlador de vista de lista en "Items"
.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
title = "Items" |
5 |
|
6 |
// Register Class
|
7 |
tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) |
8 |
}
|
Ejecute la aplicación en el simulador. Esto es lo que deberías ver en el simulador.



7. Agregar elementos - Parte 1
No importa qué tan bien elaboremos la lista de elementos semilla, el usuario seguramente querrá agregar elementos adicionales a la lista. En iOS, un enfoque común para agregar nuevos elementos a una lista es presentar al usuario un controlador de vista modal en el que se pueden ingresar nuevos datos. Esto significa que necesitaremos:
- agregue un botón a la interfaz de usuario para agregar nuevos elementos
- crear un controlador de vista que administre la vista que acepta la entrada del usuario
- crear un nuevo artículo basado en la entrada del usuario
- agregue el elemento recién creado a la vista de tabla
Paso 1: Agregar un botón
Agregar un botón a la barra de navegación requiere una línea de código. Revise el método viewDidLoad()
de la clase ListViewController
y
actualícelo para reflejar la implementación a
continuación.
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 |
}
|
En la
lección sobre controladores de barra de pestañas, aprendió que cada
controlador de vista tiene una propiedad tabBarItem
. De
forma similar, cada controlador de vista tiene una propiedad navigationItem
, una instancia única de UINavigationItem
que
representa el controlador de vista en la barra de navegación del
controlador de vista principal: el controlador de navegación.
La
propiedad navigationItem
tiene una propiedad leftBarButtonItem
, una
instancia de UIBarButtonItem
, que hace referencia al elemento del botón
de barra que se muestra en el lado izquierdo de la barra de navegación. La propiedad navigationItem
también tiene una propiedad titleView
y una
propiedad RightBarButtonItem
.
En
viewDidLoad()
, establecemos la propiedad leftBarButtonItem
del
elemento navigationItem
del controlador de vista en una instancia de
UIBarButtonItem
invocando init(barButtonSystemItem:target:action:)
, pasando .Add
como primer argumento. El primer argumento es de tipo
UIBarButtonSystemItem
, una enumeración. El resultado es una instancia
proporcionada por el sistema de UIBarButtonItem
.
Aunque
ya hemos encontrado el patrón de acción del objetivo, el segundo y
tercer parámetro de init(barButtonSystemItem:target:action:)
necesita una explicación. Cada
vez que se toca el botón en la barra de navegación, se envía un mensaje
deaddItem(_:)
al target
, es decir, self
o la instancia ListViewController
.
Como dije, ya hemos encontrado el patrón de acción del objetivo cuando conectamos el evento táctil de un botón a una acción en el storyboard. Esto es muy similar, la única diferencia es que la conexión se realiza mediante programación.
El patrón de acción objetivo es un patrón
común en Cocoa. La idea es simple. Un objeto mantiene una referencia al
mensaje que debe enviarse y al objetivo, un objeto que actúa como
receptor de ese mensaje.
El mensaje se almacena como un selector. Espera un minuto. ¿Qué es un selector? Un selector es el nombre o el identificador único que se utiliza para seleccionar un método que se espera que ejecute un objeto. Puede leer más sobre los selectores en la guía Cocoa Core Competencies.
Antes
de ejecutar la aplicación en el simulador, necesitamos crear el método
addItem(_:)
correspondiente en el controlador de vista de lista. Si
no hacemos esto, el controlador de vista no puede responder al mensaje
que recibe cuando se toca el botón y se lanza una excepción, bloqueando
la aplicación.
Eche un vistazo al formato de la definición de método en el siguiente fragmento de código. Como vimos anteriormente en esta serie, la acción acepta un argumento, el objeto que envía el mensaje al controlador de vista (destino). En este ejemplo, el remitente es el botón en la barra de navegación.
1 |
func addItem(sender: UIBarButtonItem) { |
2 |
print("Button was tapped.") |
3 |
}
|
He agregado una declaración impresa a la implementación del
método para probar si todo funciona correctamente. Cree el proyecto y
ejecute la aplicación para probar el botón en la barra de
navegación.
Paso 2: Creando un Controlador de Vista
Cree una nueva
subclase UIViewController
y asígnele el nombre AddItemViewController. En
AddItemViewController.swift, declaramos dos salidas para dos campos de
texto, que crearemos en unos momentos.
1 |
import UIKit |
2 |
|
3 |
class AddItemViewController: UIViewController { |
4 |
|
5 |
@IBOutlet var nameTextField: UITextField! |
6 |
@IBOutlet var priceTextField: UITextField! |
7 |
|
8 |
// MARK: -
|
9 |
// MARK: View Life Cycle
|
10 |
override func viewDidLoad() { |
11 |
super.viewDidLoad() |
12 |
}
|
13 |
|
14 |
}
|
También
necesitamos declarar dos acciones en AddItemViewController.swift. La
primera acción, cancel(_:)
, cancela la creación de un nuevo
elemento. La segunda acción, save(_:)
, usa la entrada del usuario
para crear y guardar un nuevo elemento.
1 |
// MARK: -
|
2 |
// MARK: Actions
|
3 |
@IBAction func cancel(sender: UIBarButtonItem) { |
4 |
|
5 |
}
|
6 |
|
7 |
@IBAction func save(sender: UIBarButtonItem) { |
8 |
|
9 |
}
|
Abra
Main.storyboard, arrastre una instancia de UIViewController
desde Object Library al espacio de trabajo y establezca su clase en
AddItemViewController
en el Identity Inspector.



Cree
una transición manual presionando Control y arrastrando desde el objeto
List View Controller al objeto Add Item View Controller. Seleccione Present Modally del menú que aparece.






Seleccione la transición que acaba de crear, abra el Attributes Inspector y establezca su Identifier en AddItemViewController.



Antes de agregar los campos de texto, seleccione Agregar controlador de vista de elementos e incrústelo en un controlador de navegación seleccionando Embed In > Navigation Controller en el menú Editor.
Anteriormente
en esta lección, programáticamente agregamos un UIBarButtonItem
al
elemento de navegación del controlador de vista de lista. Veamos cómo funciona esto en un storyboard. Amplíe
el controlador de vista Agregar elemento y agregue dos instancias de
UIBarButtonItem
a su barra de navegación, posicionando uno en cada lado. Seleccione el elemento del botón de la barra izquierda, abra el
Attributes Inspector y configure Identifier para cancelar. Haz lo
mismo con el ítem del botón de la barra derecha, configurando su
Identifier a Save.



Seleccione
el objeto Add Item View Controller , abra el Connections Inspector a la derecha y conecte la acción cancel(_:)
con el
elemento del botón de la barra izquierda y la acción save(_:)
con el elemento del botón de la barra derecha.



Arrastre
dos instancias de UITextField
desde la Object Library a la vista
del controlador de vista de elementos agregados. Coloque los campos de
texto como se muestra a continuación. No olvide agregar las
restricciones necesarias a los campos de texto.



Seleccione el campo de
texto superior, abra el Attributes Inspector e ingrese Name en el
campo Placeholder. Seleccione
el campo de texto de la parte inferior y, en el Attributes Inspector,
establezca su texto a Price y Keyboard a Number Pad. Esto garantiza que los usuarios solo puedan ingresar
números en el campo de texto inferior. Seleccione
el objeto Add Item View Controller, abra el Connections Inspector, y conecte las salidas de nameTextField
y priceTextField
con el campo de texto correspondiente en la vista del controlador de
vista.
Eso fue bastante trabajo. Todo lo que hemos hecho en el storyboard también se puede lograr mediante programación. Algunos desarrolladores ni siquiera usan storyboards y crean toda la interfaz de usuario de la aplicación mediante programación. Eso es exactamente lo que sucede debajo del capó de todos modos.
Paso 3: implementando addItem(_:)
Con AddItemViewController
listo para usar, revisemos la acción
addItem(_:)
en ListViewController
. La implementación de addItem(_:)
es corta, como puede ver a continuación. Invocamos
performSegueWithIdentifier(_:sender:)
, pasando el identificador
AddItemViewController
que establecemos en el storyboard y
self
, el controlador de vista.
1 |
func addItem(sender: UIBarButtonItem) { |
2 |
performSegueWithIdentifier("AddItemViewController", sender: self) |
3 |
}
|
Paso 4: descartar el controlador de vista
El
usuario también debe poder descartar el controlador de vista tocando el
botón cancelar o guardar del controlador de vista Agregar elemento.
Revise
las acciones cancel(_:)
y save(_:)
en AddItemViewController
y
actualice sus implementaciones como se muestra a continuación. Revisaremos la acción save(_:)
un poco más adelante en este
tutorial.
1 |
@IBAction func cancel(sender: UIBarButtonItem) { |
2 |
dismissViewControllerAnimated(true, completion: nil) |
3 |
}
|
4 |
|
5 |
@IBAction func save(sender: UIBarButtonItem) { |
6 |
dismissViewControllerAnimated(true, completion: nil) |
7 |
}
|
Cuando
llamamos a dismissViewControllerAnimated(_:completion:)
en el
controlador de vista cuya vista se presenta de forma modal, el
controlador de vista modal reenvía ese mensaje al controlador de vista
que presentó el controlador de vista. En
nuestro ejemplo, esto significa que el controlador de vista Agregar
elemento reenvía el mensaje al controlador de navegación, que, a su vez,
lo reenvía al controlador de vista de la lista. El
segundo argumento de dismissViewControllerAnimated(_:completion:)
es
un cierre que se ejecuta cuando se completa la animación.
Ejecute la
aplicación en el simulador para ver la clase AddItemViewController
en
acción. Cuando toca el campo de texto de nombre o precio, el teclado
debe aparecer automáticamente desde la parte inferior.



7. Agregar elementos - Parte 2
¿Cómo sabrá el controlador de vista de lista cuando el controlador de vista de agregar elemento ha agregado un nuevo elemento? ¿Deberíamos mantener una referencia al controlador de vista de lista que presentó el controlador de vista de agregar elementos? Esto introduciría un acoplamiento ajustado, lo cual no es una buena idea, ya que hace que nuestro código sea menos independiente y menos reutilizable.
El problema al que nos enfrentamos se puede resolver implementando un protocolo de delegado personalizado. Veamos cómo funciona esto.
Delegación
La idea es simple. Cada vez que el usuario toca el botón Guardar, el add item view controller recoge la información de los campos de texto y notifica a su delegado que se ha guardado un nuevo elemento.
El objeto delegado debe ser un objeto que se ajuste a un protocolo delegado personalizado que definamos. Depende del objeto delegado decidir qué debe hacerse con la información que envía el controlador de vista de agregar elemento. El controlador de vista Agregar elemento solo es responsable de capturar la entrada del usuario y notificar a su delegado.
Abra
AddItemViewController.swift y declare el protocolo
AddItemViewControllerDelegate
en la parte superior. El protocolo define
un método para notificar al delegado que se guardó un artículo. Pasa
junto con el nombre y el precio del artículo.
1 |
import UIKit |
2 |
|
3 |
protocol AddItemViewControllerDelegate { |
4 |
func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float) |
5 |
}
|
6 |
|
7 |
class AddItemViewController: UIViewController { |
8 |
|
9 |
...
|
10 |
|
11 |
}
|
Como
recordatorio, una declaración de protocolo define o declara los métodos
y las propiedades que deberían implementar los objetos que se ajustan
al protocolo. Se requieren todos los métodos y propiedades en un protocolo
Swift.
También necesitamos declarar una propiedad para el delegado. El
delegado es del tipo AddItemViewControllerDelegate?
. Tenga en cuenta el
signo de interrogación, lo que indica que es un tipo
opcional.
1 |
class AddItemViewController: UIViewController { |
2 |
|
3 |
@IBOutlet var nameTextField: UITextField! |
4 |
@IBOutlet var priceTextField: UITextField! |
5 |
|
6 |
var delegate: AddItemViewControllerDelegate? |
7 |
|
8 |
...
|
9 |
|
10 |
}
|
Como
mencioné en la lección sobre vistas de tabla, es una buena práctica
pasar el remitente del mensaje, el objeto que notifica al objeto
delegado, como el primer argumento de cada método de delegado. Esto
facilita que el objeto delegado se comunique con el remitente sin el
requisito estricto de mantener una referencia al delegado.
Notificar al Delegado
Es hora de usar el protocolo de delegado que declaramos hace un
momento. Vuelva
a visitar el método save(_:)
en la clase AddItemViewController
y
actualice su implementación como se muestra a
continuación.
1 |
@IBAction func save(sender: UIBarButtonItem) { |
2 |
if let name = nameTextField.text, let priceAsString = priceTextField.text, let price = Float(priceAsString) { |
3 |
// Notify Delegate
|
4 |
delegate?.controller(self, didSaveItemWithName: name, andPrice: price) |
5 |
|
6 |
// Dismiss View Controller
|
7 |
dismissViewControllerAnimated(true, completion: nil) |
8 |
}
|
9 |
}
|
Usamos enlace opcional para extraer de forma segura los valores de los campos de nombre y texto de precio. Notificamos al delegado invocando el método de delegado que declaramos anteriormente. Al final, descartamos el controlador de vista.
Dos
detalles valen la pena señalar. ¿Vio el signo de interrogación después
de la delegada property
? En Swift, este constructo se conoce como
encadenamiento opcional. Como la propiedad delegate
es una opción, no se
garantiza que tenga un valor. Al
agregar un signo de interrogación a la propiedad delegate
al invocar
el método de delegado, el método solo se invoca si la propiedad delegate
tiene un valor. El encadenamiento opcional hace que su código
sea mucho más seguro.
También tenga en cuenta que creamos un Float
a
partir del valor almacenado en priceAsString
. Esto es necesario, porque
el método de delegado espera un flotante como su tercer parámetro, no
una cadena.
Respondiendo a Save Events
La última pieza del rompecabezas es
hacer que ListViewController
se ajuste al protocolo
AddItemViewControllerDelegate
. Abra
ListViewController.swift y actualice la declaración de interfaz de
ListViewController
para que la clase se ajuste al nuevo
protocolo.
1 |
import UIKit |
2 |
|
3 |
class ListViewController: UITableViewController, AddItemViewControllerDelegate { |
4 |
|
5 |
...
|
6 |
|
7 |
}
|
Ahora
necesitamos implementar los métodos definidos en el protocolo
AddItemViewControllerDelegate
. En ListViewController.swift, agregue la
siguiente implementación de controller(_:didSaveItemWithName:andPrice:)
.
1 |
// MARK: -
|
2 |
// MARK: Add Item View Controller Delegate Methods
|
3 |
func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float) { |
4 |
// Create Item
|
5 |
let item = Item(name: name, price: price) |
6 |
|
7 |
// Add Item to Items
|
8 |
items.append(item) |
9 |
|
10 |
// Add Row to Table View
|
11 |
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: (items.count - 1), inSection: 0)], withRowAnimation: .None) |
12 |
|
13 |
// Save Items
|
14 |
saveItems() |
15 |
}
|
Creamos
una nueva instancia de Item
invocando init(name:price:)
, pasando
el nombre y el precio que recibimos del controlador de vista de agregar
elemento. En el siguiente paso, la propiedad items
se actualiza
al agregar el elemento recién creado. Por supuesto, la vista de tabla no
refleja automágicamente la adición de un nuevo elemento. Insertamos
manualmente una nueva fila en la vista de tabla. Para
guardar los cambios en el disco, llamamos a saveItems()
en el
controlador de vista, que implementamos anteriormente en este
tutorial.
Configurando al Delegado
La
última pieza de este rompecabezas algo complejo es establecer el
delegado del controlador de vista de elementos agregados cuando se lo
presenta al usuario. Hacemos esto en prepareForSegue(_:sender:)
como
vimos anteriormente en esta serie.
1 |
// MARK: -
|
2 |
// MARK: Navigation
|
3 |
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { |
4 |
if segue.identifier == "AddItemViewController" { |
5 |
if let navigationController = segue.destinationViewController as? UINavigationController, |
6 |
let addItemViewController = navigationController.viewControllers.first as? AddItemViewController { |
7 |
addItemViewController.delegate = self |
8 |
}
|
9 |
}
|
10 |
}
|
Si el identificador de segue es igual a
AddItemViewController
, le pedimos al segue su destinoViewController
. Puede
pensar que el controlador de vista de destino es el controlador de
vista de elementos agregados, pero recuerde que el controlador de vista
add item está incrustado en un controlador de navegación.
primer
elemento en la pila de navegación del controlador de navegación, que
nos proporciona el controlador de vista raíz o el objeto de controlador
de vista add item que estamos buscando A
continuación, establecemos la propiedad delegate
del controlador de
vista Agregar elemento en self
, el controlador de vista de lista.
Ejecute la aplicación una vez más para ver cómo funciona todo junto, como por arte de magia.
Conclusión
Eso fue mucho para asimilar, pero ya hemos logrado bastante. En la siguiente lección, hacemos algunos cambios en el controlador de vista de lista para editar y eliminar elementos de la lista. En esa lección, también agregamos la capacidad de crear una lista de compras de la lista de artículos.
Si tiene preguntas o comentarios, puede
dejarlos en los comentarios a continuación o comunicarse conmigo en
Twitter.