Fehler bei der Arbeit mit der Systemtastatur

Bei der Interaktion mit der Anwendung aktivieren wir irgendwann die Systemtastatur, um eine Nachricht einzugeben oder die erforderlichen Felder auszufüllen. Sind Sie auf Situationen gestoßen, in denen die Tastatur angezeigt wird, aber kein Feld für die Eingabe einer Nachricht vorhanden ist oder umgekehrt - die Tastatur ist dort, wo die Eingabe von Text nicht sichtbar ist? Fehler können mit Problemen innerhalb einer bestimmten Anwendung sowie mit allgemeinen Mängeln der Systemtastatur zusammenhängen.

Konstantin Mordan , ein iOS-Entwickler von Mail.ru, sah alles in seiner Arbeit: Nachdem er die Tastatursteuerungsmethoden in iOS analysiert hatte, beschloss er, die wichtigsten Fehler und Ansätze zu teilen, mit denen er sie erkannte und behebte.



Achtung: Unter dem Schnitt haben wir viele Gifs platziert, um Fehler deutlich zu machen. Weitere Beispiele finden Sie im Videobericht von Konstantin auf AppsConf.

Implementieren eines Systemtastaturaufrufs


Beginnen wir mit dem Verständnis, wie ein Tastaturaufruf im Allgemeinen implementiert wird.

Stellen Sie sich vor, Sie entwickeln eine Anwendung, deren Aufgabe es ist, Aika (einen South Park-Charakter) über die Tastatur zu einem ganzen Kanadier zusammenzusetzen. Wenn Sie Aiku auf den Bauch drücken, geht die Tastatur und hebt dabei die Beine unseres Helden zum Kopf.

Um die Aufgabe zu implementieren, können Sie InputAccessoryView verwenden oder Systembenachrichtigungen verarbeiten.

InputAccessoryView


Schauen wir uns die erste Option an.

Erstellen Sie im ViewController eine Ansicht, die zusammen mit der Tastatur angezeigt wird, und geben Sie ihr einen Rahmen. Es ist wichtig, dass diese Ansicht nicht als Unteransicht hinzugefügt wird. Als Nächstes überschreiben wir die Eigenschaften von canBecomeFirstResponder und geben true zurück. Nachdem wir die UIResponder-Eigenschaft - inputAccessoryView neu definiert und die Ansicht dort abgelegt haben. Um die Tastatur zu schließen, fügen Sie tapGesture hinzu und setzen Sie in ihrem Handler den ersten Antwortenden der von uns erstellten Ansicht zurück.

class ViewController: UIViewController { var tummyView: UIView { let frame = CGRect(x: x, y: y, width: width, height: height) let v = TummyView(frame: frame) return v } override var canBecomeFirstResponder: Bool { return true } override var input AccessoryView: UIView? { return tummyView } func tapHandler ( ) { tummyView.resignFirstResponder ( ) } } 

Die Aufgabe ist abgeschlossen, und das System selbst verarbeitet Änderungen des Tastaturstatus, zeigt sie an und löst die Ansicht aus, die davon abhängt.



Systembenachrichtigungsverarbeitung


Bei der Verarbeitung von Benachrichtigungen müssen wir Benachrichtigungen aus den folgenden Gruppen selbst verarbeiten:

  • wann die Tastatur angezeigt wird / wurde: keyboardWillShowNotification, keyboardDidShowNotification;
  • wann die Tastatur ausgeblendet wird / wurde: keyboardWillHideNotification, keyboardDidHideNotification;
  • Wann wird / wurde der Tastaturrahmen geändert: keyboardWilChangeFrameNotification, keyboardDidChangeFrameNotification.

Um unseren Fall zu implementieren, nehmen wir keyboardWilChangeFrameNotification , da diese Benachrichtigung sowohl gesendet wird, wenn die Tastatur angezeigt wird, als auch wenn sie ausgeblendet ist.

Wir erstellen einen tastaturtracker, abonnieren darin eine Benachrichtigung über keyboardWillChangeFrame und erhalten im Handler den Tastaturrahmen, konvertieren ihn vom Bildschirmkoordinatensystem in das Fensterkoordinatensystem, berechnen die Tastaturhöhe und ändern den Y-Wert der Ansicht, die von der Tastatur angehoben werden soll, auf diese Höhe.

 class KeyboardTracker { func enable ( ) { notificationCenter.add0observer(self, seletor: #selector( keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } func keyboardWillChangeFrame ( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo [ UIResponder.keyboardFrameEndUserInfoKey ] as! NSValue ) .cgRectValue let keyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil ) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY delegate.keyboardWillChange ( keyboardHeight ) } } 

Damit ist unsere Aufgabe erledigt, die Tastatur steigt und sammelt Ike im Kanadier.

Wie wir sehen können, ist die Implementierung der Arbeit mit der Tastatur in beiden Fällen recht einfach, sodass jeder die geeignete Methode selbst auswählen kann. In unserem Projekt haben wir uns für Benachrichtigungen entschieden, sodass weitere Beispiele und Erkenntnisse mit der Verarbeitung von Benachrichtigungen verbunden werden.

Auf der Suche nach Fehlern


Wenn die Art und Weise, die Tastatur aufzurufen, so einfach ist, woher kommen dann die Fehler? Wenn die Anwendung nur das Skript zum Öffnen und Schließen der Tastatur reproduziert, gibt es natürlich keine Probleme. Wenn Sie jedoch den üblichen Ablauf ändern, denken Sie daran, dass nicht nur unsere Anwendung die Tastatur verwenden kann, sondern auch andere, und der Benutzer auch zwischen ihnen wechseln kann. Überraschungen können nicht vermieden werden.

Schauen wir uns ein Beispiel an. Verwenden Sie dazu unsere Anwendung mit Ike: Öffnen Sie die Tastatur, wechseln Sie zu Notizen, drucken Sie etwas und kehren Sie zur Anwendung zurück.



Welche Probleme sind bereits sichtbar? Erstens gibt es im App Switcher keine Tastatur, obwohl beim Minimieren der Anwendung diese vorhanden war und stattdessen andere Inhalte sichtbar sind. Zweitens, wenn Sie zur Anwendung zurückkehren, ist die Tastatur immer noch nicht vorhanden, und Ikes Beine fallen über den Bildschirm.

Schauen wir uns die Gründe für dieses Verhalten an. Wie wir uns alle aus dem Lebenszyklusdiagramm der Anwendung erinnern, dauert der Übergang einer Anwendung von einem aktiven Zustand in einen inaktiven Zustand zuerst im Vordergrund und dann im Hintergrund einige Zeit.

Was ist mit dem Tastaturlebenszyklus? Unter iOS kann die Tastatur für jede Zeiteinheit nur einer der ausgeführten Anwendungen gehören, aber Benachrichtigungen über Änderungen des Tastaturstatus werden von allen darauf signierten Anwendungen empfangen.

Beim Wechsel von einer Anwendung zu einer anderen setzt das System seinen ersten Antwortenden zurück, der als Auslöser zum Ausblenden der Tastatur fungiert. Das System sendet zuerst eine Benachrichtigung über keyboardWillHide, damit die Tastatur verschwindet, und dann keyboardDidHideNotification. Die Benachrichtigung fliegt zur zweiten Anwendung. In der neuen Anwendung öffnen wir die Tastatur: Das System sendet keyboardWillShowNotification, damit die Tastatur angezeigt wird, und sendet dann die keyboardDidShowNotification - eine Demo mit den Phasen des Zyklus.



Wenn Sie sich einen Auszug aus dem Bericht ansehen (ab 8:39 Uhr), sehen Sie den Moment, in dem das System nach dem Ausblenden der Tastatur keyboardDidHideNotification sendet, um die erste Anwendung in einen inaktiven Zustand zu versetzen. Wenn Sie zur Sport-App wechseln und die Tastatur starten, sendet das System keyboardWillShowNotification. Da der Umschalt- und Startvorgang jedoch schnell ist und die Übergangszeit zwischen den Phasen des Lebenszyklus länger sein kann, wird in der erhaltenen Benachrichtigung nicht nur der Sportantrag bearbeitet, sondern auch der Bierantrag, der noch nicht in den Hintergrund getreten ist.

Nachdem wir die Gründe herausgefunden haben, wollen wir nun eine Lösung für das Problem mit Ike finden.

Schlechte Entscheidung


Das erste, was mir in den Sinn kommt, ist die Idee, Benachrichtigungen abzubestellen / zu abonnieren, wenn eine Anwendung durch Aktivieren / Deaktivieren von KeyboardTracker minimiert / maximiert wird.

Zum Abbestellen verwenden wir die applicationWillResignActive-Methode oder einen ähnlichen Benachrichtigungshandler aus dem System. Zum Abonnieren verwenden wir applicationDidBecomeActive. Um jedoch nichts zu verpassen, benachrichtigen wir auch die applicationWillEnterForeground-Methode, die aufgerufen wird, wenn die Anwendung in den Vordergrund tritt, aber noch nicht aktiv wird.

Wenn Sie die Tastatur in der Anwendung starten, ist höchstwahrscheinlich alles erfolgreich. Bei komplexeren Tests, z. B. Öffnen der Tastatur und Versuch, Sprachwahl aufzuzeichnen, funktioniert die Lösung jedoch nicht.



Was ist passiert? Nach dem Klicken auf die Schaltfläche zum Wählen von Sprachnachrichten wurde die Anwendung firstResponder zurückgesetzt, die Tastatur wurde geschlossen, die applicationWillResignActive-Methode wurde aufgerufen und wir haben uns abgemeldet. Nach dem Schließen der Warnung stellte das System den Anwendungsstatus wieder her, jedoch bevor die applicationWillEnterForeground-Methode und insbesondere die applicationDidBecomeActive aufgerufen wurden.

Gute Entscheidung


Eine andere Lösung ist die Verwendung einer Schutzbrühe (Bool).

 var wasTummyViewFirstResponderBeforeApp0idEnterBackground func willResignActive( notification: NSNotification) { wasTextFieldFirstResponderBeforeAppDidEnterBackground = tummyView.isFirstResponder } func willEnterForeground ( notification: NSNotification) { if wasTextFieldFirstResponderBeforeAppDidEnterBackground { UIView.performWithourAnimation { tummyView.becomeFirstResponder ( ) } } } 

Wir erinnern uns, ob die Tastatur vor dem Thema geöffnet wurde, wie die Anwendung nicht mehr aktiv war und in der applicationWillEnterForeground-Methode den vorherigen Status wiederherstellen. Das einzige, was noch behoben werden muss, ist das Loch im App-Umschalter.



App-Umschalter


Der App-Umschalter zeigt App-Snapshots an, die das System erstellt, nachdem die App in den Hintergrund getreten ist. Der Screenshot zeigt, dass der Schnappschuss unserer Anwendung zu einem Zeitpunkt erstellt wurde, als die Tastatur bereits von einer anderen Anwendung verwendet wird. Dies ist nicht kritisch, aber es sind nur ein paar Klicks erforderlich, um das Problem zu beheben.

Gute Lösung


Die Lösung kann von Bankanwendungen ausgeliehen werden, die gelernt haben, vertrauliche Daten zu verbergen, und auch von Apple gelesen werden.

Sie können die Daten in der applicationDidEnterBackground-Methode ausblenden, den Begrüßungsbildschirm verwischen und anzeigen und in der applicationWillEnterForeground-Methode zur üblichen Ansichtshierarchie zurückkehren.

Diese Option passt nicht zu uns, da unsere Anwendung zum Zeitpunkt des Aufrufs der applicationDidEnterBackground-Methode keine Tastatur mehr hat.

Gute Entscheidung


Wir werden die bekannten Methoden willResignActive, willEnterForeground und didBecomeActive verwenden.

Während unsere Anwendung noch über eine Tastatur verfügt, müssen Sie in der Methode willResignActive einen eigenen Snapshot der Anwendung erstellen und in die Hierarchie einfügen.

 func willResignActive( notificaton: NSNotification) { let keyWindow = UIApplication.shared.keyWindow imageView = UIImageView( frame: keyWindow.bounds) imageView.image = snapshot ( ) let lastSubview = keyWindow.subviews.last lastSubview( imageView) } 

In den Methoden willEnterForeground und didBecomeActive stellen wir die Ansichtshierarchie wieder her und löschen unseren Snapshot.

 func willEnterForeground( notification: NSNotification) { imageView.removeFromSuperview( ) } func didBecomeActive( notification: NSNotification) { imageView.removeFromSuperview( ) } 

Infolgedessen haben wir beide Fälle behoben: Im App-Umschalter springen beim Umschalten ein schönes Bild und die Tastatur nicht mehr. Es scheint, dass dies keine so wichtigen Dinge sind, aber für die Produktentwicklung sind diese Punkte äußerst wichtig.

Schlechte Nachrichten


Unsere erfolgreiche Lösung des Ike-Problems betraf den Fall, dass die Tastatur geöffnet wurde, bevor die Anwendung minimiert wurde. Wenn das Umschalten erfolgt, ohne die Tastatur zu erweitern, werden wir wieder sehen, dass die Beine unseres Ike nach unten gefallen sind.



Dies ist nicht nur ein Problem für unsere Anwendung, dieses Verhalten wird auch bei Facebook beobachtet, das mit Benachrichtigungen arbeitet, und sogar bei iMessage, das inputAccessoryView zur Steuerung der Tastatur verwendet. Dies liegt an der Tatsache, dass Anwendungen vor dem Wechsel in den Hintergrund die Tastaturbenachrichtigungen anderer Personen verarbeiten können.

interaktiv Tastaturentlassung


Fügen Sie unserer Anwendung mit Ike einige Funktionen hinzu und bringen Sie dem Programm bei, die Tastatur interaktiv auszublenden.



Schlechte Entscheidung


Eine Möglichkeit, diese Funktionalität zu nutzen, besteht darin, den Rahmen der Tastaturansicht zu ändern. Wir erstellen panGestureRecognizer, berechnen in seinem Handler den neuen Wert der Y-Koordinate für die Tastatur, abhängig von der Position unseres Fingers, suchen die Tastaturansicht und aktualisieren sie mit dem Y-Koordinatenwert.

 func panGestureHandler( ) { let yPosition: CGFloat = value keyboardView( )?.frame.origin.y = yPosition } 

Die Tastatur wird in einem separaten Fenster angezeigt. Sie müssen also das gesamte Fensterarray in der Anwendung durchgehen, für jedes Element des Arrays prüfen, ob es sich um ein Tastaturfenster handelt, und in diesem Fall eine Ansicht mit der Tastatur abrufen.

 func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

Leider funktioniert diese Lösung auf dem iPhone X und höher nicht normal, da Sie beim Bewegen des Fingers die untere Anzeige leicht berühren können, was für die Minimierung der Anwendung verantwortlich ist. Danach funktioniert das interaktive Verstecken nicht mehr.



Das Problem liegt in der Anordnung der Fenster.



Nach der Geste erstellt das System ein neues Tastaturfenster über dem vorhandenen. Es ist undenkbar, aber wahr. Als Ergebnis stellt sich heraus, dass das Array zwei Tastaturfenster mit denselben Koordinaten enthält, das erste jedoch ausgeblendet ist.



Es stellt sich heraus, dass wir beim Durchlaufen der Fensterreihe das erste finden, das die Bedingungen erfüllt, und damit beginnen, damit zu arbeiten, obwohl es verborgen ist.

Wie ist das behoben? Eine Reihe von Fenstern drehen.

 func panGeastureHandler( ) { let yPosition: CGFloat = 0.0 keyboardView( )?.frame.origin.y = yPosition } func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows.reversed( ) let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

Tastaturfunktionen auf dem iPad


Die Tastatur des iPad hat neben dem üblichen Zustand einen nicht angedockten Zustand. Der Benutzer kann es auf dem Bildschirm bewegen, in zwei Teile teilen und die Anwendung sogar im Slide-Over-Modus (übereinander) starten. Natürlich ist es wichtig, dass in all diesen Modi die Tastatur fehlerfrei funktioniert.

Schauen wir uns unseren Hayke an.



Leider ist dies jetzt nicht der Fall. Nachdem der Benutzer die Tastatur auf dem Bildschirm bewegt hat, fliegen Ikes Beine über seinem Kopf ab und erscheinen erst nach dem nächsten Öffnen der Tastatur. Versuchen wir, das Problem mit einer geteilten Tastatur zu beheben.

Gründe


Beginnen wir mit der Analyse von Benachrichtigungen. Nach dem Klicken auf die Schaltfläche "Teilen" erhalten wir zwei Gruppen von Benachrichtigungen: "KeyboardWillChangeFrameNotification", "KeyboardWillHideNotification", "KeyboardDidChangeFrameNotification", "KeyboardDidHideNotification". Der Unterschied zwischen Gruppen liegt nur in den Koordinaten der Tastatur.

Wenn wir auf die Schaltfläche "Teilen" klicken, wird die Tastatur kleiner und die erste Gruppe von Benachrichtigungen wird angezeigt. Als sich die Tastatur aufteilte und nach oben ging, erhielten wir ein zweites Paket mit Benachrichtigungen.

Wichtig ist, dass wir Benachrichtigungen erhalten, dass die Tastatur verschwunden ist, aber nicht, dass sie angezeigt wird. Dies ist übrigens ein weiteres Plus für die Verwendung von keyboardWillChangeFrameNotification.

Warum fliegen dann Ikes Beine weg, sobald wir die Tastatur über den Bildschirm bewegen?

In diesem Moment sendet uns das System eine keyboardWillChangeFrameNotification, aber die dort liegenden Koordinaten sind (0.0, 0.0, 0.0, 0.0), da das System nicht weiß, an welchem ​​Punkt sich die Tastatur befindet, nachdem die Bewegung abgeschlossen ist.

Wenn wir im aktuellen Code, der die Änderung des Tastaturrahmens verarbeitet, Nullen einsetzen, stellt sich heraus, dass die Höhe der Tastatur der Höhe des Fensters entspricht. Das ist der Grund, warum Ikes Beine vom Bildschirm fliegen.

Gute Entscheidung


Um unser Problem zu lösen, lernen wir zunächst zu verstehen, wann sich die Tastatur im nicht angedockten Modus befindet und der Benutzer sie auf dem Bildschirm bewegen kann.

Vergleichen Sie dazu einfach die Höhe des Fensters und der maxY-Tastatur. Wenn sie gleich sind, befindet sich die Tastatur im Normalzustand. Wenn maxY kleiner als die Höhe des Fensters ist, bewegt der Benutzer die Tastatur. Infolgedessen wird der folgende Code in keyboardTracker angezeigt:

 class KeyboardTracker { func enable( ) { notificationCenter.addObserver( self, selector:#selector( keyboardWillChangeFrame), name:UIResponder.keyboardWillChangeFrameNotification, object:nil) } func keyboardWillChangeFrame( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue let leyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY let isKeyboardUnlocked = isIPad ( ) && keyboardFrame/maxY < windowHeight if isKeyboardUnlocked { keyboardHeight = 0.0 } delegate.keyboardWillChange ( keyboardHeight) } } 

Wir haben die Höhe benutzerdefiniert auf Null gesetzt, und jetzt, mit der Bewegung der Tastatur, gehen Ikes Beine nach unten und werden dort fixiert.



Das einzige verbleibende Missverständnis ist die Tatsache, dass Ikes Beine beim Teilen der Tastatur nicht sofort herunterfallen. Wie kann ich das beheben?

Wir werden keyboardTracker beibringen, nicht nur mit keyboardWillChangeFrameNotification, sondern auch mit keyboardDidChangeFrame zu arbeiten. Sie müssen keinen neuen Code schreiben, sondern müssen nur überprüfen, ob es sich um ein iPad handelt, um keine unnötigen Berechnungen durchzuführen.

 class KeyboardTracker { func keyboardDidChangeFrame( notification: NSNotification) { if isIPad ( ) == false { return } 




Wie erkenne ich Fehler?


Reichlich Protokollierung


In unserem Projekt werden Protokolle in folgendem Format geschrieben: in eckigen Klammern der Name des Moduls und des Submoduls, zu dem das Protokoll gehört, und dann der Text des Protokolls selbst. Zum Beispiel so:
[keyboard][tracker] keyboardWillChangeFrame: calculated height - 437.9

Im Code sieht es wie folgt aus: Ein Logger wird mit einem Tag der obersten Ebene erstellt und an den Tracker übertragen. Im Tracker wird ein Logger mit einem Tag der zweiten Ebene, der zum Protokollieren innerhalb der Klasse verwendet wird, aus dem Logger ausgegliedert.

 class KeyboardTracker { init(with logger: Logger) { self.trackerLogger = logger.dequeue(withTag: "[tracker]") } func keyboardWillChangeFrame(notification: NSNotification) { let height = 0.0 trackerLogger.debug("\(#function): calculated height - \(height)") } } 

Also habe ich den gesamten KeyboardTracker zugesagt, was gut ist. Wenn Tester Probleme fanden, nahm ich die Protokolldatei und suchte genau, wo die Frames nicht passten. Dies dauerte zu lange, daher wurden neben der Protokollierung auch andere Methoden angewendet.

Wachhund


In unserem Projekt wird Watchdog verwendet, um den UI-Stream zu optimieren. Dies sagte Dmitry Kurkin auf einer der vergangenen AppsConf .

Ein Watchdog ist ein Prozess oder Thread, der einen anderen Prozess oder Thread überwacht. Mit diesem Mechanismus können Sie den Status der Tastatur und die davon abhängigen Ansichten überwachen und Probleme melden.

Um diese Funktionalität zu implementieren, erstellen wir einen Timer, der einmal pro Sekunde die korrekte Position der Ansicht mit Hayks Beinen überprüft oder diese protokolliert, wenn es sich um einen Fehler handelt.

 class Watchdog { var timer: Timer? func start ( ) { timer = Timer ( timeInterval: 1.0, repeats: true, block: { ( timer ) in self.woof ( ) } ) } } 

Übrigens können Sie nicht nur die Endergebnisse, sondern auch Zwischenberechnungen protokollieren.

Infolgedessen lieferte die reichliche Protokollierung + Watchdog genaue Daten über das Problem, den Zustand der Tastatur und verkürzte die Zeit zur Behebung von Fehlern, half jedoch Beta-Benutzern, die Fehler bis zur nächsten Version ertragen mussten, nur wenig.

Aber was ist, wenn Watchdog trainiert werden kann, um Probleme nicht nur zu finden, sondern auch zu beheben?

In dem Code, in dem Watchdog die Schlussfolgerung zieht, dass die Koordinaten der Ansicht nicht konvergieren, fügen wir die Methode fixTummyPosition hinzu und setzen die Koordinaten automatisch ein.

Bei dieser Option werden viele nützliche Informationen in meinen Protokollen gesammelt, und Benutzer bemerken überhaupt keine visuellen Probleme. Das scheint großartig zu sein, aber jetzt kann ich keine Probleme mit der Tastatur feststellen.

Es ist hilfreich, der Watchdog-Methode die Möglichkeit hinzuzufügen, einen Testcache zu generieren, wenn ein Fehler erkannt wird. Natürlich wird dieser Code unter der Remout-Konfiguration hinzugefügt.

Jetzt, nach der nächsten Version, können Sie die Testabsturzgenerierung aktivieren. Wenn ein Benutzer Probleme mit der Tastatur hat, stürzt seine Anwendung ab und dank der gesammelten Protokolle können Sie Fehler beheben.

Dashboard


Der letzte Trick, den wir eingeführt haben, ist das Senden von Statistiken, sobald wahtchdog die Statistiken aufgezeichnet hat. Basierend auf den erhaltenen Daten haben wir die Anzahl der erkannten Fehler aufgezeichnet und nach der ersten Iteration wurde die Anzahl der Operationen um das Vierfache reduziert. Natürlich war es nicht möglich, die Probleme auf Null zu reduzieren, aber die Hauptbeschwerden der Benutzer hörten auf.

Nächste Woche findet die Saint AppsConf in St. Petersburg statt, wo Sie nicht nur Konstantin, sondern auch zahlreichen Rednern des iOS-Tracks Fragen stellen können.

Source: https://habr.com/ru/post/de462515/


All Articles