Bygga ett Tinder-esque-kortgränssnitt

När jag inte rasande sveper rätt på Tinder och försöker desperat hitta mina kärlek i ett hav av slumpmässiga människor som jag aldrig har träffat, bygger jag mjukvara och gränssnitt för iPhone. Som det visar sig banade Tinder faktiskt ett otroligt intressant och unikt gestbaserat interaktionsmönster som sätter standarden för många mobila appar i mjukvaruindustrin idag. Ofta kan man höra investerare, formgivare och ingenjörer som hänvisar till deras affärsidé som "Tinder för X" - vilket talar om hur det antar Tinders svep-kapabla kortsystem och tillämpar det på något annat än semi-psykopatisk betygsätt andra människor i bråk av en sekund baserad på bara ett blick på ett fotografi.

Y-Cash-app av Eleken

I helgen snubblat jag över ett Dribbble-inlägg som fick min uppmärksamhet, som skildrade en förenklad version av Tinder-esque-kortgränssnittet. Jag hade inget annat planerat för helgen, så jag bestämde mig för att ta mig tid att implementera det infödda i Swift. Som du kan förvänta dig har jag öppnat min kod på Github och skrivit om min process bakom att bygga komponenter, gester och animationer. Jag letar alltid efter sätt att dyka in i Core Animation och förstå mer om hur man bygger dynamiska, gestbaserade animationer. Det här blev en fantastisk möjlighet för mig att lära mig lite mer om de verktyg som finns för att skapa gränssnitt som är spännande och människor älskar att använda.

SwipeableCardViewContainer

Att interagera med hela denna komponent handlar om att släppa en SwipeableCardViewContainer-vy till din Storyboard (eller kod) och överensstämma med SwipeableCardViewDataSource, SwipeableCardViewDelegate-protokollen. Denna behållarvy är den vy som är ansvarig för att lägga ut alla kort i sig själv och hantera all underliggande logik för att hålla reda på en serie kort. Den är utformad för att vara ganska lik UICollectionView och UITableView som du antagligen redan känner till.

Din DataSource kommer att tillhandahålla ett nummerOfCard, och för varje index kommer ett SwipeableCardViewCard att visas. Samt valfritt en vy att visa under alla kort som ska ses när alla kort har tagits bort.

Varje gång reloadData () anropas kommer det att ta bort alla befintliga kortvyer från övervakningen. Och sätta in de första 3 kortvyerna från dataSource som undervyer. För att uppnå överläggnings- / insättningseffekten där kort verkar staplas ovanför varandra manipuleras ramen på varje kort baserat på dess index.

En horisontell inställning beräknas såväl som en vertikalinmatning och dessa värden tillämpas på ramens ursprung och bredd, relativt gränserna för containervyn. Till exempel är det första kortets index 0, så det kommer att applicera ett 0-inlägg vertikalt och horisontellt så att kortet sitter perfekt i behållaren. Det andra kortets index är 1 så det kommer att skjuta kortets y-ursprung över, minska dets bredd och släppa kortets x-ursprung ner, allt med en faktor 1. Och så vidare för varje synligt kortindex 0 till 3.

En utmaning som jag mötte med den här metoden att uppdatera ramar var att implementera animationen som du ser som ett kort dras bort, där de befintliga korten animerar uppåt och avslöjar ett nytt kort underifrån. Varje gång jag lägger till ett kort i behållaren, skulle jag sätta in subvyn vid ursprung 0. Detta innebar att SwipeableCardViewContainers subvy-array var i omvänd ordning för de förväntade faktiska kortindexen. dvs undervyn vid ursprung 0 var den vy längst bak i visningshierarkin, även om kortindexet som är associerat med den vyn var 2 (det högsta indexet).

När jag sätter in det nya kortet längst ner i bunten, skulle jag infoga vyn vid index 0 i matris med undervyer vilket orsakar ett indexfel. Sedan när jag uppdaterade ramarna för alla vyer till deras nya position baserat på deras nya index skulle det invertera alla kortets positioner. Jag löstte det här problemet genom att säkerställa att jag iterades genom undervyerna med hjälp av .reversed () för att säkerställa att deras ramar uppdaterades baserat på deras faktiska index, inte deras index inom subvyns array.

Inspekterar vyer i Reveal

SwipeableView

Som du hade förväntat dig att den mest komplexa och tidskrävande delen av implementeringen av denna komponent var dragbara korten. Detta krävde användning av mycket komplex matematik (av vilka vissa jag fortfarande inte helt förstår). Det mesta av detta ligger i en UIView-underklass som kallas SwipeableView.

Varje kort underklassificerar SwipeableView som använder en UIPanGestureRecognizer internt för att lyssna på Pan-gester, till exempel att en användare 'griper' ett kort med fingret och flyttar det runt skärmen, sedan flickar det eller lyfter fingret. Gestrecognizers är mycket underskattade API: er som är oerhört enkla och enkla att arbeta med med tanke på hur mycket funktionalitet och kraft de ger.

Varje UIGestureRecognizer har ett tillstånd som ger viss information om vad som har eller inte har hänt. För anmärkning för denna komponent är början, ändrade, slutade tillstånd som anger om användaren har startat en panorering, en panorering pågår eller en panorering är klar. Det finns också några andra tillstånd som Possible, Canceled, Failed som utlöses om en gest ännu inte är registrerad som en pan-gest, eller om användaren avbröt panelen genom att vända sin gest eller om något misslyckades. Denna enkla enum hanterar en hel del komplicerad logik som händer under huven inom UIKit för att avgöra hur användaren interagerar med programvaran.

Jag lyssnade på en otrolig prat nyligen av Andy Matuschak som arbetade på UIKit och han förklarar varför UIKit-teamet använde denna specifika strategi för att hantera gester över andra traditionella metoder som används i React.js. Jag rekommenderar att du lägger dig tid på att lyssna eller titta på detta samtal.

När en pan-gest börjar måste ett par olika saker hända. Som att beräkna initialTouchPoint, en CGPoint som representerar exakt var användaren först startade sin panorering på skärmen i koordinatsystemet i SwipeableView. Detta används för att beräkna en ny förankringspunkt som snart kommer att ställas in som SwipeableViews lagers ankarpunkt.

UIKit beskriver ankarpunkten som en punkt där alla geometriska transformationer tillämpas relativt. Som standard är anchorPoint för alla UIViews den exakta mitten av vyn CGPoint (x: 0.5, y: 0.5).

Alla geometriska manipulationer till vyn sker om den angivna punkten. Till exempel tillämpar en rotationstransform på ett lager med standardförankringspunkten att lagret roterar runt dess centrum. Om du ändrar förankringspunkten till en annan plats skulle lagret rotera runt den nya punkten.

Genom att ställa ankarpunkten till den punkt där användaren började sin pan-gest säkerställer vi att alla översättningar och rotationer sker relativt användarens finger, vilket säkerställer en mycket mer naturlig animering och ger känslan av att användaren faktiskt har tagit tag i kortet.

När förankringspunkten har beräknats och ställts upp uppdateras lagerställningen, alla befintliga animationer (som kan ha startats någon annanstans) tas bort och SwipeableViews lagrasterizationScale ställs in på enhetens skala så att rasterisering sker relativt enhetens faktisk skala och innehåll inte krymps eller förstoras.

När panoreringstillståndet ändras, till exempel att användaren drar fingret (och kortet) runt skärmen, måste en omvandling appliceras på kortet så att kortet följer fingret och ger känslan av att användaren drar kort. Vi vill också utföra denna översättning med en liten rotation i vyn för att ge mer av en 'svängbar' båge till kortet, och få det att uppträda mer som ett kort som användaren ska vända i en viss riktning, i motsats till bara fritt flytta den. Det är här Tinder betonar en 'svep åt vänster' eller 'svep höger' för att acceptera / avvisa paradigm som faktiskt gör att detta gränssnitt känner sig mer kontrollerat och väl avrundat.

Att tillämpa denna transformation är relativt enkel, en CATransform3D skapas och både CATransform3DRotate och CATransform3DTranslate appliceras på den. Rotationen tillämpas baserat på en rotationAngle-beräkning genom att multiplicera vissa inställningar såsom animationDirectionY, rotationAngle och rotationStrength. Rotation Angle anger graden av rotation som appliceras på kortet när det rör sig eller kurvens "intensitet". Som standard är denna kurva π / 10. Rotationsstyrka beräknas baserat på översättningspunkten och en maximal översättningsinställning på 1,0.

När Pan-gesten har avslutats måste en avslutad animering appliceras på kortet så att det vipps av skärmen, eller om användaren inte helt flickade kortet förbi en viss tröskel måste vi animera det tillbaka till sin ursprungliga plats i traven.

Först beräknar vi dragDirection för pan, vilket kräver mycket komplicerad matematik som normaliserar pan gestens översättningspunkt, och utför en reducering för att beräkna den närmaste svepriktningen baserat på några statiska värden som ges till varje svepningsriktning. Medan jag inte implementerade den här logiken helt själv, kunde jag omvända en befintlig öppen källkodsimplementering av en liknande komponent - https://github.com/Yalantis/Koloda och kapsla in denna logik i ett enum som heter SwipeDirection. Varje svepningsriktning har en horisontell position och vertikal position i förhållande till var den är på ett generiskt geometriskt system (som att överst till vänster är horisontellt till vänster och vertikalt längst upp). Med hjälp av denna information kan jag beräkna svepriktningen för en given svepa till antingen vara uppe till vänster, nedre höger, höger eller vänster ... och så vidare.

På samma sätt med hjälp av en normaliserad dragpunkt och gestens dragöversättningspunkt kan jag beräkna dragprocenten som en bråkdel av avståndet från Gestens slutpunkt till 'målpunkten' där ett kort helt och hållet 'vippas'. Denna målpunkt beräknas baserat på en svepprocentmarginal, som bestämmer tröskeln för hur långt denna procentsats måste vara innan en gest anses vara "flickad" nog för att ta bort kortet. Som standard är denna tröskel 0,6, vilket innebär att om en pan-gest är större än eller lika med 60% av avståndet till målpunkten betraktas kortet som flickat och kan tas bort från stacken annars måste det återgå till sitt ursprungliga läge i stacken .

Med hjälp av Facebooks ramverk för POP-animering för att förenkla saker och ge en mer dynamisk animation utanför boxen tillämpar jag en POPBasicAnimation på kortet och översätter dess X- och Y-ursprung till ett värde utanför skärmen. När denna animering är klar kallar jag min delegatfunktion self.delegate? .DidEndSwipe (onView: self) och litar på min delegat (som har lagt till denna SwipeableView som en undervy) för att ta bort denna SwipeableView som en undervy. Det här kortet tas nu helt bort från bunten och jobbet är gjort.

Om dragPercentage inte är 60% eller mer, eller om pan-gestens tillstånd avbryts eller misslyckats, måste jag animera det tillbaka i bunten. Vanligtvis kommer denna animering att vara en liten gummibandliknande fjädrande studsanimation som kommunicerar att svepet misslyckades och användaren har "släppt" kortet, så att det kan flytta tillbaka till sin ursprungliga plats, precis som du kan förvänta dig i verklig fysik utrymme.

Att utföra denna animering är lika enkelt som att använda en POPSpringAnimation på den rotation som redan tillämpades för att vända den från dess aktuella värden till de ursprungliga värdena. Förutom en POPSpringAnimation på översättningen använde vi från dess nuvarande värden till dess ursprungliga värden. POPSpringAnimation kommer att säkerställa att de verkliga värdena överskottas något fram och tillbaka vilket resulterar i våreffekten som vi ville ha.

SampleSwipeableCard

Nu när vi har implementerat SwipeableView är det lika enkelt att skapa anpassade kort som ser annorlunda ut, innehålla sitt eget innehåll som UIL-märken, UII-bilder och UIB-knappar som undervyer som att ärva från SwipeableView. Underklassen är helt ansvarig för att hantera sitt eget innehåll och förlitar sig på superklassen för att ta hand om all den svepbara logik som just implementerades.

Jag har skapat en SampleSwipeableCard-underklass som innehåller en UIL-etikett för en titel och en undertext, samt en röd UIB-knapp med en plusikon och en UIV-bild med en distinkt bakgrundsfärg som innehåller en inre UIImageView. Alla standard, enkla och grundläggande UIKit-element kastas på en Xib exakt som du kan förvänta dig.

I min ViewController ser jag till att jag skapar en serie ViewModels för varje kort som mitt SampleSwipeableCard kan konfigurera sig själv med.

Och jag returnerar ett SampleSwipeableCard konfigurerat för en viewModel vid det givna indexet. Exakt som jag skulle konfigurera en cell inom en UICollectionView för en given ViewModel.

Med hjälp av en kod som jag tidigare använde för att applicera ett rundat hörn och en skugga (en förvånansvärt inte så enkel prestation) för min ombyggnad av det nya iOS 11 App Store-inlägget - kunde jag använda ett liknande rundat hörn och skugga till mina SampleSwipeableCard-vyer.

Något jag har gjort mycket mer nyligen är att snubbla efter något som får mitt öga eller pikar mitt intresse och sedan mycket snabbt hitta mig själv djupt i ogräs som hackar bort det. Animering är förmodligen inte min starka kostym när det gäller att bygga UI: er. Jag har använt animering med UIKit och Core Animation tidigare med mest framgång, även om jag inte är lika säker på det som jag är med att implementera andra saker. Mycket för att det aldrig finns ett rätt eller fel svar på hur man animerar något, utan istället många olika sätt att uppnå samma resultat.

Jag är ett stort fan av Tinders kort-stil gestgränssnitt och tycker att det är ett riktigt unikt sätt att svepa, sortera eller manipulera små och medelstora uppsättningar data oavsett om det är slumpmässiga potentiella kärleksintressen eller något annat. Jag är mycket skyldig med implementeringen ovan för de många öppna källkodsimplementeringarna av det här kortstilgränssnittet som jag upptäckte online, men jag tycker att det är fantastiskt att ingenjörer kan bygga något och öppna källkoden för andra att backa ingenjör för att bygga på eller tillämpas på något annat.

Jag har publicerat den här koden på Github, känn dig fri att gaffa den eller skicka in en begäran om du vill vara intresserad. Låt mig veta om du har några frågor, eller om du gillade det här inlägget och vill att jag ska skriva om något mer detaljerat.

Tack för att du läser! Följ gärna mig på twitter @phillfarrugia

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