Los errores que la mayoría de los desarrolladores de Swift no saben que están cometiendo
Publicado: 2022-03-11Viniendo de un fondo de Objective-C, al principio, sentí que Swift me estaba frenando. Swift no me permitía progresar debido a su naturaleza fuertemente tipada, lo que solía ser exasperante a veces.
A diferencia de Objective-C, Swift impone muchos requisitos en el momento de la compilación. Las cosas que están relajadas en Objective-C, como el tipo de id
y las conversiones implícitas, no son una cosa en Swift. Incluso si tiene un Int
y un Double
y desea agregarlos, deberá convertirlos a un solo tipo explícitamente.
Además, los opcionales son una parte fundamental del lenguaje, y aunque son un concepto simple, lleva un tiempo acostumbrarse a ellos.
Al principio, es posible que desee forzar el desenvolvimiento de todo, pero eso eventualmente provocará bloqueos. A medida que se familiariza con el lenguaje, comienza a amar cómo apenas tiene errores de tiempo de ejecución, ya que muchos errores se detectan en el momento de la compilación.
La mayoría de los programadores de Swift tienen una experiencia previa significativa con Objective-C que, entre otras cosas, podría llevarlos a escribir código Swift utilizando las mismas prácticas con las que están familiarizados en otros lenguajes. Y eso puede causar algunos errores graves.
En este artículo, describimos los errores más comunes que cometen los desarrolladores de Swift y las formas de evitarlos.
1. Opcionales de desenvolvimiento forzado
Una variable de un tipo opcional (p. ej String?
) puede contener o no un valor. Cuando no tienen un valor, son iguales a nil
. Para obtener el valor de un opcional, primero hay que desenvolverlos , y eso se puede hacer de dos formas distintas.
Una forma es el enlace opcional usando un if let
o un guard let
, es decir:
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
El segundo es forzar el desenvolvimiento usando el !
operador, o usando un tipo opcional desenvuelto implícitamente (por ejemplo String!
). Si el opcional es nil
, forzar un desempaquetado provocará un error de tiempo de ejecución y terminará la aplicación. Además, intentar acceder al valor de un opcional desenvuelto implícitamente causará lo mismo.
A veces tenemos variables que no podemos (o no queremos) inicializar en el inicializador de clase/estructura. Por lo tanto, tenemos que declararlos como opcionales. En algunos casos, asumimos que no serán nil
en ciertas partes de nuestro código, por lo que los forzamos a desenvolverlos o los declaramos como opcionales implícitamente desenvueltos porque eso es más fácil que tener que hacer enlaces opcionales todo el tiempo. Esto debe hacerse con cuidado.
Esto es similar a trabajar con IBOutlet
s, que son variables que hacen referencia a un objeto en un plumín o guión gráfico. No se inicializarán con la inicialización del objeto principal (generalmente un controlador de vista o UIView
personalizado), pero podemos estar seguros de que no serán nil
cuando se llame a viewDidLoad
(en un controlador de vista) o awakeFromNib
(en una vista), y así podamos acceder a ellos de forma segura.
En general, la mejor práctica es evitar forzar el desenvolvimiento y usar opciones implícitamente desencapsuladas. Considere siempre que el opcional podría ser nil
y manéjelo de manera adecuada, ya sea mediante el enlace opcional o verificando si no es nil
antes de forzar un desenvolvimiento, o acceder a la variable en caso de un opcional desenvuelto implícitamente.
2. Desconocer las trampas de los ciclos de referencia fuertes
Existe un ciclo de referencia fuerte cuando un par de objetos mantienen una referencia fuerte entre sí. Esto no es algo nuevo para Swift, ya que Objective-C tiene el mismo problema y se espera que los desarrolladores experimentados de Objective-C lo manejen adecuadamente. Es importante prestar atención a las referencias sólidas y qué hace referencia a qué. La documentación de Swift tiene una sección dedicada a este tema.
Es particularmente importante administrar sus referencias al usar cierres. De forma predeterminada, los cierres (o bloques) mantienen una fuerte referencia a cada objeto al que se hace referencia dentro de ellos. Si alguno de estos objetos tiene una fuerte referencia al cierre en sí, tenemos un ciclo de referencia fuerte. Es necesario hacer uso de listas de captura para administrar adecuadamente cómo se capturan sus referencias.
Si existe la posibilidad de que la instancia capturada por el bloque se desasigne antes de que se llame al bloque, debe capturarla como una referencia débil , que será opcional, ya que puede ser nil
. Ahora, si está seguro de que la instancia capturada no se desasignará durante la vigencia del bloque, puede capturarla como una referencia sin propietario . La ventaja de usar unowned
en lugar de weak
es que la referencia no será opcional y puede usar el valor directamente sin necesidad de desenvolverlo.
En el siguiente ejemplo, que puede ejecutar en Xcode Playground, la clase Container
tiene una matriz y un cierre opcional que se invoca cada vez que cambia su matriz (utiliza observadores de propiedades para hacerlo). La clase Whatever
que sea tiene una instancia de Container
, y en su inicializador, asigna un cierre a arrayDidChange
y este cierre hace referencia a self
, creando así una fuerte relación entre la instancia de Whatever
y el cierre.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
Si ejecuta este ejemplo, notará que deinit whatever
nunca se imprime, lo que significa que nuestra instancia w
no se desasigna de la memoria. Para arreglar esto, tenemos que usar una lista de captura para no capturarse a self
fuertemente:
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
En este caso, podemos usar unowned
, porque self
nunca será nil
durante el tiempo de vida del cierre.
Es una buena práctica usar casi siempre listas de captura para evitar ciclos de referencia, lo que reducirá las fugas de memoria y, al final, un código más seguro.
3. Usarse a self
en todas partes
A diferencia de Objective-C, con Swift, no estamos obligados a usar self
para acceder a las propiedades de una clase o estructura dentro de un método. Solo estamos obligados a hacerlo en un cierre porque necesita capturarse a self
. Usar self
donde no es necesario no es exactamente un error, funciona bien y no habrá errores ni advertencias. Sin embargo, ¿por qué escribir más código del necesario? Además, es importante mantener la consistencia del código.
4. No saber el tipo de tus tipos
Swift usa tipos de valor y tipos de referencia . Además, las instancias de un tipo de valor exhiben un comportamiento ligeramente diferente de las instancias de tipos de referencia. No saber en qué categoría encaja cada una de sus instancias generará falsas expectativas sobre el comportamiento del código.
En la mayoría de los lenguajes orientados a objetos, cuando creamos una instancia de una clase y la pasamos a otras instancias y como argumento a los métodos, esperamos que esta instancia sea la misma en todas partes. Eso significa que cualquier cambio se reflejará en todas partes, porque de hecho, lo que tenemos son solo un montón de referencias a exactamente los mismos datos. Los objetos que presentan este comportamiento son tipos de referencia y, en Swift, todos los tipos declarados como class
son tipos de referencia.
A continuación, tenemos tipos de valores que se declaran usando struct
o enum
. Los tipos de valor se copian cuando se asignan a una variable o se pasan como argumento a una función o método. Si cambia algo en la instancia copiada, la original no se modificará. Los tipos de valores son inmutables . Si asigna un nuevo valor a una propiedad de una instancia de un tipo de valor, como CGPoint
o CGSize
, se crea una nueva instancia con los cambios. Es por eso que podemos usar observadores de propiedades en una matriz (como en el ejemplo anterior en la clase Container
) para notificarnos los cambios. Lo que realmente está sucediendo es que se crea una nueva matriz con los cambios; se asigna a la propiedad y luego se invoca a didSet
.

Por lo tanto, si no sabe que el objeto con el que está tratando es de un tipo de referencia o de valor, sus expectativas sobre lo que hará su código pueden ser completamente incorrectas.
5. No utilizar todo el potencial de las enumeraciones
Cuando hablamos de enumeraciones, generalmente pensamos en la enumeración C básica, que es solo una lista de constantes relacionadas que son números enteros debajo. En Swift, las enumeraciones son mucho más poderosas. Por ejemplo, puede adjuntar un valor a cada caso de enumeración. Las enumeraciones también tienen métodos y propiedades calculadas/de solo lectura que se pueden usar para enriquecer cada caso con más información y detalles.
La documentación oficial sobre las enumeraciones es muy intuitiva, y la documentación sobre el manejo de errores presenta algunos casos de uso para la potencia adicional de las enumeraciones en Swift. Además, consulte la siguiente exploración exhaustiva de las enumeraciones en Swift para aprender prácticamente todo lo que puede hacer con ellas.
6. No usar funciones funcionales
Swift Standard Library proporciona muchos métodos que son fundamentales en la programación funcional y nos permiten hacer mucho con una sola línea de código, como mapear, reducir y filtrar, entre otros.
Examinemos algunos ejemplos.
Digamos, tienes que calcular la altura de una vista de tabla. Dado que tiene una subclase UITableViewCell
como la siguiente:
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
Considere, tenemos una matriz de instancias de modelo modelArray
; podemos calcular la altura de la vista de tabla con una línea de código:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
El map
generará una matriz de CGFloat
, que contiene la altura de cada celda, y reduce
los sumará.
Si desea eliminar elementos de una matriz, puede terminar haciendo lo siguiente:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
Este ejemplo no parece elegante ni muy eficiente ya que estamos llamando a indexOf
para cada elemento. Considere el siguiente ejemplo:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
Ahora, el código es más eficiente, pero se puede mejorar aún más usando el filter
:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
El siguiente ejemplo ilustra cómo puede eliminar todas las subvistas de una UIView
que cumplan con ciertos criterios, como el marco que se cruza con un rectángulo en particular. Puedes usar algo como:
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
Sin embargo, debemos tener cuidado, ya que podría tener la tentación de encadenar un par de llamadas a estos métodos para crear un filtrado y una transformación sofisticados, lo que puede terminar con una línea de código espagueti ilegible.
7. Permanecer en la zona de confort y no probar la programación orientada a protocolos
Se afirma que Swift es el primer lenguaje de programación orientado a protocolos , como se menciona en la sesión Programación orientada a protocolos de la WWDC en Swift. Básicamente, eso significa que podemos modelar nuestros programas en torno a protocolos y agregar comportamiento a los tipos simplemente ajustándonos a los protocolos y extendiéndolos. Por ejemplo, dado que tenemos un protocolo Shape
, podemos extender CollectionType
(que está conformado por tipos como Array
, Set
, Dictionary
) y agregarle un método que calcule el área total que representa las intersecciones.
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
La instrucción where Generator.Element: Shape
es una restricción que establece que los métodos de la extensión solo estarán disponibles en instancias de tipos que se ajusten a CollectionType
, que contiene elementos de tipos que se ajusten a Shape
. Por ejemplo, estos métodos se pueden invocar en una instancia de Array<Shape>
, pero no en una instancia de Array<String>
. Si tenemos una clase Polygon
que se ajusta al protocolo Shape
, esos métodos también estarán disponibles para una instancia de Array<Polygon>
.
Con las extensiones de protocolo, puede otorgar una implementación predeterminada a los métodos declarados en el protocolo, que luego estarán disponibles en todos los tipos que se ajusten a ese protocolo sin tener que realizar ningún cambio en esos tipos (clases, estructuras o enumeraciones). Esto se hace ampliamente en la biblioteca estándar de Swift, por ejemplo, map
y reduce
se definen en una extensión de CollectionType
, y esta misma implementación se comparte con tipos como Array
y Dictionary
sin ningún código adicional.
Este comportamiento es similar a los mixins de otros lenguajes, como Ruby o Python. Al simplemente ajustarse a un protocolo con implementaciones de métodos predeterminados, agrega funcionalidad a su tipo.
La programación orientada a protocolos puede parecer bastante incómoda y no muy útil a primera vista, lo que podría hacer que la ignores y ni siquiera le des una oportunidad. Esta publicación brinda una buena comprensión del uso de la programación orientada a protocolos en aplicaciones reales.
Como aprendimos, Swift no es un lenguaje de juguete
Swift fue inicialmente recibido con mucho escepticismo; la gente parecía pensar que Apple iba a reemplazar Objective-C con un lenguaje de juguete para niños o con algo para no programadores. Sin embargo, Swift ha demostrado ser un lenguaje serio y poderoso que hace que la programación sea muy amena. Dado que está fuertemente tipado, es difícil cometer errores y, como tal, es difícil enumerar los errores que puede cometer con el idioma.
Cuando te acostumbres a Swift y vuelvas a Objective-C, notarás la diferencia. Te perderás las buenas características que ofrece Swift y tendrás que escribir código tedioso en Objective-C para lograr el mismo efecto. Otras veces, enfrentará errores de tiempo de ejecución que Swift habría detectado durante la compilación. Es una gran actualización para los programadores de Apple, y aún queda mucho por venir a medida que el lenguaje madure.