I 10 errori più comuni che gli sviluppatori iOS non sanno che stanno facendo

Pubblicato: 2022-03-11

Qual è l'unica cosa peggiore che avere un'app con bug rifiutata dall'App Store? Averlo accettato. Una volta che le recensioni a una stella iniziano ad arrivare, è quasi impossibile recuperare. Questo costa alle aziende denaro e agli sviluppatori il loro lavoro.

iOS è ora il secondo sistema operativo mobile più grande al mondo. Ha anche un tasso di adozione molto alto, con oltre l'85% degli utenti sull'ultima versione. Come ci si potrebbe aspettare, gli utenti altamente coinvolti hanno grandi aspettative: se la tua app o l'aggiornamento non sono impeccabili, ne sentirai parlare.

Con la domanda di sviluppatori iOS che continua a salire alle stelle, molti ingegneri sono passati allo sviluppo mobile (ogni giorno vengono inviate ad Apple più di 1.000 nuove app). Ma la vera esperienza di iOS si estende ben oltre la codifica di base. Di seguito sono riportati 10 errori comuni di cui cadono preda gli sviluppatori iOS e come evitarli.

L'85% degli utenti iOS utilizza l'ultima versione del sistema operativo. Ciò significa che si aspettano che la tua app o l'aggiornamento siano impeccabili.
Twitta

Errore comune n. 1: non comprendere i processi asincroni

Un tipo di errore molto comune tra i nuovi programmatori è la gestione impropria del codice asincrono. Consideriamo uno scenario tipico: un utente apre una schermata con la vista tabella. Alcuni dati vengono recuperati dal server e visualizzati in una vista tabella. Possiamo scriverlo in modo più formale:

 @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 prima vista, tutto sembra a posto: prendiamo i dati dal server e quindi aggiorniamo l'interfaccia utente. Tuttavia, il problema è che il recupero dei dati è un processo asincrono e non restituirà immediatamente nuovi dati, il che significa che reloadData verrà chiamato prima di ricevere i nuovi dati. Per correggere questo errore, dovremmo spostare la riga n. 2 subito dopo la riga n. 1 all'interno del blocco.

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

Tuttavia, potrebbero esserci situazioni in cui questo codice non si comporta ancora come previsto, il che ci porta a ...

Errore comune n. 2: eseguire codice relativo all'interfaccia utente su un thread diverso dalla coda principale

Immaginiamo di aver utilizzato un esempio di codice corretto dal precedente errore comune, ma la nostra vista tabella non viene ancora aggiornata con i nuovi dati anche dopo che il processo asincrono è stato completato con successo. Cosa potrebbe esserci di sbagliato in un codice così semplice? Per capirlo, possiamo impostare un punto di interruzione all'interno del blocco e scoprire su quale coda viene chiamato questo blocco. C'è un'alta probabilità che si verifichi il comportamento descritto perché la nostra chiamata non è nella coda principale, dove dovrebbe essere eseguito tutto il codice relativo all'interfaccia utente.

Le librerie più popolari, come Alamofire, AFNetworking e Haneke, sono progettate per chiamare il completionBlock sulla coda principale dopo aver eseguito un'attività asincrona. Tuttavia, non puoi sempre fare affidamento su questo ed è facile dimenticare di inviare il tuo codice alla coda giusta.

Per assicurarti che tutto il tuo codice relativo all'interfaccia utente sia nella coda principale, non dimenticare di inviarlo a quella coda:

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

Errore comune n. 3: Incomprensione della concorrenza e multithreading

La concorrenza potrebbe essere paragonata a un coltello davvero affilato: puoi facilmente tagliarti se non sei abbastanza attento o esperto, ma è estremamente utile ed efficiente una volta che sai come usarlo in modo corretto e sicuro.

Puoi provare a evitare di usare la concorrenza, ma indipendentemente dal tipo di app che stai creando, c'è un'alta probabilità che tu non possa farne a meno. La concorrenza può avere vantaggi significativi per la tua applicazione. In particolare:

  • Quasi tutte le applicazioni hanno chiamate a servizi Web (ad esempio, per eseguire calcoli pesanti o leggere dati da un database). Se queste attività vengono eseguite sulla coda principale, l'applicazione si bloccherà per un po' di tempo, rendendola non rispondente. Inoltre, se questo richiede troppo tempo, iOS chiuderà completamente l'app. Lo spostamento di queste attività in un'altra coda consente all'utente di continuare a utilizzare l'applicazione durante l'esecuzione dell'operazione senza che l'app sembri bloccarsi.
  • I moderni dispositivi iOS hanno più di un core, quindi perché l'utente dovrebbe attendere che le attività finiscano in sequenza quando possono essere eseguite in parallelo?

Ma i vantaggi della concorrenza non derivano dalla complessità e dalla possibilità di introdurre bug nodosi, come condizioni di gara che sono davvero difficili da riprodurre.

Consideriamo alcuni esempi del mondo reale (notare che alcuni codici vengono omessi per semplicità).

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

Il codice multithread:

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

A prima vista, tutto è sincronizzato e sembra funzionare come previsto, poiché ThreadSaveVar wrapping del counter e lo rende thread-safe. Sfortunatamente, questo non è vero, poiché due thread potrebbero raggiungere la linea di incremento contemporaneamente e counter.value == someValue non diventerà mai vero di conseguenza. Come soluzione alternativa, possiamo creare ThreadSafeCounter che restituisce il suo valore dopo l'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 } } }

In questo caso, dispatch_barrier_sync è stato utilizzato per sincronizzare l'accesso all'array. Questo è uno schema preferito per garantire la sincronizzazione degli accessi. Sfortunatamente, questo codice non tiene conto del fatto che struct esegue una copia ogni volta che aggiungiamo un elemento ad esso, avendo così una nuova coda di sincronizzazione ogni volta.

Qui, anche se a prima vista sembra corretto, potrebbe non funzionare come previsto. Richiede anche molto lavoro per testarlo ed eseguirne il debug, ma alla fine puoi migliorare la velocità e la reattività dell'app.

Errore comune n. 4: non conoscere le insidie ​​degli oggetti mutevoli

Swift è molto utile per evitare errori con i tipi di valore, ma ci sono ancora molti sviluppatori che usano Objective-C. Gli oggetti mutevoli sono molto pericolosi e possono portare a problemi nascosti. È una regola ben nota che gli oggetti immutabili dovrebbero essere restituiti dalle funzioni, ma la maggior parte degli sviluppatori non sa perché. Consideriamo il seguente codice:

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

Il codice sopra è corretto, perché NSMutableArray è una sottoclasse di NSArray . Quindi cosa può andare storto con questo codice?

La prima e più ovvia cosa è che un altro sviluppatore potrebbe arrivare e fare quanto segue:

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

Questo codice rovinerà la tua classe. Ma in tal caso, è un odore di codice e spetta allo sviluppatore raccogliere i pezzi.

Ecco il caso, però, che è molto peggio e mostra un comportamento inaspettato:

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

L'aspettativa qui è che [newChildBoxes count] > [childBoxes count] , ma se non lo fosse? Quindi la classe non è ben progettata perché muta un valore che è stato già restituito. Se ritieni che la disuguaglianza non debba essere vera, prova a sperimentare con UIView e [view subviews] .

Fortunatamente, possiamo facilmente correggere il nostro codice, riscrivendo il getter dal primo esempio:

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

Errore comune n. 5: non capire come funziona internamente iOS NSDictionary

Se hai mai lavorato con una classe personalizzata e NSDictionary , potresti renderti conto che non puoi usare la tua classe se non è conforme a NSCopying come chiave del dizionario. La maggior parte degli sviluppatori non si è mai chiesto perché Apple abbia aggiunto quella restrizione. Perché Apple copia la chiave e usa quella copia invece dell'oggetto originale?

La chiave per capirlo è capire come funziona internamente NSDictionary . Tecnicamente, è solo una tabella hash. Ricapitoliamo rapidamente come funziona ad alto livello aggiungendo un oggetto per una chiave (il ridimensionamento della tabella e l'ottimizzazione delle prestazioni sono omessi qui per semplicità):

Passaggio 1: calcola hash(Key) . Passaggio 2: in base all'hash, cerca un posto dove mettere l'oggetto. Di solito, questo viene fatto prendendo il modulo del valore hash con la lunghezza del dizionario. L'indice risultante viene quindi utilizzato per memorizzare la coppia chiave/valore. Passaggio 3: se non ci sono oggetti in quella posizione, crea un elenco collegato e memorizza il nostro record (oggetto e chiave). In caso contrario, aggiunge il record alla fine dell'elenco.

Ora, descriviamo come viene recuperato un record dal dizionario:

Passaggio 1: calcola hash(Key) . Passaggio 2: ricerca una chiave tramite hash. Se non ci sono dati, viene restituito nil . Passaggio 3: se è presente un elenco collegato, scorre l'oggetto fino [storedkey isEqual:Key] .

Con questa comprensione di ciò che sta accadendo sotto il cofano, si possono trarre due conclusioni:

  1. Se l'hash della chiave cambia, il record dovrebbe essere spostato in un altro elenco collegato.
  2. Le chiavi dovrebbero essere univoche.

Esaminiamo questo su una classe semplice:

 @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

Ora immagina che NSDictionary non copi le chiavi:

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

Oh! Abbiamo un errore di battitura! Risolviamolo!

 p.name = @"Jon Snow";

Cosa dovrebbe succedere con il nostro dizionario? Poiché il nome è stato mutato, ora abbiamo un hash diverso. Ora il nostro oggetto si trova nel posto sbagliato (ha ancora il vecchio valore hash, poiché il dizionario non è a conoscenza della modifica dei dati) e non è molto chiaro quale hash dovremmo usare per cercare i dati nel dizionario. Potrebbe esserci un caso ancora peggiore. Immagina se avessimo già "Jon Snow" nel nostro dizionario con una valutazione di 5. Il dizionario finirebbe con due valori diversi per la stessa chiave.

Come puoi vedere, ci sono molti problemi che possono sorgere dall'avere chiavi mutabili in NSDictionary . La procedura migliore per evitare tali problemi è copiare l'oggetto prima di archiviarlo e contrassegnare le proprietà come copy . Questa pratica ti aiuterà anche a mantenere la tua classe coerente.

Errore comune n. 6: usare storyboard invece di XIB

La maggior parte dei nuovi sviluppatori iOS segue il suggerimento di Apple e utilizza gli storyboard per impostazione predefinita per l'interfaccia utente. Tuttavia, ci sono molti inconvenienti e solo pochi vantaggi (discutibili) nell'uso degli storyboard.

Gli svantaggi dello storyboard includono:

  1. È davvero difficile modificare uno storyboard per diversi membri del team. Tecnicamente, puoi utilizzare molti storyboard, ma l'unico vantaggio, in tal caso, è la possibilità di avere passaggi tra i controller sullo storyboard.
  2. I nomi dei controller e dei seguis dagli storyboard sono stringhe, quindi devi reinserire tutte quelle stringhe nel codice (e un giorno lo interromperai) o mantenere un enorme elenco di costanti dello storyboard. Potresti usare SBConstants, ma rinominare lo storyboard non è ancora un compito facile.
  3. Gli storyboard ti costringono a un design non modulare. Mentre si lavora con uno storyboard, c'è molto poco incentivo a rendere le tue viste riutilizzabili. Questo può essere accettabile per il prodotto minimo valido (MVP) o per la prototipazione rapida dell'interfaccia utente, ma nelle applicazioni reali potrebbe essere necessario utilizzare la stessa visualizzazione più volte nell'app.

Vantaggi dello Storyboard (discutibili):

  1. L'intera navigazione dell'app può essere vista a colpo d'occhio. Tuttavia, le applicazioni reali possono avere più di dieci controller, collegati in direzioni diverse. Gli storyboard con tali connessioni sembrano un gomitolo di lana e non forniscono alcuna comprensione di alto livello dei flussi di dati.
  2. Tabelle statiche. Questo è l'unico vero vantaggio che mi viene in mente. Il problema è che il 90 percento delle tabelle statiche tende a trasformarsi in tabelle dinamiche durante l'evoluzione dell'app e una tabella dinamica può essere gestita più facilmente dagli XIB.

Errore comune n. 7: confronto tra oggetti e puntatori che confonde

Confrontando due oggetti, possiamo considerare due uguaglianza: puntatore e uguaglianza di oggetti.

L'uguaglianza dei puntatori è una situazione in cui entrambi i puntatori puntano allo stesso oggetto. In Objective-C, utilizziamo l'operatore == per confrontare due puntatori. L'uguaglianza degli oggetti è una situazione in cui due oggetti rappresentano due oggetti logicamente identici, come lo stesso utente di un database. In Objective-C, utilizziamo isEqual , o ancora meglio, operatori specifici del tipo isEqualToString , isEqualToDate , ecc. per confrontare due oggetti.

Considera il seguente codice:

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

Cosa verrà stampato sulla console quando eseguiamo quel codice? Otterremo a is equal to b b a allo stesso oggetto in memoria.

Ma ora cambiamo la riga 2 in:

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

Ora otteniamo a is NOT equal to b poiché questi puntatori ora puntano a oggetti diversi anche se quegli oggetti hanno gli stessi valori.

Questo problema può essere evitato basandosi su isEqual o digitando funzioni specifiche. Nel nostro esempio di codice, dovremmo sostituire la riga 3 con il codice seguente affinché funzioni sempre correttamente:

 if ([a isEqual:b]) {

Errore comune n. 8: utilizzo di valori hardcoded

Ci sono due problemi principali con i valori hardcoded:

  1. Spesso non è chiaro cosa rappresentino.
  2. Devono essere reinseriti (o copiati e incollati) quando devono essere utilizzati in più punti del codice.

Considera il seguente esempio:

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

Cosa rappresenta 172800? Perché viene utilizzato? Probabilmente non è ovvio che questo corrisponda al numero di secondi in 2 giorni (ci sono 24 x 60 x 60, o 86.400 secondi in un giorno).

Anziché utilizzare valori codificati, è possibile definire un valore utilizzando l'istruzione #define . Per esempio:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define è una macro del preprocessore che sostituisce la definizione denominata con il relativo valore nel codice. Quindi, se hai #define in un file di intestazione e lo importi da qualche parte, anche tutte le occorrenze del valore definito in quel file verranno sostituite.

Funziona bene, tranne che per un problema. Per illustrare il problema rimanente, considera il codice seguente:

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

Quale ti aspetteresti che sia il valore di y dopo l'esecuzione di questo codice? Se hai detto 1.5, non sei corretto. y sarà uguale a 1 ( non 1.5) dopo l'esecuzione di questo codice. Come mai? La risposta è che #define non ha informazioni sul tipo. Quindi, nel nostro caso, abbiamo una divisione di due valori Int (3 e 2), che risulta in un Int (cioè, 1) che viene poi convertito in un Float .

Questo può essere evitato utilizzando invece costanti che sono, per definizione, digitate:

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

Errore comune n. 9: utilizzo di una parola chiave predefinita in un'istruzione Switch

L'utilizzo della parola chiave default in un'istruzione switch può portare a bug e comportamenti imprevisti. Considera il seguente codice in Objective-C:

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

Lo stesso codice scritto in Swift:

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

Questo codice funziona come previsto, consentendo solo agli utenti amministratori di poter modificare altri record. Tuttavia, cosa potrebbe succedere se aggiungiamo un altro tipo di utente, "manager", che dovrebbe essere in grado di modificare anche i record? Se dimentichiamo di aggiornare questa istruzione switch , il codice verrà compilato, ma non funzionerà come previsto. Tuttavia, se lo sviluppatore ha utilizzato i valori enum invece della parola chiave predefinita fin dall'inizio, la svista verrà identificata in fase di compilazione e potrebbe essere corretta prima di passare al test o alla produzione. Ecco un buon modo per gestirlo in 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; } }

Lo stesso codice scritto in 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 } }

Errore comune n. 10: utilizzo di NSLog per la registrazione

Molti sviluppatori iOS utilizzano NSLog nelle loro app per la registrazione, ma il più delle volte questo è un terribile errore. Se controlliamo la documentazione Apple per la descrizione della funzione NSLog , vedremo che è molto semplice:

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

Cosa potrebbe andare storto con esso? In effetti, niente. Tuttavia, se colleghi il tuo dispositivo all'organizer Xcode, vedrai tutti i tuoi messaggi di debug lì. Solo per questo motivo, non dovresti mai usare NSLog per la registrazione: è facile mostrare alcuni dati interni indesiderati, inoltre sembra poco professionale.

Un approccio migliore consiste nel sostituire NSLogs con CocoaLumberjack configurabile o qualche altro framework di registrazione.

Incartare

iOS è una piattaforma molto potente e in rapida evoluzione. Apple fa un enorme sforzo continuo per introdurre nuovo hardware e funzionalità per lo stesso iOS, espandendo anche continuamente il linguaggio Swift.

Migliorare le tue abilità con Objective-C e Swift ti renderà un ottimo sviluppatore iOS e ti offrirà l'opportunità di lavorare su progetti impegnativi utilizzando tecnologie all'avanguardia.

Correlati: una guida per sviluppatori iOS: da Objective-C a Learning Swift