Students Save 30%! Learn & create with unlimited courses & creative assets Students Save 30%! Save Now
Advertisement
  1. Code
  2. Swift
Code

Swift desde cero: Initialization e Initializer Delegation

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Swift From Scratch.
Swift From Scratch: Access Control and Property Observers

Spanish (Español) translation by CYC (you can also view the original English article)

En las entregas anteriores de Swift desde cero, creamos una aplicación funcional de tareas. Sin embargo, el modelo de datos podría usar algo de amor. En este tutorial, vamos a refactorizar el modelo de datos implementando una clase de modelo personalizada.

1. El Data Model

El modelo de datos que estamos a punto de implementar incluye dos clases, una clase Task y una clase ToDo que hereda de la clase Task. Mientras creamos e implementamos estas clases de modelos, continuamos nuestra exploración de la programación orientada a objetos en Swift. En este tutorial, hacemos un acercamiento a la inicialización de instancias de clase y qué rol desempeña la herencia durante la inicialización.

Clase Task

Comencemos con la implementación de la clase Task. Crea un nuevo archivo Swift seleccionando Nuevo > Archivo... desde el menú Archivo de Xcode. Selecciona Archivo Swift desde la sección iOS > Fuente. Nombra el archivo como Task.swift y presiona en Crear.

La implementación básica es corta y simple. La clase Task hereda de NSObject, definida en el framework de Foundation, y tiene una propiedad variable name de tipo String. La clase define dos inicializadores, init e init(name:). Hay algunos detalles que pueden hacerte tropezar, así que déjame explicarte qué está pasando.

Debido a que el método init también se define en la clase NSObject, necesitamos ponerle un prefijo al inicializador con la palabra clave override. Cubrimos los métodos generales anteriores en esta serie. En el método init, invocamos el método init(name:), pasando en "New Task" como el valor para el parámetro name.

El método init(name:) es otro inicializador que acepta un único parámetro name de tipo String. En este inicializador, el valor del parámetro name se asigna a la propiedad name. Esto es bastante fácil de entender. ¿Verdad?

Inicializadores designados y de conveniencia

¿Qué pasa con la palabra clave convenience prefijando el método init? Las clases pueden tener dos tipos de inicializadores, inicializadores designados e inicializadores de conveniencia. Los inicializadores de conveniencia están prefijados con la palabra clave convenience, lo que implica que init(name:) es un inicializador designado. ¿Porqué eso es así? ¿Cuál es la diferencia entre inicializadores designados y convenientes?

Los inicializadores designados inicializan por completo una instancia de una clase, lo que significa que cada propiedad de la instancia tiene un valor inicial después de la inicialización. Al observar la clase Task, por ejemplo, vemos que la propiedad name se establece con el valor del parámetro name del inicializador init(name:). El resultado después de la inicialización es una instancia de Task totalmente inicializada.

Sin embargo, los inicializadores de conveniencia dependen de un inicializador designado para crear una instancia completamente inicializada de la clase. Es por eso que el inicializador init de la clase Task invoca el inicializador init(name:) en su implementación. Esto se conoce como delegación de inicializador. El inicializador init delega la inicialización en un inicializador designado para crear una instancia completamente inicializada de la clase Task.

Los inicializadores de conveniencia son opcionales. No todas las clases tienen un inicializador de conveniencia. Se requieren inicializadores designados y una clase necesita tener al menos un inicializador designado para crear una instancia completamente inicializada de sí mismo.

Protocolo NSCoding

La implementación de la clase Task no está completa. Más adelante en este artículo, escribiremos una matriz de instancias de ToDo en el disco. Esto solo es posible si las instancias de la clase ToDo pueden codificarse y decodificarse.

¡No te preocupes, esto no es ciencia espacial! Solo tenemos que hacer que las clases Task y ToDo se ajusten al protocolo NSCoding. Es por eso que la clase Task hereda de la clase NSObject ya que el protocolo NSCoding solo puede ser implementado por clases heredadas, directa o indirectamente, de NSObject. Al igual que la clase NSObject, el protocolo NSCoding se define en el framework de Foundation.

Adoptar un protocolo es algo que ya hemos tratado en esta serie, pero hay algunas trampas que quiero señalar. Comencemos diciéndole al compilador que la clase Task se ajusta al protocolo NSCoding.

A continuación, debemos implementar los dos métodos declarados en el protocolo NSCoding, init(coder:) y encodeWithCoder(_:). La implementación es sencilla si estás familiarizado con el protocolo NSCoding.

El inicializador init(coder:) es un inicializador designado que inicializa una instancia de Task. Aunque implementamos el método init(coder:) para cumplir con el protocolo NSCoding, nunca necesitarás invocar este método directamente. Lo mismo es cierto para encodeWithCoder(_:), que codifica una instancia de la clase Task.

La palabra clave required prefijando el método init(coder:) indica que cada subclase de la clase Task necesita implementar este método. La palabra clave required solo se aplica a los inicializadores, por lo que no es necesario que la agreguemos al método encodeWithCoder(_:).

Antes de continuar, necesitamos hablar sobre el atributo @objc. Como el protocolo NSCoding es un protocolo Objective-C, la conformidad del protocolo solo se puede verificar agregando el atributo @objc. En Swift, no existe la conformidad del protocolo ni los métodos de protocolo opcionales. En otras palabras, si una clase se adhiere a un protocolo particular, el compilador verifica y espera que todos los métodos del protocolo se implementen.

Clase ToDo

Con la clase Task implementada, es hora de implementar la clase ToDo. Crea un nuevo archivo Swift y dale el nombre de ToDo.swift. Veamos la implementación de la clase ToDo.

La clase ToDo hereda de la clase Task y declara una propiedad variable done de tipo Bool. Además de los dos métodos requeridos del protocolo NSCoding hereda de la clase Task, también declara un inicializador designado, init(name:done:).

Como en Objective-C, la palabra clave super se refiere a la superclase, la clase Task en este ejemplo. Hay un detalle importante que merece atención. Antes de invocar el método init(name:) en la superclase, todas las propiedades declaradas por la clase ToDo deben inicializarse. En otras palabras, antes de que la clase ToDo delegue la inicialización en su superclase, cada propiedad que declare la clase ToDo necesita tener un valor inicial válido. Puedes verificar esto al cambiar el orden de las declaraciones e inspeccionar el error que aparece.

Lo mismo se aplica al método init(coder:). Primero inicializamos la propiedad done antes de invocar init(coder:) en la superclase. También ten en cuenta que bajamos y forzamos desenvolver el resultado de decodeObjectForKey(_:) un un Bool usando as!

Inicializadores y herencia

Cuando se trata de herencia e inicialización, hay algunas reglas a tener en cuenta. La regla para inicializadores designados es simple.

  • Un inicializador designado necesita invocar un inicializador designado de su superclase. En la clase ToDo, por ejemplo, el método init(coder:) invoca el método init(coder:) de su superclase. Esto también se conoce como delegating up.

Las reglas para los inicializadores de conveniencia son un poco más complejas. Hay dos reglas para tener en cuenta.

  • Un inicializador de conveniencia siempre necesita invocar a otro inicializador de la clase en la que está definido. En la clase Task, por ejemplo, el método init es un inicializador conveniente y delega la inicialización a otro inicializador, init(name:) en el ejemplo. Esto se conoce como delegating across.
  • Aunque un inicializador de conveniencia no tiene que delegar la inicialización a un inicializador designado, un inicializador conveniente necesita llamar a un inicializador designado en algún momento. Esto es necesario para inicializar completamente la instancia que se está inicializando.

Con ambas clases de modelo implementadas, es hora de refactorizar las clases ViewController y AddItemViewController. Comencemos con este último.

2. Refactorizar AddItemViewController

Paso 1: Actualización del protocolo AddItemViewControllerDelegate

Los únicos cambios que debemos hacer en la clase AddItemViewController están relacionados con el protocolo AddItemViewControllerDelegate. En la declaración de protocolo, cambia el tipo de didAddItem de String a ToDo, la clase de modelo que implementamos anteriormente.

Paso 2: Actualizar la acción create

Esto significa que también debemos actualizar la acción create en la que invocamos el método delegado. En la implementación actualizada, creamos una instancia ToDo, pasándola al método delegate.

3. Refactorización de ViewController

Paso 1: Actualizar la propiedad items

La clase ViewController requiere un poco más de trabajo. Primero tenemos que cambiar el tipo de propiedad de los items a [ToDo], una matriz de instancias de ToDo.

Paso 2: Métodos de Table View Data Source

Esto también significa que debemos refactorizar algunos otros métodos, como el método cellForRowAtIndexPath(_:) que se muestra a continuación. Debido a que el conjunto de items ahora contiene instancias de ToDo pendientes, es mucho más simple verificar si un elemento está marcado como hecho. Usamos el operador condicional ternario de Swift para actualizar el tipo de accesorio de la celda de la vista de tabla.

Cuando el usuario elimina un elemento, solo necesitamos actualizar la propiedad items eliminando la instancia correspondiente de ToDo. Esto se refleja en la implementación del método tableView(_:commitEditingStyle: forRowAtIndexPath:) que se muestra a continuación.

Paso 3: Métodos de Table View Delegate

La actualización del estado de un elemento cuando el usuario toca una fila se maneja en el método tableView(_:didSelectRowAtIndexPath:). La implementación de este método UITableViewDelegate es mucho más simple gracias a la clase ToDo.

La instancia correspondiente de ToDo se actualiza y este cambio se refleja en el table view. Para guardar el estado, invocamos saveItems en lugar de saveCheckedItems.

Paso 4: Agregar métodos Item View Controller Delegate

Debido a que actualizamos el protocolo AddItemViewControllerDelegate, también necesitamos actualizar la implementación de ViewController de este protocolo. El cambio, sin embargo, es simple. Solo necesitamos actualizar la firma del método.

Paso 5: Guardando los Items

pathForItems

En lugar de almacenar los elementos en la base de datos predeterminada del usuario, los almacenaremos en el directorio de documentos de la aplicación. Antes de actualizar los métodos loadItems y saveItems, vamos a implementar un método auxiliar llamado pathForItems. El método es privado y devuelve una ruta, la ubicación de los elementos en el directorio de documentos.

Primero buscamos la ruta al directorio de documentos en el entorno limitado de la aplicación invocando NSSearchPathForDirectoriesInDomains(_:_:_:). Como este método devuelve una matriz de cadenas, tomamos el primer elemento, forzamos desenvolverlo y lo bajamos a un String. El valor que devolvemos desde pathForItems se compone de la ruta al directorio de documentos con la cadena "items" anexa.

loadItems

El método loadItems cambia bastante. Primero almacenamos el resultado de pathForItems en un path (una ruta) con nombre constante. Ahora des-archivamos el objeto archivado en esa ruta y lo transmitimos a una matriz opcional de instancias de ToDo. Usamos un enlace opcional para desenvolver el opcional y asignarlo a una constante llamada items. En la cláusula if, asignamos el valor almacenado en los items a la propiedad items.

saveItems

El método saveItems es corto y simple. Almacenamos el resultado de pathForItems en un path con nombre constante e invocamos archiveRootObject(_:toFile:) en NSKeyedArchiver, pasando la propiedad items y el path. Imprimimos el resultado de la operación en la consola.

Paso 6: Limpiar

Terminemos con la parte divertida, eliminando el código. Comienza eliminando la propiedad checkedItems en la parte superior ya que ya no la necesitamos. Como resultado, también podemos eliminar los métodos loadCheckedItems y saveCheckedItems, y cada referencia a estos métodos en la clase ViewController.

Compila y ejecuta la aplicación para ver si todo sigue funcionando. El modelo de datos hace que el código de la aplicación sea mucho más simple y confiable. Gracias a la clase ToDo, administrar los elementos de nuestra lista ahora es mucho más fácil y menos propenso a errores.

Conclusión

En este tutorial, refactorizamos el modelo de datos de nuestra aplicación. Aprendiste más acerca de la programación orientada a objetos y la herencia. La inicialización de instancias es un concepto importante en Swift, así que asegúrate de comprender lo que hemos cubierto en este tutorial. Puedes leer más sobre la inicialización y la delegación de inicializadores en The Swift Programming Language.

Advertisement
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.