Erstellen einer zunderähnlichen Kartenschnittstelle

Wenn ich nicht wütend auf Tinder herumwische und verzweifelt versuche, die Liebe meines Lebens in einem Meer von zufälligen Menschen zu finden, die ich noch nie getroffen habe, baue ich Software und Schnittstellen für das iPhone. Wie sich herausstellt, hat Tinder tatsächlich ein unglaublich interessantes und einzigartiges gestenbasiertes Interaktionsmuster entwickelt, das heute den Standard für viele Handy-Apps in der Softwareindustrie setzt. Oft hört man Investoren, Designer und Ingenieure, die ihre Geschäftsidee als „Zunder für X“ bezeichnen - was auf die Art und Weise anspielt, in der Tinders wischbares Kartensystem übernommen und auf etwas anderes angewendet wird, als andere Menschen in Brüchen halbpsychopathisch zu bewerten von einer Sekunde, die auf nichts anderem als einem bloßen Blick auf ein Foto basiert.

Y-Cash App von Eleken

Dieses Wochenende bin ich auf einen Dribbble-Post gestoßen, der meine Aufmerksamkeit auf sich gezogen hat und eine vereinfachte Version der Tinder-ähnlichen Kartenschnittstelle darstellt. Ich hatte für das Wochenende nichts anderes geplant und beschloss, mir die Zeit zu nehmen, um es nativ in Swift zu implementieren. Wie zu erwarten, habe ich meinen Code auf Github als Open-Source-Version bereitgestellt und über meinen Prozess beim Erstellen der Komponenten, Gesten und Animationen geschrieben. Ich bin immer auf der Suche nach Möglichkeiten, in die Kernanimation einzutauchen und mehr darüber zu erfahren, wie man dynamische, gestenbasierte Animationen erstellt. Dies war eine großartige Gelegenheit für mich, etwas mehr über die verfügbaren Tools zum Erstellen von Schnittstellen zu erfahren, die aufregend sind und die die Leute gerne verwenden.

SwipeableCardViewContainer

Bei der Interaktion mit dieser gesamten Komponente wird eine SwipeableCardViewContainer-Ansicht in Ihr Storyboard (oder Ihren Code) eingefügt und den Protokollen SwipeableCardViewDataSource und SwipeableCardViewDelegate angepasst. Diese Containeransicht ist die Ansicht, die für das Auslegen aller Karten in sich selbst und das Behandeln der gesamten zugrunde liegenden Logik zum Verfolgen einer Reihe von Karten verantwortlich ist. Es ähnelt UICollectionView und UITableView, mit denen Sie wahrscheinlich bereits vertraut sind.

Ihre DataSource stellt eine Anzahl von Karten und für jeden Index eine SwipeableCardViewCard zur Anzeige bereit. Optional kann eine Ansicht unter allen Karten angezeigt werden, die angezeigt wird, wenn alle Karten weggewischt wurden.

Bei jedem Aufruf von reloadData () werden alle vorhandenen Kartenansichten aus der Übersicht entfernt. Fügen Sie die ersten 3 Kartenansichten aus der dataSource als Unteransichten ein. Um den Overlay / Inset-Effekt zu erzielen, bei dem Karten übereinander gestapelt zu sein scheinen, wird der Rahmen jeder Karte basierend auf ihrem Index manipuliert.

Es wird ein horizontaler Satz sowie ein vertikaler Satz berechnet, und diese Werte werden auf den Ursprung und die Breite des Rahmens relativ zu den Grenzen der Containeransicht angewendet. Beispielsweise ist der Index der ersten Karte 0, sodass vertikal und horizontal eine 0 eingefügt wird, damit die Karte perfekt im Behälter sitzt. Der Index der zweiten Karte ist 1, sodass der y-Ursprung der Karte verschoben, die Breite verringert und der x-Ursprung der Karte um den Faktor 1 nach unten gesenkt wird. Und so weiter für jeden sichtbaren Kartenindex von 0 bis 3.

Eine Herausforderung, der ich mich bei diesem Ansatz der Aktualisierung von Frames gegenübersah, war die Implementierung der Animation, die Sie als Karte sehen, wird weggewischt, wobei die vorhandenen Karten nach oben animiert werden und eine neue Karte von unten sichtbar wird. Jedes Mal, wenn ich dem Container eine Karte hinzufügte, fügte ich die Unteransicht am Ursprung 0 ein. Dies bedeutete, dass das Unteransichten-Array von SwipeableCardViewContainer in umgekehrter Reihenfolge der tatsächlich erwarteten Kartenindizes war. dh Die Unteransicht am Ursprung 0 war die Ansicht, die am weitesten hinten in der Ansichtshierarchie liegt, obwohl der dieser Ansicht zugeordnete Kartenindex 2 war (der höchste Index).

Beim Einsetzen der neuen Karte am unteren Rand des Stapels würde ich die Ansicht bei Index 0 in das Array der Unteransichten einfügen, was zu einer Indexinkongruenz führt. Wenn ich dann die Frames aller Ansichten basierend auf ihrem neuen Index auf ihre neue Position aktualisiere, werden alle Positionen der Karte invertiert. Ich habe dieses Problem behoben, indem ich sichergestellt habe, dass ich die Unteransichten mit .reversed () durchlaufen habe, um sicherzustellen, dass ihre Frames basierend auf ihrem tatsächlichen Index und nicht auf ihrem Index innerhalb des Unteransichts-Arrays aktualisiert wurden.

Ansichten in Reveal überprüfen

SwipeableView

Wie zu erwarten war, waren die ziehbaren Karten der komplexeste und zeitaufwändigste Teil bei der Implementierung dieser Komponente. Dies erforderte eine Menge komplexer Mathematik (von denen ich einige immer noch nicht vollständig verstehe). Das meiste davon befindet sich in einer UIView-Unterklasse namens SwipeableView.

Jede Karte ist eine SwipeableView-Unterklasse, die intern einen UIPanGestureRecognizer verwendet, um auf Pan-Gesten zu warten, z. B. wenn ein Benutzer eine Karte mit dem Finger „greift“ und sie über den Bildschirm bewegt, dann schnippt oder den Finger hebt. Gestenerkenner sind sehr unterschätzte APIs, mit denen man unglaublich einfach und unkompliziert arbeiten kann, wenn man bedenkt, wie viel Funktionalität und Leistung sie bieten.

Jeder UIGestureRecognizer verfügt über einen Status, der Informationen darüber enthält, was passiert ist oder nicht. Bemerkenswert für diese Komponente sind die Zustände Began, Changed, Ended, die angeben, ob der Benutzer einen Pan gestartet hat, ein Pan ausgeführt wird oder ein Pan beendet wurde. Es gibt auch einige andere Zustände wie "Möglich", "Abgebrochen", "Fehlgeschlagen", die ausgelöst werden, wenn eine Geste noch nicht als Schwenkgeste registriert ist oder der Benutzer das Schwenken durch Umkehren seiner Geste abgebrochen hat oder wenn etwas fehlgeschlagen ist. Diese einfache Aufzählung behandelt eine Menge komplizierter Logik, die unter der Haube von UIKit geschieht, um zu bestimmen, wie der Benutzer mit der Software interagiert.

Ich habe kürzlich einen unglaublichen Vortrag von Andy Matuschak gehört, der an UIKit gearbeitet hat, und er erklärt, warum das UIKit-Team diesen speziellen Ansatz verwendet hat, um Gesten gegenüber anderen traditionellen Ansätzen zu handhaben, wie sie in React.js verwendet werden. Ich empfehle, die Zeit zu verbringen, um diesen Vortrag zu hören oder zu sehen.

Wenn eine Schwenkgeste beginnt, müssen einige verschiedene Dinge passieren. Beispiel: Berechnen des initialTouchPoint, eines CGPoint, der genau darstellt, wo der Benutzer seine Schwenkbewegung auf dem Bildschirm im Koordinatensystem der SwipeableView zum ersten Mal gestartet hat. Dies wird verwendet, um einen neuen Ankerpunkt zu berechnen, der bald als Ankerpunkt der Ebene von SwipeableView festgelegt wird.

UIKit beschreibt den anchorPoint als einen Punkt, an dem alle geometrischen Transformationen relativ zu angewendet werden. Standardmäßig ist der Ankerpunkt für alle UIViews die genaue Mitte des Ansichts-CGPoint (x: 0,5, y: 0,5).

Alle geometrischen Manipulationen an der Ansicht erfolgen um den angegebenen Punkt. Wenn Sie beispielsweise eine Rotationstransformation auf eine Ebene mit dem Standardankerpunkt anwenden, wird die Ebene um ihre Mitte gedreht. Wenn Sie den Ankerpunkt an eine andere Stelle ändern, dreht sich die Ebene um diesen neuen Punkt.

Indem Sie den Ankerpunkt auf den Punkt setzen, an dem der Benutzer seine Schwenkgeste begonnen hat, stellen wir sicher, dass alle Übersetzungen und Rotationen relativ zum Finger des Benutzers erfolgen. Dies sorgt für eine weitaus natürlichere Animation und vermittelt den Eindruck, dass der Benutzer tatsächlich einen Griff nach ihm hat die Karte.

Sobald der Ankerpunkt berechnet und festgelegt wurde, wird die Ebenenposition aktualisiert, vorhandene Animationen (die möglicherweise an anderer Stelle gestartet wurden) werden entfernt, und die Ebenenrasterisierungsskala von SwipeableView wird auf die Skalierung des Geräts festgelegt, sodass die Rasterung relativ zur Geräteposition erfolgt Der tatsächliche Umfang und Inhalt wird nicht verkleinert oder vergrößert.

Wenn der Schwenkgestenstatus geändert wird, z. B. wenn der Benutzer seinen Finger (und die Karte) über den Bildschirm zieht, muss eine Transformation auf die Karte angewendet werden, damit die Karte ihrem Finger folgt und den Eindruck erweckt, dass der Benutzer die Karte zieht Karte. Wir möchten diese Übersetzung auch mit einer leichten Drehung in der Ansicht ausführen, um der Karte mehr "flickbaren" Bogen zu verleihen und sie eher wie eine Karte zu verhalten, die der Benutzer in eine bestimmte Richtung bewegen sollte, anstatt nur frei es bewegen. Hier betont Tinder ein "Nach links wischen" oder "Nach rechts wischen", um ein Paradigma zu akzeptieren / abzulehnen, mit dem sich diese Benutzeroberfläche kontrollierter und abgerundeter anfühlt.

Das Anwenden dieser Transformation ist relativ einfach. Es wird eine CATransform3D erstellt, auf die sowohl eine CATransform3DRotate als auch eine CATransform3DTranslate angewendet werden. Die Drehung wird basierend auf einem Rotationswinkel angewendet, der durch Multiplizieren einiger Einstellungen wie animationDirectionY, Rotationswinkel und Rotationsstärke berechnet wird. Der Drehwinkel gibt den Grad der Drehung an, der auf die Karte angewendet wird, wenn sie sich bewegt, oder die 'Intensität' der Kurve. Standardmäßig ist diese Kurve π / 10. Die Rotationsstärke wird basierend auf dem Translationspunkt und einer maximalen Translationseinstellung von 1,0 berechnet.

Sobald die Schwenkgeste beendet ist, muss eine beendete Animation auf die Karte angewendet werden, damit sie vom Bildschirm abweicht. Wenn der Benutzer die Karte nicht vollständig über einen bestimmten Schwellenwert hinaus bewegt hat, müssen wir sie wieder an ihren ursprünglichen Ort animieren im Stapel.

Zuerst berechnen wir die DragDirection des Pan, was eine Menge komplizierter Mathematik erfordert, die den Übersetzungspunkt der Pan-Geste normalisiert, und führen eine Reduzierung durch, um die nächstgelegene Swipe-Richtung basierend auf einigen statischen Werten zu berechnen, die jeder Swipe-Richtung zugewiesen wurden. Obwohl ich diese Logik selbst nicht vollständig implementiert habe, konnte ich eine vorhandene Open Source-Implementierung einer ähnlichen Komponente - https://github.com/Yalantis/Koloda - zurückentwickeln und diese Logik in eine Aufzählung namens SwipeDirection kapseln. Jede Wischrichtung hat eine horizontale Position und eine vertikale Position in Bezug auf die Position in einem generischen geometrischen System (z. B. oben links horizontal links und vertikal oben). Mit diesen Informationen kann ich die Wischrichtung eines bestimmten Wischs so berechnen, dass sie entweder oben links, unten rechts, rechts oder links ist ... und so weiter.

In ähnlicher Weise kann ich unter Verwendung eines normalisierten Ziehpunkts und des Ziehübersetzungspunkts der Geste den Ziehungsprozentsatz als Bruchteil der Entfernung vom Endpunkt der Geste zum "Zielpunkt" berechnen, an dem eine Karte vollständig "geschleudert" würde. Dieser Zielpunkt wird basierend auf einem prozentualen Wischspielraum berechnet, der den Schwellenwert dafür bestimmt, wie weit dieser Prozentsatz sein muss, bevor eine Geste als "bewegt" genug angesehen wird, um die Karte zu entfernen. Standardmäßig beträgt dieser Schwellenwert 0,6. Wenn also eine Schwenkgeste größer oder gleich 60% der Entfernung zum Zielpunkt ist, gilt die Karte als gedreht und kann aus dem Stapel entfernt werden. Andernfalls muss sie an ihre ursprüngliche Position im Stapel zurückkehren .

Mit dem POP-Animationsframework von Facebook, um die Dinge zu vereinfachen und eine sofort einsatzbereite dynamische Animation bereitzustellen, wende ich eine POPBasicAnimation auf die Karte an, die ihren X- und Y-Ursprung in einen Wert außerhalb des Bildschirms übersetzt. Sobald diese Animation abgeschlossen ist, rufe ich meine Delegatenfunktion self.delegate? .DidEndSwipe (onView: self) auf und verlasse mich auf meinen Delegaten (der diese SwipeableView als Unteransicht hinzugefügt hat), um diese SwipeableView als Unteransicht zu entfernen. Diese Karte wird nun vollständig vom Stapel entfernt und ihre Arbeit ist erledigt.

Wenn der dragPercentage nicht 60% oder mehr beträgt oder wenn der Status der Schwenkgeste abgebrochen oder fehlgeschlagen ist, muss ich ihn wieder im Stapel animieren. In der Regel handelt es sich bei dieser Animation um eine kleine, gummibandartige, federnde Sprunganimation, die mitteilt, dass der Wisch fehlgeschlagen ist und der Benutzer die Karte „losgelassen“ hat, sodass sie genau wie erwartet in die ursprüngliche Position zurückfliegen kann Physikraum der realen Welt.

Das Ausführen dieser Animation ist so einfach wie das Anwenden einer POPSpringAnimation auf die bereits angewendete Drehung, um sie von ihren aktuellen Werten auf die ursprünglichen Werte umzukehren. Neben einer POPSpringAnimation für die Übersetzung haben wir die aktuellen Werte auf die ursprünglichen Werte angewendet. POPSpringAnimation stellt sicher, dass die tatsächlichen Werte leicht hin und her überschritten werden, was zu dem gewünschten Federeffekt führt.

SampleSwipeableCard

Nachdem wir SwipeableView implementiert haben, können benutzerdefinierte Karten erstellt werden, die anders aussehen und eigene Inhalte wie UILabels, UIImages und UIButtons enthalten, da Unteransichten so einfach sind wie das Erben von SwipeableView. Die Unterklasse selbst ist streng für die Verwaltung ihrer eigenen Inhalte verantwortlich und verlässt sich auf die Oberklasse, um sich um die gesamte gerade implementierte Swipe-fähige Logik zu kümmern.

Ich habe eine SampleSwipeableCard-Unterklasse erstellt, die ein UILabel für einen Titel und einen Untertitel sowie einen roten UIButton mit einem Plus-Symbol und eine UIView mit einer eindeutigen Hintergrundfarbe enthält, die eine innere UIImageView enthält. Alle Standard-, einfachen und grundlegenden UIKit-Elemente werden genau so auf ein Xib geworfen, wie Sie es erwarten würden.

In meinem ViewController stelle ich sicher, dass ich für jede Karte eine Reihe von ViewModels erstelle, mit denen sich meine SampleSwipeableCard selbst konfigurieren kann.

Und ich gebe eine SampleSwipeableCard zurück, die für ein viewModel am angegebenen Index konfiguriert ist. Genau so, wie ich eine Zelle in einer UICollectionView für ein bestimmtes ViewModel konfigurieren würde.

Mit einem Code, den ich zuvor zum Anwenden abgerundeter Ecken und eines Schattens verwendet hatte (eine überraschend nicht so einfache Aufgabe), konnte ich beim Neuerstellen des neuen iOS 11 App Store-Beitrags eine ähnliche abgerundete Ecke anwenden und Schatten zu meinen SampleSwipeableCard-Ansichten.

Etwas, was ich in letzter Zeit viel getan habe, ist, auf etwas zu stoßen, das meine Aufmerksamkeit erregt oder mein Interesse weckt, und mich dann sehr schnell tief im Unkraut zu finden, das sich daran hackt. Animation ist wahrscheinlich nicht meine Stärke, wenn es darum geht, Benutzeroberflächen zu erstellen. Ich habe Animation mit UIKit und Core Animation zuvor mit größtenteils Erfolg angewendet, obwohl ich damit nicht so sicher bin wie bei der Implementierung anderer Dinge. Meistens, weil es nie eine richtige oder falsche Antwort gibt, wie man etwas animiert, sondern viele verschiedene Wege, um das gleiche Ergebnis zu erzielen.

Ich bin ein großer Fan von Tinders Gestenoberfläche im Kartenstil und denke, es ist eine wirklich einzigartige Art, kleine und mittlere Datensätze zu wischen, zu sortieren oder zu manipulieren, egal ob es sich um zufällige potenzielle Liebesinteressen handelt oder um etwas anderes. Ich verdanke einen Großteil der obigen Implementierung den vielen Open-Source-Implementierungen dieser Schnittstelle im Kartenstil, die ich online entdeckt habe, aber ich finde es großartig, dass Ingenieure etwas erstellen und ihren Code als Open-Source-Code für andere zurückentwickeln können, um darauf aufzubauen oder auf etwas anderes anwenden.

Ich habe diesen Code auf Github veröffentlicht. Wenn Sie interessiert sind, können Sie ihn gerne teilen oder eine Pull-Anfrage einreichen. Lassen Sie mich wissen, wenn Sie Fragen haben oder wenn Ihnen dieser Beitrag gefallen hat und ich über etwas ausführlicher schreiben soll.

Danke fürs Lesen! Fühlen Sie sich frei, mir auf Twitter @phillfarrugia zu folgen

http://www.phillfarrugia.com/2017/10/22/building-a-tinder-esque-card-interface/