Os 10 erros mais comuns que os desenvolvedores iOS não sabem que estão cometendo

Publicados: 2022-03-11

Qual é a única coisa pior do que ter um aplicativo com bugs rejeitado pela App Store? Tendo-o aceito. Uma vez que as avaliações de uma estrela começam a aparecer, é quase impossível recuperar. Isso custa dinheiro às empresas e aos desenvolvedores seus empregos.

O iOS é agora o segundo maior sistema operacional móvel do mundo. Ele também tem uma taxa de adoção muito alta, com mais de 85% dos usuários na versão mais recente. Como você pode esperar, usuários altamente engajados têm grandes expectativas – se seu aplicativo ou atualização não for impecável, você ouvirá falar.

Com a demanda por desenvolvedores iOS continuando a disparar, muitos engenheiros mudaram para o desenvolvimento móvel (mais de 1.000 novos aplicativos são enviados à Apple todos os dias). Mas a verdadeira experiência em iOS vai muito além da codificação básica. Abaixo estão 10 erros comuns que os desenvolvedores do iOS são vítimas e como você pode evitá-los.

85% dos usuários do iOS usam a versão mais recente do sistema operacional. Isso significa que eles esperam que seu aplicativo ou atualização seja impecável.
Tweet

Erro comum nº 1: não entender os processos assíncronos

Um tipo de erro muito comum entre os novos programadores é manipular código assíncrono de forma inadequada. Vamos considerar um cenário típico: um usuário abre uma tela com a visualização de tabela. Alguns dados são obtidos do servidor e exibidos em uma visualização de tabela. Podemos escrevê-lo mais 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; }

À primeira vista, tudo parece certo: buscamos dados do servidor e atualizamos a interface do usuário. No entanto, o problema é que a busca de dados é um processo assíncrono e não retornará novos dados imediatamente, o que significa que reloadData será chamado antes de receber os novos dados. Para corrigir esse erro, devemos mover a linha #2 logo após a linha #1 dentro do bloco.

 @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; }

No entanto, pode haver situações em que este código ainda não se comporta como esperado, o que nos leva a…

Erro comum nº 2: execução de código relacionado à interface do usuário em um thread que não seja a fila principal

Vamos imaginar que usamos um exemplo de código corrigido do erro comum anterior, mas nossa visualização de tabela ainda não foi atualizada com os novos dados, mesmo após a conclusão bem-sucedida do processo assíncrono. O que pode estar errado com um código tão simples? Para entender isso, podemos definir um ponto de interrupção dentro do bloco e descobrir em qual fila esse bloco é chamado. Há uma grande chance de que o comportamento descrito esteja acontecendo porque nossa chamada não está na fila principal, onde todo o código relacionado à interface do usuário deve ser executado.

As bibliotecas mais populares, como Alamofire, AFNetworking e Haneke, são projetadas para chamar completionBlock na fila principal após executar uma tarefa assíncrona. No entanto, você nem sempre pode confiar nisso e é fácil esquecer de enviar seu código para a fila certa.

Para garantir que todo o código relacionado à interface do usuário esteja na fila principal, não se esqueça de despachá-lo para essa fila:

 dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });

Erro comum nº 3: mal-entendido sobre simultaneidade e multithreading

A simultaneidade pode ser comparada a uma faca realmente afiada: você pode se cortar facilmente se não for cuidadoso ou experiente o suficiente, mas é extremamente útil e eficiente quando você sabe como usá-lo corretamente e com segurança.

Você pode tentar evitar o uso de simultaneidade, mas não importa que tipo de aplicativo você esteja criando, há uma chance muito grande de você não poder ficar sem ele. A simultaneidade pode ter benefícios significativos para seu aplicativo. Notavelmente:

  • Quase todos os aplicativos têm chamadas para serviços da Web (por exemplo, para realizar alguns cálculos pesados ​​ou ler dados de um banco de dados). Se essas tarefas forem executadas na fila principal, o aplicativo congelará por algum tempo, deixando-o sem resposta. Além disso, se isso demorar muito, o iOS encerrará o aplicativo completamente. Mover essas tarefas para outra fila permite que o usuário continue usando o aplicativo enquanto a operação está sendo executada sem que o aplicativo pareça estar congelado.
  • Dispositivos iOS modernos têm mais de um núcleo, então por que o usuário deve esperar que as tarefas terminem sequencialmente quando elas podem ser executadas em paralelo?

Mas as vantagens da simultaneidade não vêm sem complexidade e o potencial para a introdução de bugs gnarly, como condições de corrida que são realmente difíceis de reproduzir.

Vamos considerar alguns exemplos do mundo real (observe que alguns códigos são omitidos por simplicidade).

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 } } } }

O código multithread:

 let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }

À primeira vista, tudo está sincronizado e aparece como se devesse funcionar conforme o esperado, já que ThreadSaveVar envolve o counter e o torna seguro para threads. Infelizmente, isso não é verdade, pois duas threads podem alcançar a linha de incremento simultaneamente e counter.value == someValue nunca se tornará true como resultado. Como solução alternativa, podemos fazer ThreadSafeCounter que retorna seu valor após o incremento:

 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 } } }

Nesse caso, dispatch_barrier_sync foi usado para sincronizar o acesso ao array. Este é um padrão favorito para garantir a sincronização de acesso. Infelizmente, esse código não leva em conta que struct faz uma cópia toda vez que anexamos um item a ele, tendo assim uma nova fila de sincronização a cada vez.

Aqui, mesmo que pareça correto à primeira vista, pode não funcionar como esperado. Também requer muito trabalho para testá-lo e depurá-lo, mas, no final, você pode melhorar a velocidade e a capacidade de resposta do seu aplicativo.

Erro comum nº 4: não conhecer as armadilhas dos objetos mutáveis

Swift é muito útil para evitar erros com tipos de valor, mas ainda existem muitos desenvolvedores que usam Objective-C. Objetos mutáveis ​​são muito perigosos e podem levar a problemas ocultos. É uma regra bem conhecida que objetos imutáveis ​​devem ser retornados de funções, mas a maioria dos desenvolvedores não sabe por quê. Vamos considerar o seguinte 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

O código acima está correto, porque NSMutableArray é uma subclasse de NSArray . Então, o que pode dar errado com este código?

A primeira e mais óbvia coisa é que outro desenvolvedor pode aparecer e fazer o seguinte:

 NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }

Este código vai atrapalhar sua classe. Mas, nesse caso, é um cheiro de código, e cabe a esse desenvolvedor juntar os pedaços.

Aqui está o caso, porém, que é muito pior e demonstra um comportamento inesperado:

 Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];

A expectativa aqui é que [newChildBoxes count] > [childBoxes count] , mas e se não for? Então, a classe não está bem projetada porque altera um valor que já foi retornado. Se você acredita que a desigualdade não deve ser verdadeira, experimente UIView e [view subviews] .

Felizmente, podemos corrigir facilmente nosso código, reescrevendo o getter do primeiro exemplo:

 - (NSArray *)boxes { return [self.m_boxes copy]; }

Erro comum nº 5: não entender como o iOS NSDictionary funciona internamente

Se você já trabalhou com uma classe personalizada e NSDictionary , pode perceber que não pode usar sua classe se ela não estiver em conformidade com NSCopying como uma chave de dicionário. A maioria dos desenvolvedores nunca se perguntou por que a Apple adicionou essa restrição. Por que a Apple copia a chave e usa essa cópia em vez do objeto original?

A chave para entender isso é descobrir como o NSDictionary funciona internamente. Tecnicamente, é apenas uma tabela de hash. Vamos recapitular rapidamente como ele funciona em alto nível ao adicionar um objeto para uma chave (o redimensionamento da tabela e a otimização de desempenho são omitidos aqui para simplificar):

Passo 1: Calcula hash(Key) . Passo 2: Com base no hash, ele procura um local para colocar o objeto. Normalmente, isso é feito tomando o módulo do valor de hash com o comprimento do dicionário. O índice resultante é então usado para armazenar o par Chave/Valor. Passo 3: Se não houver nenhum objeto nesse local, ele cria uma lista encadeada e armazena nosso registro (objeto e chave). Caso contrário, ele anexa o registro ao final da lista.

Agora, vamos descrever como um registro é buscado no dicionário:

Passo 1: Calcula hash(Key) . Passo 2: Pesquisa uma chave por hash. Se não houver dados, nil será retornado. Etapa 3: Se houver uma lista vinculada, ela itera pelo Object até [storedkey isEqual:Key] .

Com essa compreensão do que está ocorrendo sob o capô, duas conclusões podem ser tiradas:

  1. Se o hash da chave for alterado, o registro deverá ser movido para outra lista vinculada.
  2. As chaves devem ser únicas.

Vamos examinar isso em uma classe simples:

 @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

Agora imagine que o NSDictionary não copie chaves:

 NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;

Oh! Temos um erro de digitação aí! Vamos corrigi-lo!

 p.name = @"Jon Snow";

O que deve acontecer com o nosso dicionário? Como o nome foi alterado, agora temos um hash diferente. Agora nosso objeto está no lugar errado (ele ainda tem o antigo valor de hash, pois o dicionário não sabe sobre a mudança de dados), e não está muito claro qual hash devemos usar para pesquisar dados no dicionário. Poderia haver um caso ainda pior. Imagine se já tivéssemos “Jon Snow” em nosso dicionário com uma classificação de 5. O dicionário terminaria com dois valores diferentes para a mesma chave.

Como você pode ver, existem muitos problemas que podem surgir por ter chaves mutáveis ​​no NSDictionary . A melhor prática para evitar esses problemas é copiar o objeto antes de armazená-lo e marcar as propriedades como copy . Esta prática também irá ajudá-lo a manter sua classe consistente.

Erro comum nº 6: usar storyboards em vez de XIBs

A maioria dos novos desenvolvedores iOS segue a sugestão da Apple e usa storyboards por padrão para a interface do usuário. No entanto, existem muitas desvantagens e apenas algumas vantagens (discutíveis) no uso de storyboards.

As desvantagens do storyboard incluem:

  1. É muito difícil modificar um storyboard para vários membros da equipe. Tecnicamente, você pode usar muitos storyboards, mas a única vantagem, nesse caso, é possibilitar as sequências entre os controladores no storyboard.
  2. Nomes de controladores e segues de storyboards são strings, então você precisa inserir novamente todas essas strings em todo o seu código (e um dia você o quebrará ) ou manter uma lista enorme de constantes de storyboard. Você poderia usar SBConstants, mas renomear no storyboard ainda não é uma tarefa fácil.
  3. Os storyboards forçam você a um design não modular. Ao trabalhar com um storyboard, há muito pouco incentivo para tornar suas visualizações reutilizáveis. Isso pode ser aceitável para o produto mínimo viável (MVP) ou prototipagem de interface do usuário rápida, mas em aplicativos reais pode ser necessário usar a mesma visualização várias vezes em seu aplicativo.

Vantagens do storyboard (discutíveis):

  1. Toda a navegação do aplicativo pode ser vista de relance. No entanto, aplicações reais podem ter mais de dez controladores, conectados em direções diferentes. Os storyboards com essas conexões parecem um novelo de lã e não fornecem nenhuma compreensão de alto nível dos fluxos de dados.
  2. Tabelas estáticas. Esta é a única vantagem real em que posso pensar. O problema é que 90% das tabelas estáticas tendem a se transformar em tabelas dinâmicas durante a evolução do aplicativo e uma tabela dinâmica pode ser mais facilmente manipulada por XIBs.

Erro comum nº 7: Comparação confusa de objetos e ponteiros

Ao comparar dois objetos, podemos considerar duas igualdades: igualdade de ponteiro e de objeto.

A igualdade de ponteiro é uma situação em que ambos os ponteiros apontam para o mesmo objeto. Em Objective-C, usamos o operador == para comparar dois ponteiros. A igualdade de objetos é uma situação em que dois objetos representam dois objetos logicamente idênticos, como o mesmo usuário de um banco de dados. Em Objective-C, usamos isEqual , ou ainda melhor, operadores específicos de tipo isEqualToString , isEqualToDate , etc. para comparar dois objetos.

Considere o seguinte 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); }

O que será impresso no console quando executarmos esse código? Teremos a is equal to b , pois ambos os objetos a e b estão apontando para o mesmo objeto na memória.

Mas agora vamos mudar a linha 2 para:

 NSString *b = [[@"a" mutableCopy] copy];

Agora obtemos que a is NOT equal to b pois esses ponteiros agora estão apontando para objetos diferentes, embora esses objetos tenham os mesmos valores.

Esse problema pode ser evitado contando com isEqual ou funções específicas de tipo. Em nosso exemplo de código, devemos substituir a linha 3 pelo código a seguir para que funcione sempre corretamente:

 if ([a isEqual:b]) {

Erro comum nº 8: usando valores codificados permanentemente

Há dois problemas principais com valores codificados:

  1. Muitas vezes não é claro o que eles representam.
  2. Eles precisam ser reinseridos (ou copiados e colados) quando precisam ser usados ​​em vários lugares no código.

Considere o seguinte exemplo:

 if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];

O que 172800 representa? Por que está sendo usado? Provavelmente não é óbvio que isso corresponda ao número de segundos em 2 dias (há 24 x 60 x 60, ou 86.400 segundos em um dia).

Em vez de usar valores codificados, você pode definir um valor usando a instrução #define . Por exemplo:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define é uma macro de pré-processador que substitui a definição nomeada por seu valor no código. Portanto, se você tiver #define em um arquivo de cabeçalho e importá-lo em algum lugar, todas as ocorrências do valor definido nesse arquivo também serão substituídas.

Isso funciona bem, exceto por um problema. Para ilustrar o problema restante, considere o seguinte código:

 #define X = 3 ... CGFloat y = X / 2;

Qual seria o valor de y após a execução desse código? Se você disse 1,5, você está incorreto. y será igual a 1 ( não 1,5) após a execução deste código. Por quê? A resposta é que #define não tem informações sobre o tipo. Então, no nosso caso, temos uma divisão de dois valores Int (3 e 2), que resulta em um Int (ou seja, 1) que é então convertido em um Float .

Isso pode ser evitado usando constantes que são, por definição, digitadas:

 static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected

Erro comum nº 9: usando a palavra-chave padrão em uma instrução de switch

Usar a palavra-chave default em uma instrução switch pode levar a erros e comportamento inesperado. Considere o seguinte código em Objective-C:

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }

O mesmo código escrito em Swift:

 enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }

Esse código funciona conforme o esperado, permitindo que apenas usuários administradores possam alterar outros registros. No entanto, o que pode acontecer se adicionarmos outro tipo de usuário, “gerente”, que também deve ser capaz de editar registros? Se esquecermos de atualizar esta instrução switch , o código será compilado, mas não funcionará conforme o esperado. No entanto, se o desenvolvedor usou valores enum em vez da palavra-chave padrão desde o início, a supervisão será identificada em tempo de compilação e poderá ser corrigida antes de ir para teste ou produção. Aqui está uma boa maneira de lidar com isso em 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; } }

O mesmo código escrito em 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 } }

Erro comum nº 10: usando NSLog para registro

Muitos desenvolvedores iOS usam o NSLog em seus aplicativos para registro, mas na maioria das vezes isso é um erro terrível. Se verificarmos a documentação da Apple para a descrição da função NSLog , veremos que é muito simples:

 void NSLog(NSString *format, ...);

O que poderia dar errado com isso? Na verdade, nada. No entanto, se você conectar seu dispositivo ao organizador do Xcode, verá todas as suas mensagens de depuração lá. Por esse motivo, você nunca deve usar o NSLog para registro: é fácil mostrar alguns dados internos indesejados, além de parecer pouco profissional.

A melhor abordagem é substituir NSLogs por CocoaLumberjack configurável ou alguma outra estrutura de log.

Embrulhar

iOS é uma plataforma muito poderosa e em rápida evolução. A Apple faz um grande esforço contínuo para introduzir novos hardwares e recursos para o próprio iOS, enquanto também expande continuamente a linguagem Swift.

Aprimorar suas habilidades em Objective-C e Swift fará de você um ótimo desenvolvedor iOS e oferecerá oportunidades de trabalhar em projetos desafiadores usando tecnologias de ponta.

Relacionado: Guia do desenvolvedor iOS: do Objective-C ao aprendizado Swift