Los 10 errores más comunes que los desarrolladores de iOS no saben que están cometiendo
Publicado: 2022-03-11¿Qué es lo único peor que tener una aplicación con errores rechazada por la App Store? Tenerlo aceptado. Una vez que las reseñas de una estrella comienzan a llegar, es casi imposible recuperarse. Esto le cuesta dinero a las empresas y a los desarrolladores sus trabajos.
iOS es ahora el segundo sistema operativo móvil más grande del mundo. También tiene una tasa de adopción muy alta, con más del 85% de los usuarios en la última versión. Como era de esperar, los usuarios altamente comprometidos tienen grandes expectativas: si su aplicación o actualización no es perfecta, se enterará.
Dado que la demanda de desarrolladores de iOS continúa aumentando, muchos ingenieros se han pasado al desarrollo móvil (cada día se envían más de 1000 nuevas aplicaciones a Apple). Pero la verdadera experiencia en iOS se extiende mucho más allá de la codificación básica. A continuación se presentan 10 errores comunes de los que son víctimas los desarrolladores de iOS y cómo puede evitarlos.
Error común n.º 1: no comprender los procesos asincrónicos
Un tipo de error muy común entre los nuevos programadores es el manejo inadecuado del código asíncrono. Consideremos un escenario típico: un usuario abre una pantalla con la vista de tabla. Algunos datos se obtienen del servidor y se muestran en una vista de tabla. Podemos escribirlo más formalmente:
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
A primera vista, todo parece correcto: Obtenemos datos del servidor y luego actualizamos la interfaz de usuario. Sin embargo, el problema es que la obtención de datos es un proceso asíncrono y no devolverá nuevos datos inmediatamente, lo que significa que se llamará a reloadData
antes de recibir los nuevos datos. Para corregir este error, debemos mover la línea n.° 2 justo después de la línea n.° 1 dentro del bloque.
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
Sin embargo, puede haber situaciones en las que este código todavía no se comporte como se esperaba, lo que nos lleva a...
Error común n.º 2: ejecutar código relacionado con la interfaz de usuario en un subproceso que no sea la cola principal
Imaginemos que usamos un ejemplo de código corregido del error común anterior, pero nuestra vista de tabla aún no se actualiza con los nuevos datos incluso después de que el proceso asincrónico se haya completado con éxito. ¿Qué podría estar mal con un código tan simple? Para entenderlo, podemos establecer un punto de interrupción dentro del bloque y averiguar en qué cola se llama este bloque. Existe una alta probabilidad de que ocurra el comportamiento descrito porque nuestra llamada no está en la cola principal, donde se debe realizar todo el código relacionado con la interfaz de usuario.
Las bibliotecas más populares, como Alamofire, AFNetworking y Haneke, están diseñadas para llamar a completionBlock
en la cola principal después de realizar una tarea asincrónica. Sin embargo, no siempre puede confiar en esto y es fácil olvidar enviar su código a la cola correcta.
Para asegurarse de que todo su código relacionado con la interfaz de usuario esté en la cola principal, no olvide enviarlo a esa cola:
dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });
Error común n.º 3: no entender la concurrencia y los subprocesos múltiples
La concurrencia podría compararse con un cuchillo realmente afilado: puede cortarse fácilmente si no tiene el cuidado o la experiencia suficientes, pero es extremadamente útil y eficiente una vez que sabe cómo usarlo de manera adecuada y segura.
Puede intentar evitar el uso de la concurrencia, pero no importa qué tipo de aplicaciones esté creando, existe una gran posibilidad de que no pueda prescindir de ella. La simultaneidad puede tener beneficios significativos para su aplicación. Notablemente:
- Casi todas las aplicaciones tienen llamadas a servicios web (por ejemplo, para realizar algunos cálculos pesados o leer datos de una base de datos). Si estas tareas se realizan en la cola principal, la aplicación se congelará durante algún tiempo y dejará de responder. Además, si esto lleva demasiado tiempo, iOS cerrará la aplicación por completo. Mover estas tareas a otra cola le permite al usuario continuar usando la aplicación mientras se realiza la operación sin que parezca que la aplicación se congela.
- Los dispositivos iOS modernos tienen más de un núcleo, entonces, ¿por qué el usuario debería esperar a que las tareas terminen secuencialmente cuando se pueden realizar en paralelo?
Pero las ventajas de la concurrencia no vienen sin la complejidad y el potencial de introducir errores retorcidos, como condiciones de carrera que son realmente difíciles de reproducir.
Consideremos algunos ejemplos del mundo real (tenga en cuenta que se omite parte del código por simplicidad).
Caso 1
final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }
El código multiproceso:
let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }
A primera vista, todo está sincronizado y parece que debería funcionar como se esperaba, ya que ThreadSaveVar
ajusta el counter
y lo hace seguro para subprocesos. Desafortunadamente, esto no es cierto, ya que dos subprocesos pueden llegar a la línea de incremento simultáneamente y counter.value == someValue
nunca se volverá verdadero como resultado. Como solución alternativa, podemos hacer ThreadSafeCounter
que devuelve su valor después de incrementar:
class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }
Caso 2
struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }
En este caso, dispatch_barrier_sync
se usó para sincronizar el acceso a la matriz. Este es un patrón favorito para garantizar la sincronización de acceso. Desafortunadamente, este código no tiene en cuenta que struct hace una copia cada vez que le agregamos un elemento, por lo que tiene una nueva cola de sincronización cada vez.
Aquí, aunque parezca correcto a primera vista, es posible que no funcione como se esperaba. También requiere mucho trabajo probarlo y depurarlo, pero al final, puede mejorar la velocidad y la capacidad de respuesta de su aplicación.
Error común n.º 4: no conocer las trampas de los objetos mutables
Swift es muy útil para evitar errores con los tipos de valor, pero todavía hay muchos desarrolladores que usan Objective-C. Los objetos mutables son muy peligrosos y pueden generar problemas ocultos. Es una regla bien conocida que los objetos inmutables deben devolverse desde las funciones, pero la mayoría de los desarrolladores no saben por qué. Consideremos el siguiente código:
// Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end
El código anterior es correcto, porque NSMutableArray
es una subclase de NSArray
. Entonces, ¿qué puede salir mal con este código?
Lo primero y más obvio es que otro desarrollador podría aparecer y hacer lo siguiente:
NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }
Este código estropeará tu clase. Pero en ese caso, es un olor a código, y le corresponde al desarrollador recoger las piezas.
Este es el caso, sin embargo, que es mucho peor y demuestra un comportamiento inesperado:
Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];
La expectativa aquí es que [newChildBoxes count] > [childBoxes count]
, pero ¿y si no es así? Entonces la clase no está bien diseñada porque muta un valor que ya se devolvió. Si cree que la desigualdad no debería ser cierta, intente experimentar con UIView y [view subviews]
.
Afortunadamente, podemos arreglar fácilmente nuestro código, reescribiendo el captador del primer ejemplo:
- (NSArray *)boxes { return [self.m_boxes copy]; }
Error común n.° 5: no comprender cómo funciona internamente el NSDictionary
de iOS
Si alguna vez trabajó con una clase personalizada y NSDictionary
, es posible que se dé cuenta de que no puede usar su clase si no se ajusta a NSCopying
como clave de diccionario. La mayoría de los desarrolladores nunca se han preguntado por qué Apple agregó esa restricción. ¿Por qué Apple copia la clave y usa esa copia en lugar del objeto original?
La clave para comprender esto es descubrir cómo funciona internamente NSDictionary
. Técnicamente, es solo una tabla hash. Recapitulemos rápidamente cómo funciona en un alto nivel al agregar un objeto para una clave (aquí se omite el cambio de tamaño de la tabla y la optimización del rendimiento por simplicidad):
Paso 1: Calcula
hash(Key)
. Paso 2: Con base en el hash, busca un lugar para colocar el objeto. Por lo general, esto se hace tomando el módulo del valor hash con la longitud del diccionario. El índice resultante se usa luego para almacenar el par Clave/Valor. Paso 3: Si no hay ningún objeto en esa ubicación, crea una lista enlazada y almacena nuestro registro (objeto y clave). De lo contrario, agrega el registro al final de la lista.
Ahora, describamos cómo se obtiene un registro del diccionario:
Paso 1: Calcula
hash(Key)
. Paso 2: Busca una Clave por hash. Si no hay datos, se devuelvenil
. Paso 3: si hay una lista vinculada, itera a través del Objeto hasta[storedkey isEqual:Key]
.
Con esta comprensión de lo que está ocurriendo debajo del capó, se pueden sacar dos conclusiones:
- Si cambia el hash de la clave, el registro debe moverse a otra lista vinculada.
- Las claves deben ser únicas.
Examinemos esto en una clase simple:
@interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end
Ahora imagina que NSDictionary
no copia claves:
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;
¡Oh! ¡Tenemos un error tipográfico allí! ¡Arreglemoslo!
p.name = @"Jon Snow";
¿Qué debe pasar con nuestro diccionario? Como el nombre fue mutado, ahora tenemos un hash diferente. Ahora nuestro objeto se encuentra en el lugar equivocado (todavía tiene el valor hash anterior, ya que el diccionario no sabe sobre el cambio de datos), y no está muy claro qué hash debemos usar para buscar datos en el diccionario. Podría haber un caso aún peor. Imagínese si ya tuviéramos "Jon Snow" en nuestro diccionario con una calificación de 5. El diccionario terminaría con dos valores diferentes para la misma clave.
Como puede ver, hay muchos problemas que pueden surgir al tener claves mutables en NSDictionary
. La mejor práctica para evitar estos problemas es copiar el objeto antes de almacenarlo y marcar las propiedades como copy
. Esta práctica también le ayudará a mantener su clase consistente.
Error común n.º 6: usar guiones gráficos en lugar de XIB
La mayoría de los nuevos desarrolladores de iOS siguen la sugerencia de Apple y usan guiones gráficos de forma predeterminada para la interfaz de usuario. Sin embargo, hay muchos inconvenientes y solo unas pocas ventajas (discutibles) en el uso de guiones gráficos.
Los inconvenientes del guión gráfico incluyen:
- Es realmente difícil modificar un guión gráfico para varios miembros del equipo. Técnicamente, puede usar muchos guiones gráficos, pero la única ventaja, en ese caso, es que permite tener transiciones entre los controladores en el guión gráfico.
- Los nombres de los controladores y segues de los guiones gráficos son cadenas, por lo que debe volver a ingresar todas esas cadenas a lo largo de su código (y un día lo romperá ) o mantener una lista enorme de constantes del guión gráfico. Podría usar SBConstants, pero cambiar el nombre en el guión gráfico aún no es una tarea fácil.
- Los guiones gráficos te obligan a un diseño no modular. Mientras trabaja con un guión gráfico, hay muy pocos incentivos para hacer que sus vistas sean reutilizables. Esto puede ser aceptable para el producto mínimo viable (MVP) o la creación rápida de prototipos de interfaz de usuario, pero en aplicaciones reales es posible que deba usar la misma vista varias veces en su aplicación.
Ventajas del guión gráfico (discutibles):
- Toda la navegación de la aplicación se puede ver de un vistazo. Sin embargo, las aplicaciones reales pueden tener más de diez controladores, conectados en diferentes direcciones. Los guiones gráficos con tales conexiones parecen una bola de hilo y no brindan una comprensión de alto nivel de los flujos de datos.
- Mesas estáticas. Esta es la única ventaja real que se me ocurre. El problema es que el 90 por ciento de las tablas estáticas tiende a convertirse en tablas dinámicas durante la evolución de la aplicación y los XIB pueden manejar una tabla dinámica más fácilmente.
Error común n.º 7: Comparación confusa de objetos y punteros
Al comparar dos objetos, podemos considerar dos igualdades: puntero e igualdad de objetos.
La igualdad de punteros es una situación en la que ambos punteros apuntan al mismo objeto. En Objective-C, usamos el operador ==
para comparar dos punteros. La igualdad de objetos es una situación en la que dos objetos representan dos objetos lógicamente idénticos, como el mismo usuario de una base de datos. En Objective-C, usamos isEqual
, o incluso mejor, operadores específicos de tipo isEqualToString
, isEqualToDate
, etc. para comparar dos objetos.
Considere el siguiente código:
NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }
¿Qué se imprimirá en la consola cuando ejecutemos ese código? Obtendremos a is equal to b
, ya que ambos objetos a
y b
apuntan al mismo objeto en la memoria.
Pero ahora cambiemos la línea 2 a:
NSString *b = [[@"a" mutableCopy] copy];
Ahora obtenemos que a is NOT equal to b
ya que estos punteros ahora apuntan a diferentes objetos aunque esos objetos tengan los mismos valores.
Este problema se puede evitar confiando en isEqual
o escribiendo funciones específicas. En nuestro ejemplo de código, debemos reemplazar la línea 3 con el siguiente código para que siempre funcione correctamente:
if ([a isEqual:b]) {
Error común n.º 8: usar valores codificados
Hay dos problemas principales con los valores codificados:
- A menudo no está claro lo que representan.
- Deben volver a ingresarse (o copiarse y pegarse) cuando deben usarse en varios lugares del código.
Considere el siguiente ejemplo:
if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];
¿Qué representa 172800? ¿Por qué se está utilizando? Probablemente no sea obvio que esto corresponde a la cantidad de segundos en 2 días (hay 24 x 60 x 60, o 86 400 segundos en un día).
En lugar de usar valores codificados, puede definir un valor usando la instrucción #define
. Por ejemplo:
#define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"
#define
es una macro de preprocesador que reemplaza la definición nombrada con su valor en el código. Por lo tanto, si tiene #define
en un archivo de encabezado y lo importa en alguna parte, también se reemplazarán todas las apariciones del valor definido en ese archivo.
Esto funciona bien, excepto por un problema. Para ilustrar el problema restante, considere el siguiente código:
#define X = 3 ... CGFloat y = X / 2;
¿Cuál esperaría que fuera el valor de y
después de que se ejecute este código? Si dijiste 1.5, estás equivocado. y
será igual a 1 ( no 1.5) después de que se ejecute este código. ¿Por qué? La respuesta es que #define
no tiene información sobre el tipo. Entonces, en nuestro caso, tenemos una división de dos valores Int
(3 y 2), lo que da como resultado un Int
(es decir, 1) que luego se convierte en Float
.
Esto se puede evitar utilizando constantes que, por definición, se escriben:
static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected
Error común n.º 9: usar la palabra clave predeterminada en una declaración de cambio
El uso de la palabra clave default
en una declaración de cambio puede generar errores y comportamientos inesperados. Considere el siguiente código en Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }
El mismo código escrito en Swift:
enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }
Este código funciona según lo previsto, lo que permite que solo los usuarios administradores puedan cambiar otros registros. Sin embargo, ¿qué podría pasar si agregamos otro tipo de usuario, "administrador", que también debería poder editar registros? Si olvidamos actualizar esta declaración de switch
, el código se compilará, pero no funcionará como se esperaba. Sin embargo, si el desarrollador usó valores de enumeración en lugar de la palabra clave predeterminada desde el principio, el descuido se identificará en el momento de la compilación y podría corregirse antes de pasar a la prueba o producción. Aquí hay una buena manera de manejar esto en Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }
El mismo código escrito en Swift:
enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }
Error común n.º 10: usar NSLog
para iniciar sesión
Muchos desarrolladores de iOS usan NSLog
en sus aplicaciones para iniciar sesión, pero la mayoría de las veces esto es un terrible error. Si revisamos la documentación de Apple para la descripción de la función NSLog
, veremos que es muy simple:
void NSLog(NSString *format, ...);
¿Qué podría salir mal con eso? De hecho, nada. Sin embargo, si conecta su dispositivo al organizador Xcode, verá todos sus mensajes de depuración allí. Solo por esta razón, nunca debe usar NSLog
para iniciar sesión: es fácil mostrar algunos datos internos no deseados, además de que se ve poco profesional.
Un mejor enfoque es reemplazar NSLogs
con CocoaLumberjack configurable o algún otro marco de registro.
Envolver
iOS es una plataforma muy potente y en rápida evolución. Apple hace un gran esfuerzo continuo para introducir nuevo hardware y funciones para el propio iOS, al mismo tiempo que expande continuamente el lenguaje Swift.
Mejorar sus habilidades en Objective-C y Swift lo convertirá en un excelente desarrollador de iOS y le ofrecerá oportunidades para trabajar en proyectos desafiantes utilizando tecnologías de vanguardia.