Swift – Propiedades

En esta ocasión aprenderemos sobre las Propiedades en el lenguaje Swift. Veremos como nos ayudan a crear APIs mucho más robustas y elegantes para nuestras estructuras y clases, aprenderemos a elegir entre una propiedad computada y un método.

En Swift tanto las estructuras como las clases pueden contener variables y constantes asociadas, estas modelan propiedades de los objetos que representan.

Estas variables y constantes son las llamadas propiedades en Swift.

Pero esto no es todo, las propiedades tienen matices que son muy importantes de conocer. ¡Comencemos!

Propiedades almacenadas

Las propiedades almacenadas o stored properties son las más básicas de todas las propiedades, digamos que las más simples. Su única función es almacenar un valor.

Veamos un ejemplo:

struct Airplane {
    
    let manufacturer: String
    let model: String
    
} // Airplane

let airbus350 = Airplane(manufacturer: "Airbus", model: "A350 XWB")

print("Usted volará de MEX -> MAD en un avión \(airbus350.manufacturer) modelo \(airbus350.model).")

En este código tenemos una estructura Airplane en representación de un avión. Esta estructura cuenta con dos propiedades almacenadas, ambas constantes de nombres manufacturer y model.

La salida en pantalla sería:

Usted volará de MEX -> MAD en un avión Airbus modelo A350 XWB.

Como podemos observar su funcionamiento es bien simple, las propiedades almacenadas se comportan como variables o constantes comunes.

Propiedades computadas

Una propiedad computada no almacena información. Es una propiedad (de un objeto) cuyo valor depende de cierta lógica. Una lógica que tiene que ser evaluada cada vez que se requiere de esta información o deseamos modificarla.

Para lograr esta disntinción dentro de las propiedades computadas podemos asociar un código a los eventos de lectura (get) o escritura (set) de la misma. Su forma básica es la siguiente:

var computedProperty: String {

    get {
        
        // code
        
    }

    set {
        
        // code
        
    }

} // computedProperty

Es decir, las propiedades computadas no almacenan información. El dato que representan se conforma como producto del código asociado a los bloques get o set.

Vale decir que aplican ciertas reglas:

  • Si solo queremos trabajar con el bloque get no es necesario definirlo. Si este es el caso podemos pasar a definir nuestra lógica directamente dentro de las llaves de la propiedad. Quedando así una propiedad de solo lectura.
  • Cuando declaramos un bloque set estamos obligados a declarar también uno get. No existen propiedades computadas de solo escritura.

Veamos un ejemplo más realista, pongamos en contexto todo esto:

struct Point {
    
    var x = 0.0
    var y = 0.0

} // Point

struct Size {
    
    var width = 0.0
    var height = 0.0

} // Size

struct Rect {
    
    var origin = Point()
    var size = Size()
    
    var center: Point {
        
        get {
            
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            
            return Point(x: centerX, y: centerY)
            
        } // get
        
        set (newCenter) {
            
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
            
        } // set
        
    } // center

    var area: Double {

        return size.width * size.height

    } // area
    
} // Rect

Aquí tenemos tres estructuras, una de nombre Point en representación de un punto, otra de nombre Size en representación del alto y ancho de cierto objeto, y por último tenemos la estructura Rect en representación de un rectángulo.

Como parte de la estructura Rect tenemos varios elementos que la conforman. Una instancia de tipo Point llamada origin en la cual se establece el punto inicial desde donde se dibujará la figura, y otra instancia de tipo Size donde declaramos el tamaño.

También hemos definido dos propiedades computadas, la primera de nombre center y la segunda de nombre area.

La primera cuenta con dos bloques de acceso, uno get y otro set. Dentro de get se computa el centro de la figura tomando como referencia el punto de origen de la figura y su tamaño. Aquí hagamos una pausa porque aquí se encuentra la clave.

¿Porque no guardamos el punto centro en una propiedad almacenada?

Lo pudiéramos hacer pero no sería la solución más optima o elegante. Como podemos analizar en el código el punto centro depende del origen y del tamaño.Si establecemos este valor dentro de una propiedad almacenada cuando la figura cambie su punto de origen o su tamaño el valor del punto centró estará desactualizado.

La solución a esto sería implementar un método donde computemos el valor actualizado del centro. Pero en Swift contamos con las propiedades computadas que están diseñadas específicamente para estos casos.

La segunda propiedad computada es area y en este caso no vemos ningún bloque como en el caso de center. Lo que sucede es que cuando no especificamos un bloque set el bloque get se puede omitir, es decir que en el caso de esta propiedad computada tenemos un bloque get implícito.

Dicho esto, al código anterior podemos agregar la siguientes líneas:

var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))

print("\n- La figura comienza en el punto (\(square.origin.x), \(square.origin.y))")

print("\n- El centro se encuentra en el punto (\(square.center.x), \(square.center.y))")

print("\n- El área de la figura es de \(square.area) unidades")

let initialSquareCenter = square.center

square.center = Point(x: 15.0, y: 15.0)

print("\n* Movemos el centro de la figura al punto (15, 15)")

square.size = Size(width: 300, height: 100)

print("\n* Establecemos el tamaño de la figura a 300x100")

print("\n- La figura comienza en el punto (\(square.origin.x), \(square.origin.y))")

print("\n- El centro se encuentra en el punto (\(square.center.x), \(square.center.y))")

print("\n- El área de la figura es de \(square.area) unidades")

Mediante estas líneas estamos probando el código que acabamos de implementar. Creamos un cuadrado mediante la estructura Rect (nada nos lo impide) en posición (0, 0) y de tamaño 10×10. Mostramos estos datos en pantalla y acto seguido movemos la figura estableciendo un nuevo centro:

Propiedades Computadas - Movimiento de Figura Geométrica

en esta ocasión en el punto (15, 15). Es decir que hemos movido la figura 10 unidades hacia arriba (en el eje y) y 10 hacia la derecha (en el eje x). Luego cambiamos el tamaño tomando como referencia el punto de origen y lo convertimos en un rectángulo. Esto lógicamente mueve el centro y ahora lo tenemos en el punto (160, 60).

La salida en pantalla completa sería:

- La figura comienza en el punto (0.0, 0.0)

- El centro se encuentra en el punto (5.0, 5.0)

- El área de la figura es de 100.0 unidades

* Movemos el centro de la figura al punto (15, 15)

* Establecemos el tamaño de la figura a 300x100

- La figura comienza en el punto (10.0, 10.0)

- El centro se encuentra en el punto (160.0, 60.0)

- El área de la figura es de 30000.0 unidades

Recapitulando

Podemos resumir que las propiedades computadas aplican sobre valores dinámicos que dependen de otras propiedades almacenadas.

Las propiedades computadas no almacenan ningún valor:

  • Cuando obtenemos datos a través de get se computan mediante otras propiedades almacenadas.
  • Cuando le pasamos datos a través de set el valor se procesa y se almacena (o se descompone) entre una o varias propiedades almacenadas.

Despedimos esta sección con otro ejemplo y como siempre os digo: cualquier duda que tengan la pueden dejar en los comentarios.

struct User {
    
    let name: String
    let lastName: String
    
    let birthday: Date

    var fullName: String {
        
        return String("\(name) \(lastName)")
        
    } // fullName
    
    var age: Int {
        
        let calendar = Calendar.current
        
        let date = calendar.dateComponents([.year], from: birthday, to: Date())
        
        return date.year ?? 1

    } // age

} // User

let calendar = Calendar.current

var dateComponents = calendar.dateComponents([Calendar.Component.day, Calendar.Component.month, Calendar.Component.year], from: Date())

dateComponents.day = 10
dateComponents.month = 08
dateComponents.year = 1990
dateComponents.timeZone = TimeZone.current

let birthday = calendar.date(from: dateComponents)

let paco = User(name: "Paco", lastName: "Mota", birthday: birthday ?? Date())

print("El nombre completo de \(paco.name) es \(paco.fullName) y tiene \(paco.age) años de edad.")

la salida en pantalla de este código es:

El nombre completo de Paco es Paco Mota y tiene 29 años de edad.

Observadores de propiedad

Ser capaces de «observar» cambios en los valores de ciertas variable es esencial bajo diferentes estilos de programación. Ya sea que estemos interactuando con delegados, funciones o programación reactiva, gran parte de nuestra lógica a menudo está impulsada por cambios en estados y valores.

Si bien hay una serie de abstracciones que podemos implementar a favor de observar y comunicar los cambios que ocurran en una variable: Swift brilla nuevamente como un lenguaje muy bien pensado.

Exacto, en Swift contamos con una forma simple pero poderosa de establecer observaciones en cualquier tipo de propiedad almacenada (no lazy), de ahí su apropiado nombre: observadores de propiedad.

Veamos la sintaxis y como se comportan los observadores:

struct Example {
    
    var computedProperty: String {
        
        willSet {
            
            print("\nwillSet llamado")
            print("computedProperty es ahora igual a: \(self.computedProperty)")
            print("computedProperty será igualado al valor: \(newValue)")
            
        } // willSet
        
        didSet {
            
            print("\ndidSet llamado")
            print("computedProperty es ahora igual a: \(self.computedProperty)")
            print("computedProperty era igual al valor: \(oldValue)")
            
        } // didSet
        
    } // computedProperty
    
} // Example

var example = Example(computedProperty: "Texto de ejemplo 1")

example.computedProperty = "Texto de ejemplo 2"

Aquí tenemos una estructura de ejemplo y una propiedad llamada computedProperty en la cual hemos establecido dos observadores. El primero (willSet) se dispara antes de que un nuevo valor sea asociado a la variable, mientras que el otro (didSet) lo hace una vez que el valor ya ha sido almacenado.

En el ámbito de ambos observadores tenemos acceso a:

  • willSet: El nuevo valor que se almacenará se encuentra en la constante newValue.
  • didSet: El antiguo valor de la propiedad se encuentra en la constante oldValue.

Esto es bien util ya que en ciertas ocaciones necesitaremos del nuevo valor a almacenarse antes de que este evento ocurra, y de igual manera del antiguo valor una vez que el evento ya ocurrió.

En otras palabras, no hay necesidad de declarar una variable temporal para almacenar los valores que luego necesitemos, Swift está muy bien pensado, es lo suficientemente flexible y moderno para brindarnos características como estas.

La salida en pantalla es la siguiente:

willSet llamado
computedProperty es ahora igual a: Texto de ejemplo 1
computedProperty será igualado al valor: Texto de ejemplo 2

didSet llamado
computedProperty es ahora igual a: Texto de ejemplo 2
computedProperty era igual al valor: Texto de ejemplo 1

Aquí mediante las funciones print que hemos colocado dentro de willSet y didSet podemos seguir mucho mejor la lógica de los observadores.

Propiedades lazy

Lazy significa vago o perezoso en inglés, pero ¿a qué nos referimos con propiedades lazy? En Swift las propiedades pueden adoptar un comportamiento «perezoso» mediante el modificador lazy.

Una variable lazy es aquella cuyo valor asociado no se computa o se almacena hasta que la variables es llamada por primera vez.

Veamos un ejemplo:

struct Person {
    
    let name: String
    let lastName: String
    
    lazy var fullName = String("\(name) \(lastName)")
    
    init(name: String, lastName: String) {
        
        self.name = name
        self.lastName = lastName
        
    } // init

} // Person

let carlos = Person(name: "Carlos", lastName: "Maure")

print("Nombre completo: \(carlos.fullName)")

Este código tiene un error, y muchos quizás se preguntarán: ¿Dónde, por qué? Leamos el mensaje de error:

error: cannot use mutating getter on immutable value: 'carlos' is a 'let' constant

¿Mutating? Sí, al ejecutar la propiedad lazy fullName se inicializa dicha posición de memoria con el valor concatenado de name y lastName, de ahí que estamos mutando dicha propiedad y por ende la estructura completa debe restablecerse nuevamente en memoria.

Nota: Si no sabes a que me refiero cuando digo que la estructura debe restablecerse nuevamente en memoria, te recomiendo leer el siguiente artículo Estructuras, Clases y Métodos, en especial la sección 2.1.

Para corregir el error solo tenemos que cambiar let por var en la instancia carlos, quedando así:

var carlos = Person(name: "Carlos", lastName: "Maure")

la salida en pantalla:

Nombre completo: Carlos Maure

Las propiedades lazy son útiles sobre todo cuando el valor inicial de una propiedad depende de factores externos, valores que no se conocen hasta que la inicialización de la instancia se haya completado.

Este tipo de propiedades también son útiles cuando el valor inicial de una propiedad requiere de un alto costo computacional, que no debe realizarse a menos que se necesite.

En el ejemplo anterior hasta que no ejecutamos la propiedad fullName no se le asignó su valor, ahora imaginen que dicho valor estuviera en un servidor remoto y que fuera un arreglo de varios elementos.

Si ejecutáramos algo así de manera sincrónica nos boquearía la ejecución de la aplicación hasta que la inicialización finalizara correctamente. Más aún cuando quizás es un dato que no se requiere necesariamente desde un inicio, como en el caso de fullName:

¿Qué sentido tiene que fullName tenga almacenado su valor cuando quizás jamás se ejecute?

por ahí va la lógica de las propiedades lazy.

Propiedades estáticas

Las propiedades también pueden ser declaradas como estáticas, permitiéndonos hacer llamadas a estas sin necesidad de instanciar la clase o estructura a la que pertenezca.

Al igual que en otros lenguajes de programación esto lo podemos lograr mediante la palabra clave static, veamos:

struct Structure {
    
    static var storedTypeProperty = "Some value."
    
    static var computedTypeProperty: Int {
        
        return 1
        
    } // computedTypeProperty
    
} // Structure

enum Enumeration {
    
    static var storedTypeProperty = "Some value."
    
    static var computedTypeProperty: Int {
        
        return 6
        
    } // computedTypeProperty
    
} // Enumeration

class SomeClass {
    
    static var storedTypeProperty = "Some value."
    
    static var computedTypeProperty: Int {
        
        return 27
        
    } // computedTypeProperty
    
    class var overrideableComputedTypeProperty: Int {
        
        return 107
        
    } // overrideableComputedTypeProperty
    
} // SomeClass

print(Structure.storedTypeProperty)

Structure.storedTypeProperty = "Another value."

print(Structure.storedTypeProperty)

print(Enumeration.computedTypeProperty)

print(SomeClass.computedTypeProperty)

print(SomeClass.overrideableComputedTypeProperty)

la salida en pantalla:

Some value.
Another value.
6
27
107

En este ejemplo tenemos una estructura, una enumeración y una clase, en cada una de estas tenemos propiedades almacenadas y computadas declaradas como estáticas.

Luego hacemos uso de todas estas propiedades sin necesidad de crear una instancia para cada uno de estos objetos.

La palabra clave class dentro de la clase SomeClass no es un error.

Utilizamos la palabra clave class para un método cuando necesitamos tener un método estático que pueda ser sobre-escrito desde una subclase, es decir desde una clase que herede de la nuestra, desde una clase hija.

Exacto, los métodos marcados como static no pueden ser sobre-escritos desde una subclase.

¿Qué sentido tiene un método static?

Puede que muchos alumnos no encuentren sentido a los métodos static, mediante una instancia pueden lograr lo mismo, ¿es cuestión de pereza?

Necesitamos de una instancia, que es en la mayoría de los casos, cuando vamos a representar una entidad de algo y necesitamos que esta perdure en el tiempo, ejemplo:

Instancias

  • En un software donde gestionamos los trabajadores de la empresa, tendremos instancias de un objeto Employee (trabajador) como también de Wage (salario).
  • En una aplicación donde gestionamos viajes, una instancia de un objeto Flight (vuelo).
  • En un software donde gestionamos los carros que entrar al taller, tendremos instancias de Car y de Client. Haciendo referencia al cliente dueño del carro (Client) y al carro como tal (Car).

Las propiedades estáticas brindan funcionalidades bien específicas de uso inmediato, no necesariamente de solo lectura, digamos: cuando ejecutamos la llamada Double.pi, en este caso pi es una propiedad estática.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!