Die 10 häufigsten Fehler, von denen iOS-Entwickler nicht wissen, dass sie sie machen

Veröffentlicht: 2022-03-11

Was ist schlimmer, als wenn eine fehlerhafte App vom App Store abgelehnt wird? Es angenommen zu haben. Sobald die Ein-Stern-Bewertungen eintreffen, ist es fast unmöglich, sich davon zu erholen. Das kostet Unternehmen Geld und Entwickler ihre Jobs.

iOS ist heute das zweitgrößte mobile Betriebssystem der Welt. Es hat auch eine sehr hohe Akzeptanzrate, mit mehr als 85 % der Benutzer auf der neuesten Version. Wie zu erwarten, haben sehr engagierte Benutzer hohe Erwartungen – wenn Ihre App oder Ihr Update nicht fehlerfrei ist, werden Sie davon erfahren.

Da die Nachfrage nach iOS-Entwicklern weiterhin sprunghaft ansteigt, sind viele Ingenieure auf die mobile Entwicklung umgestiegen (täglich werden mehr als 1.000 neue Apps bei Apple eingereicht). Aber wahres iOS-Know-how geht weit über das grundlegende Programmieren hinaus. Nachfolgend finden Sie 10 häufige Fehler, denen iOS-Entwickler zum Opfer fallen, und wie Sie sie vermeiden können.

85 % der iOS-Benutzer verwenden die neueste Betriebssystemversion. Das heißt, sie erwarten, dass Ihre App oder Ihr Update fehlerfrei ist.
Twittern

Häufiger Fehler Nr. 1: Asynchrone Prozesse nicht verstehen

Ein sehr häufiger Fehler unter neuen Programmierern ist der unsachgemäße Umgang mit asynchronem Code. Betrachten wir ein typisches Szenario: Ein Benutzer öffnet einen Bildschirm mit der Tabellenansicht. Einige Daten werden vom Server abgerufen und in einer Tabellenansicht angezeigt. Wir können es formeller schreiben:

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

Auf den ersten Blick sieht alles richtig aus: Wir holen Daten vom Server und aktualisieren dann die UI. Das Problem besteht jedoch darin, dass das Abrufen von Daten ein asynchroner Prozess ist und neue Daten nicht sofort zurückgegeben werden, was bedeutet, dass reloadData wird, bevor die neuen Daten empfangen werden. Um diesen Fehler zu beheben, sollten wir Zeile 2 direkt nach Zeile 1 innerhalb des Blocks verschieben.

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

Es kann jedoch Situationen geben, in denen sich dieser Code immer noch nicht wie erwartet verhält, was uns zu …

Häufiger Fehler Nr. 2: Ausführen von UI-bezogenem Code in einem anderen Thread als der Hauptwarteschlange

Stellen wir uns vor, wir haben ein korrigiertes Codebeispiel aus dem vorherigen häufigen Fehler verwendet, aber unsere Tabellenansicht wird immer noch nicht mit den neuen Daten aktualisiert, selbst nachdem der asynchrone Prozess erfolgreich abgeschlossen wurde. Was könnte an einem so einfachen Code falsch sein? Um es zu verstehen, können wir einen Haltepunkt innerhalb des Blocks setzen und herausfinden, in welcher Warteschlange dieser Block aufgerufen wird. Es besteht eine hohe Wahrscheinlichkeit, dass das beschriebene Verhalten auftritt, da sich unser Anruf nicht in der Hauptwarteschlange befindet, in der der gesamte UI-bezogene Code ausgeführt werden sollte.

Die gängigsten Bibliotheken – wie Alamofire, AFNetworking und Haneke – sind so konzipiert, dass sie den „ completionBlock “ in der Hauptwarteschlange aufrufen, nachdem sie eine asynchrone Aufgabe ausgeführt haben. Darauf können Sie sich jedoch nicht immer verlassen und es wird leicht vergessen, Ihren Code an die richtige Warteschlange zu senden.

Um sicherzustellen, dass sich Ihr gesamter UI-bezogener Code in der Hauptwarteschlange befindet, vergessen Sie nicht, ihn an diese Warteschlange zu senden:

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

Häufiger Fehler Nr. 3: Missverständnis von Parallelität und Multithreading

Parallelität kann mit einem wirklich scharfen Messer verglichen werden: Sie können sich leicht schneiden, wenn Sie nicht vorsichtig oder erfahren genug sind, aber es ist äußerst nützlich und effizient, wenn Sie wissen, wie man es richtig und sicher verwendet.

Sie können versuchen, Parallelität zu vermeiden, aber egal, welche Art von Apps Sie erstellen, es besteht eine sehr hohe Wahrscheinlichkeit, dass Sie nicht darauf verzichten können. Parallelität kann erhebliche Vorteile für Ihre Anwendung haben. Vor allem:

  • Fast jede Anwendung ruft Webdienste auf (z. B. um einige umfangreiche Berechnungen durchzuführen oder Daten aus einer Datenbank zu lesen). Wenn diese Aufgaben in der Hauptwarteschlange ausgeführt werden, friert die Anwendung für einige Zeit ein und reagiert nicht mehr. Wenn dies zu lange dauert, fährt iOS die App außerdem vollständig herunter. Durch das Verschieben dieser Aufgaben in eine andere Warteschlange kann der Benutzer die Anwendung weiter verwenden, während der Vorgang ausgeführt wird, ohne dass die App einzufrieren scheint.
  • Moderne iOS-Geräte haben mehr als einen Kern, warum also sollte der Benutzer warten, bis Aufgaben nacheinander beendet werden, wenn sie parallel ausgeführt werden können?

Aber die Vorteile der Parallelität kommen nicht ohne Komplexität und das Potenzial für das Einführen knorriger Fehler, wie z. B. Race Conditions, die wirklich schwer zu reproduzieren sind.

Betrachten wir einige Beispiele aus der realen Welt (beachten Sie, dass einige Codes der Einfachheit halber weggelassen wurden).

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

Der Multithread-Code:

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

Auf den ersten Blick ist alles synchronisiert und sieht so aus, als ob es wie erwartet funktionieren sollte, da ThreadSaveVar den counter umschließt und ihn Thread-sicher macht. Leider ist dies nicht wahr, da zwei Threads gleichzeitig die Inkrementzeile erreichen können und counter.value == someValue niemals wahr wird. Als Problemumgehung können wir ThreadSafeCounter , der seinen Wert nach dem Erhöhen zurückgibt:

 class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }

Fall 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 diesem Fall wurde dispatch_barrier_sync verwendet, um den Zugriff auf das Array zu synchronisieren. Dies ist ein bevorzugtes Muster, um die Zugriffssynchronisierung sicherzustellen. Leider berücksichtigt dieser Code nicht, dass struct jedes Mal, wenn wir ein Element anhängen, eine Kopie erstellt, wodurch jedes Mal eine neue Synchronisierungswarteschlange entsteht.

Auch wenn es auf den ersten Blick richtig aussieht, funktioniert es hier möglicherweise nicht wie erwartet. Es erfordert auch viel Arbeit, es zu testen und zu debuggen, aber am Ende können Sie die Geschwindigkeit und Reaktionsfähigkeit Ihrer App verbessern.

Häufiger Fehler Nr. 4: Die Fallstricke veränderlicher Objekte nicht kennen

Swift ist sehr hilfreich, um Fehler mit Werttypen zu vermeiden, aber es gibt immer noch viele Entwickler, die Objective-C verwenden. Veränderliche Objekte sind sehr gefährlich und können zu versteckten Problemen führen. Es ist eine bekannte Regel, dass unveränderliche Objekte von Funktionen zurückgegeben werden sollten, aber die meisten Entwickler wissen nicht, warum. Betrachten wir den folgenden Code:

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

Der obige Code ist korrekt, da NSMutableArray eine Unterklasse von NSArray ist. Was kann also mit diesem Code schief gehen?

Das erste und offensichtlichste ist, dass ein anderer Entwickler kommen und Folgendes tun könnte:

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

Dieser Code wird Ihre Klasse durcheinander bringen. Aber in diesem Fall ist es ein Code-Geruch, und es bleibt dem Entwickler überlassen, die Teile aufzusammeln.

Hier ist jedoch der Fall, der viel schlimmer ist und ein unerwartetes Verhalten zeigt:

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

Die Erwartung hier ist, dass [newChildBoxes count] > [childBoxes count] , aber was ist, wenn dies nicht der Fall ist? Dann ist die Klasse nicht gut entworfen, weil sie einen Wert verändert, der bereits zurückgegeben wurde. Wenn Sie glauben, dass Ungleichheit nicht wahr sein sollte, versuchen Sie, mit UIView und [view subviews] zu experimentieren.

Glücklicherweise können wir unseren Code leicht reparieren, indem wir den Getter aus dem ersten Beispiel umschreiben:

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

Häufiger Fehler Nr. 5: Nicht verstehen, wie iOS NSDictionary funktioniert

Wenn Sie jemals mit einer benutzerdefinierten Klasse und NSDictionary haben, stellen Sie möglicherweise fest, dass Sie Ihre Klasse nicht verwenden können, wenn sie nicht NSCopying als Wörterbuchschlüssel entspricht. Die meisten Entwickler haben sich nie gefragt, warum Apple diese Einschränkung hinzugefügt hat. Warum kopiert Apple den Schlüssel und verwendet diese Kopie anstelle des ursprünglichen Objekts?

Der Schlüssel zum Verständnis liegt darin herauszufinden, wie NSDictionary intern funktioniert. Technisch gesehen ist es nur eine Hash-Tabelle. Lassen Sie uns kurz zusammenfassen, wie es auf hoher Ebene funktioniert, während Sie ein Objekt für einen Schlüssel hinzufügen (Tabellengrößenänderung und Leistungsoptimierung werden hier der Einfachheit halber weggelassen):

Schritt 1: Es berechnet hash(Key) . Schritt 2: Basierend auf dem Hash wird nach einem Ort gesucht, an dem das Objekt abgelegt werden kann. Normalerweise geschieht dies, indem der Modulus des Hash-Werts mit der Wörterbuchlänge genommen wird. Der resultierende Index wird dann verwendet, um das Schlüssel/Wert-Paar zu speichern. Schritt 3: Wenn an diesem Ort kein Objekt vorhanden ist, wird eine verknüpfte Liste erstellt und unser Datensatz (Objekt und Schlüssel) gespeichert. Andernfalls wird der Datensatz an das Ende der Liste angehängt.

Lassen Sie uns nun beschreiben, wie ein Datensatz aus dem Wörterbuch abgerufen wird:

Schritt 1: Es berechnet hash(Key) . Schritt 2: Es sucht einen Schlüssel nach Hash. Wenn keine Daten vorhanden sind, wird nil zurückgegeben. Schritt 3: Wenn eine verknüpfte Liste vorhanden ist, wird das Objekt durchlaufen, bis [storedkey isEqual:Key] .

Mit diesem Verständnis dessen, was unter der Haube passiert, können zwei Schlussfolgerungen gezogen werden:

  1. Wenn sich der Hash des Schlüssels ändert, sollte der Datensatz in eine andere verknüpfte Liste verschoben werden.
  2. Schlüssel sollten eindeutig sein.

Lassen Sie uns dies an einer einfachen Klasse untersuchen:

 @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

Stellen Sie sich nun vor, NSDictionary keine Schlüssel:

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

Oh! Da haben wir einen Tippfehler! Lass es uns reparieren!

 p.name = @"Jon Snow";

Was soll mit unserem Wörterbuch passieren? Da der Name mutiert wurde, haben wir jetzt einen anderen Hash. Jetzt liegt unser Objekt an der falschen Stelle (es hat immer noch den alten Hash-Wert, da das Wörterbuch nichts von der Datenänderung weiß), und es ist nicht wirklich klar, welchen Hash wir verwenden sollen, um Daten im Wörterbuch nachzuschlagen. Es könnte einen noch schlimmeren Fall geben. Stellen Sie sich vor, wir hätten „Jon Snow“ bereits in unserem Wörterbuch mit einer Bewertung von 5. Das Wörterbuch würde am Ende zwei verschiedene Werte für denselben Schlüssel enthalten.

Wie Sie sehen können, gibt es viele Probleme, die durch veränderliche Schlüssel in NSDictionary entstehen können. Die beste Vorgehensweise zur Vermeidung solcher Probleme besteht darin, das Objekt vor dem Speichern zu kopieren und Eigenschaften als copy zu markieren. Diese Übung wird Ihnen auch helfen, Ihre Klasse konsistent zu halten.

Häufiger Fehler Nr. 6: Verwenden von Storyboards anstelle von XIBs

Die meisten neuen iOS-Entwickler folgen dem Vorschlag von Apple und verwenden standardmäßig Storyboards für die Benutzeroberfläche. Es gibt jedoch viele Nachteile und nur wenige (umstrittene) Vorteile bei der Verwendung von Storyboards.

Zu den Nachteilen des Storyboards gehören:

  1. Es ist wirklich schwierig, ein Storyboard für mehrere Teammitglieder zu ändern. Technisch gesehen können Sie viele Storyboards verwenden, aber der einzige Vorteil besteht in diesem Fall darin, dass es möglich ist, Übergänge zwischen Controllern auf dem Storyboard zu haben.
  2. Controller- und Segues-Namen aus Storyboards sind Zeichenfolgen, also müssen Sie entweder alle diese Zeichenfolgen im gesamten Code erneut eingeben (und eines Tages werden Sie es brechen) oder eine riesige Liste von Storyboard-Konstanten pflegen. Sie könnten SBConstants verwenden, aber das Umbenennen auf dem Storyboard ist immer noch keine leichte Aufgabe.
  3. Storyboards zwingen Sie zu einem nicht-modularen Design. Bei der Arbeit mit einem Storyboard gibt es kaum einen Anreiz, Ihre Ansichten wiederverwendbar zu machen. Dies kann für das Minimum Viable Product (MVP) oder schnelles UI-Prototyping akzeptabel sein, aber in echten Anwendungen müssen Sie dieselbe Ansicht möglicherweise mehrmals in Ihrer App verwenden.

Storyboard (umstrittene) Vorteile:

  1. Die gesamte App-Navigation ist auf einen Blick ersichtlich. Reale Anwendungen können jedoch mehr als zehn Controller haben, die in verschiedene Richtungen verbunden sind. Storyboards mit solchen Verbindungen sehen aus wie ein Wollknäuel und vermitteln kein umfassendes Verständnis von Datenflüssen.
  2. Statische Tabellen. Das ist der einzige wirkliche Vorteil, den ich mir vorstellen kann. Das Problem ist, dass 90 Prozent der statischen Tabellen während der App-Evolution in dynamische Tabellen umgewandelt werden und eine dynamische Tabelle einfacher von XIBs gehandhabt werden kann.

Häufiger Fehler Nr. 7: Verwirrender Objekt- und Zeigervergleich

Beim Vergleich zweier Objekte können wir zwei Gleichheiten berücksichtigen: Zeiger- und Objektgleichheit.

Zeigergleichheit ist eine Situation, in der beide Zeiger auf dasselbe Objekt zeigen. In Objective-C verwenden wir den Operator == , um zwei Zeiger zu vergleichen. Objektgleichheit ist eine Situation, in der zwei Objekte zwei logisch identische Objekte darstellen, wie z. B. derselbe Benutzer aus einer Datenbank. In Objective-C verwenden wir isEqual oder noch besser typspezifische isEqualToString , isEqualToDate usw. Operatoren zum Vergleichen zweier Objekte.

Betrachten Sie den folgenden Code:

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

Was wird auf der Konsole ausgegeben, wenn wir diesen Code ausführen? Wir erhalten a is equal to b , da beide Objekte a und b auf dasselbe Objekt im Speicher zeigen.

Aber jetzt ändern wir Zeile 2 zu:

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

Jetzt erhalten wir, dass a is NOT equal to b da diese Zeiger jetzt auf verschiedene Objekte zeigen, obwohl diese Objekte dieselben Werte haben.

Dieses Problem kann vermieden werden, indem man sich auf isEqual oder typspezifische Funktionen verlässt. In unserem Codebeispiel sollten wir Zeile 3 durch folgenden Code ersetzen, damit es immer richtig funktioniert:

 if ([a isEqual:b]) {

Häufiger Fehler Nr. 8: Verwendung fest codierter Werte

Es gibt zwei Hauptprobleme mit hartcodierten Werten:

  1. Oft ist nicht klar, wofür sie stehen.
  2. Sie müssen erneut eingegeben (oder kopiert und eingefügt) werden, wenn sie an mehreren Stellen im Code verwendet werden müssen.

Betrachten Sie das folgende Beispiel:

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

Was bedeutet 172800? Warum wird es verwendet? Es ist wahrscheinlich nicht offensichtlich, dass dies der Anzahl von Sekunden in 2 Tagen entspricht (ein Tag hat 24 x 60 x 60 oder 86.400 Sekunden).

Anstatt hartcodierte Werte zu verwenden, könnten Sie einen Wert mit der #define define-Anweisung definieren. Zum Beispiel:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define ist ein Präprozessormakro, das die benannte Definition durch ihren Wert im Code ersetzt. Wenn Sie also #define in einer Header-Datei haben und es irgendwo importieren, werden alle Vorkommen des definierten Werts in dieser Datei ebenfalls ersetzt.

Das funktioniert gut, bis auf ein Problem. Um das verbleibende Problem zu veranschaulichen, betrachten Sie den folgenden Code:

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

Was würden Sie erwarten, dass der Wert von y sein wird, nachdem dieser Code ausgeführt wurde? Wenn Sie 1,5 gesagt haben, liegen Sie falsch. y ist gleich 1 ( nicht 1,5), nachdem dieser Code ausgeführt wurde. Warum? Die Antwort ist, dass #define keine Informationen über den Typ hat. In unserem Fall haben wir also eine Division von zwei Int -Werten (3 und 2), was zu einem Int (dh 1) führt, das dann in ein Float wird.

Dies kann vermieden werden, indem stattdessen Konstanten verwendet werden, die per Definition typisiert sind:

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

Häufiger Fehler Nr. 9: Verwendung des Standardschlüsselworts in einer Switch-Anweisung

Die Verwendung des Schlüsselworts default in einer switch-Anweisung kann zu Fehlern und unerwartetem Verhalten führen. Betrachten Sie den folgenden Code in Objective-C:

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

Derselbe Code in Swift geschrieben:

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

Dieser Code funktioniert wie vorgesehen, sodass nur Admin-Benutzer andere Datensätze ändern können. Was könnte jedoch passieren, wenn wir einen weiteren Benutzertyp hinzufügen, „Manager“, der ebenfalls in der Lage sein sollte, Datensätze zu bearbeiten? Wenn wir vergessen, diese switch Anweisung zu aktualisieren, wird der Code zwar kompiliert, funktioniert aber nicht wie erwartet. Wenn der Entwickler jedoch von Anfang an Enum-Werte anstelle des Standardschlüsselworts verwendet hat, wird das Versehen zur Kompilierzeit identifiziert und könnte behoben werden, bevor es in den Test oder in die Produktion geht. Hier ist eine gute Möglichkeit, dies in Objective-C zu handhaben:

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

Derselbe Code in Swift geschrieben:

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

Häufiger Fehler Nr. 10: Verwendung NSLog für die Protokollierung

Viele iOS-Entwickler verwenden NSLog in ihren Apps für die Protokollierung, aber meistens ist dies ein schrecklicher Fehler. Wenn wir in der Apple-Dokumentation nach der Beschreibung der NSLog Funktion suchen, werden wir sehen, dass es sehr einfach ist:

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

Was könnte daran schiefgehen? Eigentlich nichts. Wenn Sie Ihr Gerät jedoch mit dem Xcode-Organizer verbinden, sehen Sie dort alle Ihre Debug-Meldungen. Allein aus diesem Grund sollten Sie NSLog niemals zum Protokollieren verwenden: Es ist einfach, einige unerwünschte interne Daten anzuzeigen, und es sieht unprofessionell aus.

Ein besserer Ansatz besteht darin, NSLogs durch das konfigurierbare CocoaLumberjack oder ein anderes Protokollierungsframework zu ersetzen.

Einpacken

iOS ist eine sehr leistungsfähige und sich schnell entwickelnde Plattform. Apple unternimmt ständig große Anstrengungen, um neue Hardware und Funktionen für iOS selbst einzuführen, während es gleichzeitig die Swift-Sprache kontinuierlich erweitert.

Die Verbesserung Ihrer Objective-C- und Swift-Fähigkeiten macht Sie zu einem großartigen iOS-Entwickler und bietet Möglichkeiten, an herausfordernden Projekten mit modernsten Technologien zu arbeiten.

Siehe auch: Ein iOS-Entwicklerhandbuch: Von Objective-C zu Learning Swift