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

iOS desde cero con Swift: creación de una aplicación de lista de compras 1

Scroll to top
Read Time: 31 min
This post is part of a series called iOS From Scratch With Swift.
iOS From Scratch With Swift: Data Persistence and Sandboxing on iOS
iOS From Scratch With Swift: Building a Shopping List Application 2

() 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.

Choosing the Single View Application TemplateChoosing the Single View Application TemplateChoosing the Single View Application Template

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.

Configuring the ProjectConfiguring the ProjectConfiguring the Project

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.

Creating a Cocoa Touch ClassCreating a Cocoa Touch ClassCreating a Cocoa Touch Class

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.

Configuring the List View Controller ClassConfiguring the List View Controller ClassConfiguring the List View Controller Class

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.

Adding a Tab Bar ControllerAdding a Tab Bar ControllerAdding a Tab Bar 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.

Embedding the List View Controller in a Navigation ControllerEmbedding the List View Controller in a Navigation ControllerEmbedding the List View Controller in a Navigation Controller

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

Running the Application for the First TimeRunning the Application for the First TimeRunning the Application for the First Time

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 tipo String para identificar de forma única cada elemento
  • name de tipo String
  • price de tipo Float
  • inShoppingList de tipo Bool 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.

Populating the List View Controller With ItemsPopulating the List View Controller With ItemsPopulating the List View Controller With Items

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.

Creating the Add Item View Controller ClassCreating the Add Item View Controller ClassCreating the Add Item View Controller Class

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.

Creating a Segue to the Add Item View ControllerCreating a Segue to the Add Item View ControllerCreating a Segue to the Add Item View Controller
Creating a Segue to the Add Item View ControllerCreating a Segue to the Add Item View ControllerCreating a Segue to the Add Item View Controller

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

Configuring the Segue to the Add Item View ControllerConfiguring the Segue to the Add Item View ControllerConfiguring the Segue to the Add Item View Controller

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.

Configuring the Navigation Bar of the Add Item View ControllerConfiguring the Navigation Bar of the Add Item View ControllerConfiguring the Navigation Bar of the Add Item View Controller

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.

Connecting the Actions in the Connections InspectorConnecting the Actions in the Connections InspectorConnecting the Actions in the Connections Inspector

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.

Adding Text Fields to the Add Item View ControllerAdding Text Fields to the Add Item View ControllerAdding Text Fields to the Add Item View Controller

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.

Adding an Item in the Add Item View ControllerAdding an Item in the Add Item View ControllerAdding an Item in the Add Item View Controller

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.

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.