
Zunder - wir alle wissen, dass dies eine Dating-Anwendung ist, bei der Sie einfach jemanden ablehnen oder akzeptieren können, indem Sie nach links oder rechts wischen. Diese Idee für Kartenleser wird jetzt in unzähligen Anwendungen verwendet. Diese Art der Datenanzeige ist für Sie geeignet, wenn Sie die Verwendung von Tabellen- und Sammlungsansichten satt haben. Es gibt viele Lehrbücher zu diesem Thema, aber dieses Projekt hat mich viel Zeit gekostet.
Sie können das vollständige Projekt auf meinem
Github sehen .
Zunächst möchte ich den
Beitrag von Phill Farrugia zu diesem Thema und dann die
YouTube- Serie im Big Mountain-Studio zu einem ähnlichen Thema würdigen. Wie machen wir diese Schnittstelle? Ich habe Hilfe bei der Veröffentlichung von Phil zu diesem Thema bekommen. Im Wesentlichen besteht die Idee darin, UIViews zu erstellen und diese als Unteransichten in die Containeransicht einzufügen. Anhand des Index geben wir dann jedem UIView eine horizontale und vertikale Einfügung und ändern seine Breite geringfügig. Wenn Sie einen Finger über eine Karte ziehen, werden alle Frames der Ansichten entsprechend dem neuen Indexwert neu angeordnet.
Wir beginnen mit der Erstellung einer Containeransicht in einem einfachen ViewController.
class ViewController: UIViewController {
Wie Sie sehen können, habe ich meine eigene Klasse namens SwipeContainerView erstellt und nur stackViewContainer mithilfe automatischer Einschränkungen konfiguriert. Nichts Schlimmes. Die Größe der SwipeContainerView beträgt 300 x 400 und wird auf der X-Achse und nur 60 Pixel über der Mitte der Y-Achse zentriert.
Nachdem wir stackContainer konfiguriert haben, gehen wir zur Unterklasse von StackContainerView und laden alle Arten von Maps hinein. Vorher werden wir ein Protokoll erstellen, das drei Methoden hat:
protocol SwipeCardsDataSource { func numberOfCardsToShow() -> Int func card(at index: Int) -> SwipeCardView func emptyView() -> UIView? }
Stellen Sie sich dieses Protokoll als TableViewDataSource vor. Durch die Übereinstimmung unserer ViewController-Klasse mit diesem Protokoll können Informationen zu unseren Daten an die SwipeCardContainer-Klasse übertragen werden. Es gibt drei Methoden:
numberOfCardsToShow () -> Int
: Gibt die Anzahl der Karten zurück, die numberOfCardsToShow () -> Int
werden müssen. Es ist nur ein Datenarray-Zähler.card(at index: Int) -> SwipeCardView
: gibt SwipeCardView zurück (wir werden diese Klasse in einem Moment erstellen)EmptyView
-> Wir werden nichts damit EmptyView
, aber sobald alle Karten gelöscht sind, gibt der Aufruf dieser Delegate-Methode eine leere Ansicht mit einer Meldung zurück (ich werde dies in dieser speziellen Lektion nicht implementieren, versuchen Sie es selbst).
Richten Sie den View Controller an diesem Protokoll aus:
extension ViewController : SwipeCardsDataSource { func numberOfCardsToShow() -> Int { return viewModelData.count } func card(at index: Int) -> SwipeCardView { let card = SwipeCardView() card.dataSource = viewModelData[index] return card } func emptyView() -> UIView? { return nil } }
Die erste Methode gibt die Anzahl der Elemente im Datenarray zurück. Erstellen Sie in der zweiten Methode eine neue SwipeCardView () -Instanz, senden Sie die Array-Daten für diesen Index und geben Sie dann die SwipeCardView-Instanz zurück.
SwipeCardView ist eine Unterklasse von UIView mit UIImage, UILabel und einer Gestenerkennung. Dazu später mehr. Wir werden dieses Protokoll verwenden, um mit der Präsentation des Containers zu kommunizieren.
stackContainer.dataSource = self
Wenn der obige Code ausgelöst wird, wird die Funktion reloadData aufgerufen, die dann diese Datenquellenfunktionen aufruft.
Class StackViewContainer: UIView { . . var dataSource: SwipeCardsDataSource? { didSet { reloadData() } } ....
ReloadData-Funktion:
func reloadData() { guard let datasource = dataSource else { return } setNeedsLayout() layoutIfNeeded() numberOfCardsToShow = datasource.numberOfCardsToShow() remainingcards = numberOfCardsToShow for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) } }
In der Funktion reloadData erhalten wir zuerst die Anzahl der Karten und speichern sie in der Variablen numberOfCardsToShow. Dann weisen wir dies einer anderen Variablen mit dem Namen verbleibende Karten zu. In der for-Schleife erstellen wir eine Karte, die eine Instanz von SwipeCardView ist, unter Verwendung des Indexwerts.
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) }
Tatsächlich möchten wir, dass weniger als 3 Karten gleichzeitig erscheinen. Daher verwenden wir die min-Funktion. CardsToBeVisible ist eine Konstante gleich 3. Wenn numberOfToShow größer als 3 ist, werden nur drei Karten angezeigt. Wir erstellen diese Karten aus dem Protokoll:
func card(at index: Int) -> SwipeCardView
Die Funktion addCardView () wird einfach zum Einfügen von Karten als Unteransichten verwendet.
private func addCardView(cardView: SwipeCardView, atIndex index: Int) { cardView.delegate = self addCardFrame(index: index, cardView: cardView) cardViews.append(cardView) insertSubview(cardView, at: 0) remainingcards -= 1 }
In dieser Funktion fügen wir cardView zur Hierarchie der Ansichten hinzu und fügen Karten als Unteransicht hinzu. Wir reduzieren die verbleibenden Karten um 1. Sobald wir cardView als Unteransicht hinzufügen, legen wir den Rahmen dieser Karten fest. Dazu verwenden wir eine andere Funktion addCardFrame ():
func addCardFrame(index: Int, cardView: SwipeCardView) { var cardViewFrame = bounds let horizontalInset = (CGFloat(index) * self.horizontalInset) let verticalInset = CGFloat(index) * self.verticalInset cardViewFrame.size.width -= 2 * horizontalInset cardViewFrame.origin.x += horizontalInset cardViewFrame.origin.y += verticalInset cardView.frame = cardViewFrame }
Diese addCardFrame () - Logik stammt direkt aus Phils Beitrag. Hier setzen wir den Rahmen der Karte entsprechend ihrem Index. Die erste Karte mit Index 0 hat einen Rahmen, genau wie ein Container. Dann ändern wir den Ursprung des Rahmens und die Breite der Karte entsprechend der Einfügung. Daher fügen wir die Karte ein wenig rechts von der Karte oben hinzu, verringern ihre Breite und ziehen die Karten notwendigerweise nach unten, um das Gefühl zu erzeugen, dass die Karten übereinander gestapelt sind.
Sobald dies erledigt ist, werden Sie sehen, dass die Karten übereinander gestapelt sind. Ziemlich gut!

Jetzt müssen wir der Kartenansicht jedoch eine Wischgeste hinzufügen. Wenden wir uns nun der SwipeCardView-Klasse zu.
SwipeCardView
Die swipeCardView-Klasse ist eine reguläre Unterklasse von UIView. Aus Gründen, die nur Apple-Ingenieuren bekannt sind, ist es jedoch unglaublich schwierig, einem UIView mit einer abgerundeten Ecke Schatten hinzuzufügen. Um Kartenansichten Schatten hinzuzufügen, erstelle ich zwei UIViews. Eines davon ist shadowView und dann swipeView. Im Wesentlichen hat shadowView einen Schatten und das wars. SwipeView hat abgerundete Ecken. In swipeView habe ich UIImageView hinzugefügt, ein UILabel zur Darstellung von Daten und Bildern.
var swipeView : UIView! var shadowView : UIView!
Einstellen von shadowView und swipeView:
func configureShadowView() { shadowView = UIView() shadowView.backgroundColor = .clear shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 0, height: 0) shadowView.layer.shadowOpacity = 0.8 shadowView.layer.shadowRadius = 4.0 addSubview(shadowView) shadowView.translatesAutoresizingMaskIntoConstraints = false shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true } func configureSwipeView() { swipeView = UIView() swipeView.layer.cornerRadius = 15 swipeView.clipsToBounds = true shadowView.addSubview(swipeView) swipeView.translatesAutoresizingMaskIntoConstraints = false swipeView.leftAnchor.constraint(equalTo: shadowView.leftAnchor).isActive = true swipeView.rightAnchor.constraint(equalTo: shadowView.rightAnchor).isActive = true swipeView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true swipeView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true }
Dann habe ich diesem Kartentyp einen Gestenerkenner hinzugefügt, und die Auswahlfunktion wird bei Erkennung aufgerufen. Diese Auswahlfunktion verfügt über eine große Logik zum Scrollen, Kippen usw. Mal sehen:
@objc func handlePanGesture(sender: UIPanGestureRecognizer){ let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y) switch sender.state { case .ended: if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return } UIView.animate(withDuration: 0.2) { card.transform = .identity card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) self.layoutIfNeeded() } case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation) default: break } }
Die ersten vier Zeilen im obigen Code:
let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
Zuerst bekommen wir die Idee, mit der die Geste gehalten wurde. Als nächstes verwenden wir die Übertragungsmethode, um herauszufinden, wie oft der Benutzer die Karte getroffen hat. Die dritte Zeile erhält im Wesentlichen den Mittelpunkt des übergeordneten Containers. Die letzte Zeile, in der wir card.center installieren. Wenn der Benutzer einen Finger über die Karte streicht, wird die Mitte der Karte um den übersetzten Wert von x und den übersetzten Wert von y erhöht. Um dieses Fangverhalten zu erzielen, ändern wir den Mittelpunkt der Karte erheblich von festen Koordinaten. Wenn die Übersetzung der Gesten beendet ist, geben wir sie an card.center zurück.
Im Falle von state.ended:
if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }
Wir prüfen, ob card.center.x größer als 400 ist oder ob card.center.x kleiner als -65 ist. Wenn ja, dann werfen wir diese Karten ab und ändern die Mitte.
Wenn Sie nach rechts wischen:
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
Wenn Sie nach links wischen:
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
Wenn der Benutzer die Geste in der Mitte zwischen 400 und -65 beendet, setzen wir die Mitte der Karte zurück. Wir rufen auch die Delegate-Methode auf, wenn das Wischen endet. Dazu später mehr.
Um diese Neigung zu erhalten, wenn Sie über die Karte streichen; Ich werde brutal ehrlich sein. Ich habe ein wenig Geometrie verwendet und verschiedene Werte für die Senkrechte und die Basis verwendet und dann die Bräunungsfunktion verwendet, um den Drehwinkel zu ermitteln. Auch dies war nur Versuch und Irrtum. Die Verwendung von point.x und der Breite des Containers als zwei Perimeter schien gut zu funktionieren. Fühlen Sie sich frei, mit diesen Werten zu experimentieren.
case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation)
Lassen Sie uns nun über die Delegatenfunktion sprechen. Wir werden die Delegate-Funktion verwenden, um zwischen SwipeCardView und ContainerView zu kommunizieren.
protocol SwipeCardsDelegate { func swipeDidEnd(on view: SwipeCardView) }
Diese Funktion berücksichtigt den Typ, in dem das Wischen stattgefunden hat, und wir werden mehrere Schritte ausführen, um es aus den Unteransichten zu entfernen und dann alle Frames für die Karten darunter zu wiederholen. So:
func swipeDidEnd(on view: SwipeCardView) { guard let datasource = dataSource else { return } view.removeFromSuperview() if remainingcards > 0 { let newIndex = datasource.numberOfCardsToShow() - remainingcards addCardView(cardView: datasource.card(at: newIndex), atIndex: 2) for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } }else { for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } } }
Entfernen Sie zuerst diese Ansicht aus der Superansicht. Überprüfen Sie anschließend, ob noch eine Karte vorhanden ist. Wenn ja, erstellen wir einen neuen Index für die zu erstellende Karte. Wir werden newIndex erstellen, indem wir die Gesamtzahl der Karten subtrahieren, die mit den restlichen Karten angezeigt werden sollen. Dann fügen wir die Karte als Unteransicht hinzu. Diese neue Karte ist jedoch die niedrigste, sodass die von uns gesendete 2 im Wesentlichen garantiert, dass der hinzugefügte Rahmen mit Index 2 oder der niedrigsten übereinstimmt.
Um die Frames der verbleibenden Karten zu animieren, verwenden wir den Subview-Index. Dazu erstellen wir ein Array visibleCards, das alle Unteransichten des Containers als Array enthält.
var visibleCards: [SwipeCardView] { return subviews as? [SwipeCardView] ?? [] }
Das Problem ist jedoch, dass das Array "VisibleCards" einen invertierten Subview-Index hat. Somit ist die erste Karte die dritte, die zweite bleibt auf dem zweiten Platz und die dritte auf der ersten Position. Um dies zu verhindern, führen wir das Array "VisibleCards" in umgekehrter Reihenfolge aus, um den tatsächlichen Unteransichtsindex abzurufen, nicht wie sie sich im Array "VisibleCards" befinden.
for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) }
Jetzt werden wir die Frames der restlichen cardViews aktualisieren.
Das ist alles. Dies ist ein idealer Weg, um eine kleine Datenmenge darzustellen.