Making a Tinder-esque Card Swiping interface using Swift
Tinder - we all know that dating app where you can just reject or accept someone by swiping right or left. BAM! The whole card swiping idea is now used in tons of apps. It’s a way to show data if you have grown tired of using table views and collection views. There are tons of tutorial on this and this project took me good bit of time.
You can check out the full project on my Github.
First of all, I would like to give credit to Phill Farrugia’s medium post on this and then Big Mountain Studio’s YouTube series on a similar topic. So how do we go about making this interface. I got quite a lot of help from Phill’s medium post on this front. Basically the idea is to create UIViews and insert them as subviews in a container view. Then using the index we will give each UIView some horizontal and vertical inset and tweak its width a bit. Then when we swipe away one card, all the views frames will be rearranged according to the new index value.
We will start by creating a container view in a simple ViewController
.
As you can see I have created a custom class called SwipeContainerView
and just configuring the stackViewContainer
using the auto layout constraints. Nothing too bad. The SwipeContainerView
will be 300 x 400 in size and it will be centered in X axis and just 60 pixels above the middle of Y axis.
Now that we have set up the stackContainer
, we will head over to the StackContainerView
subclass and load all the card views in it. Before doing that we will create a protocol that will have three methods:
Think of this protocol as a TableViewDataSource
. Conforming our ViewController class to this protocol will allow us to transfer information of our data to the SwipeCardContainer
class. The three methods we have in this are:
numberOfCardsToShow() -> Int
: Returns the number of cards we need to show. This is just the count of the data array.card(at index: Int) -> SwipeCardView
: Returns the SwipeCardView(we will create this class in just a moment)EmptyView ->
Not going to do anything with this but once all the cards have been swiped out, call to this delegate method will return an empty view with some sort of message (I will not implement this in this particular tutorial, give it a try! )
Conform the view controller to this protocol:
In the first method, return the number of items in the data array. In the second method, create a new instance of SwipeCardView()
and send the array data for that index and then return the SwipeCardView
instance.
SwipeCardView
is a subclass of UIView
on which there is a UIImage
, UILabel
, and a gesture recognizer
. More on this later. We will use this protocol to communicate with the container view.
stackContainer.dataSource = self
When the above code is triggered, this will trigger a function of reloadData
that will then call these datasource
functions.
The reloadData
function:
In this reloadData
function, we are first getting the number of cards and storing it in numberOfCardsToShow
variable. Then we are assigning that to another variable called remainingCards
. In the for loop, we are creating a card which is instance of SwipeCardView
using the index value.
We essentially want less than 3 cards to appear at any one time. Therefore we are using min
function. CardsToBeVisible
is a constant at 3
. If the numberOfToShow
is more than 3 then only three cards will be rendered. We are creating these cards from protocol method of:
func card(at index: Int) -> SwipeCardView
The addCardView()
function is just used to insert the cards as subviews.
In this function we are adding the cardView to the view hierarchy and as we are adding the cards as subview we are decrementing the remaining cards by 1. Once we have added the cardView
as the subview, we then set the frame of those cards. For this we are using another function of addCardFrame()
:
This addCardFrame()
logic is taken directly from Phill’s post. Here we are setting frame of the card according to the index of that card. The first card with index 0 will have the frame directly as that of the container view. Then we are changing the origin of the frame and width of the card according to the inset. So essentially we add the card slightly to the right of the card above, decrease it’s width, also make sure to pull the cards downwards to get that feeling of the cards being stacked on top of each other.
Once this is done, what you will see is cards being stacked on top of each other. Pretty good so far!
However now we need to add the swipe gesture to this card view. Let’s now bring our attention to the SwipeCardView
class.
SwipeCardView
The swipeCardView
class is just a normal UIView subclass. However, for reasons only known to Apple engineers, it is incredibly difficult to add shadows to a UIView
with rounded corner. In order to add shadows to the card views, I am creating two UIViews
. One is a shadowView and then on top of that there is a swipeView
. The shadowView
essentially has the shadow and all. The swipeView
has rounded corners. On the swipeView
I have added UIImageView
, a UILabel
to showcase the data and pictures.
Configuring shadowView and swipeView
Then I have added a pan gesture recognizer to this card view and on recognition a selector function is called. This selector function has tons of logic behind swipe, tilting etc. Let’s take a look:
The first four lines in the above code:
First we get the view on which the gesture has been swiped. Next we are using the translation method to get how much the user has swiped on the card. The third line is essentially getting the mid point of the parent container. The last line is where we are setting the card.center. As the user swipes on the card, the center of the card increases by translated x
value and translated y
value. In order to get that snapping behavior we are essentially changing the center point of the card from fixed coordinates. When the gesture translation ends we get it back to the card.center.
In state.ended
case:
we are checking if the card.center.x
value is greater than 400
or if the card.center.x
is less than -65
. If it is so then we are dismissing those cards by changing the card.center.
If swiped to the right
:
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
If swiped to the left
:
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
If the user ends the gesture mid way between 400
and -65
well then we will reset the center of the card. We are also calling a delegate method when the swipe ends. More on this later.
For getting that tilt when you swipe on the card; I will be brutally honest. I used bit of geometry and used different values of perpendicular and base and then used tan function to get a rotation angle. Again this was just trial and error. Using the point.x and container’s width as two perimeters seemed to work well. Feel free to experiment with these values.
Now lets talk about the delegate function. We will use the delegate function to communicate between the SwipeCardView
and the ContainerView
.
This function will take in a view on which the swipe took place and we will do some steps on it to remove it from the subviews and then redo all the frames for the cards beneath it. Here is how:
First remove this view from the super view. Once that is done check if there is any remaining card left. If there is, then we will create a new index for a new card to be created. We will create the newIndex by subtracting the total number of cards to show with the remaining cards. Then we will add the card as subview. However, this new card will be the bottom most one so the 2 that we are sending will essentially make sure that the frame we will add is corresponding to the index 2 or the bottom most.
In order to animate the frames of the rest of the cards, we will use the index of the subviews. To do this we will create a visibleCards array that will have all the subviews of the containerView
in it as an array.
The problem however is that visibleCards array will have subviews index inverted. So the first card will be third, second will remain in second place but the third card will be in the first position. To prevent this from happening we will run the visibleCards
array in reversed manner in order to get the actual index of the subview and not how they are arranged in the visibleCards
array.
So now we will update the frames of the rest of the cardViews.
So that’s basically it. This is a perfect way to present small amount of data.