Building Spotify With SwiftUI
Implementing the gestures from Spotify’s Your Library view
Hello and welcome!
This is the first article of my ‘Building with SwiftUI’ series where I write about replicating some of my favourite parts of apps I use on my phone natively in SwiftUI.
I’m using this as way to learn more about UI design, deepen my understanding of the framework and share with anyone who cares along the way 🙂 The series will not focus on any backend implementation and all the code will be available on my Github.
This week I have been looking into implementing Spotify. In particular, the animation of the Your Library view with drag gestures. Below is what I managed to achieve.
So as you can see, I managed to get the gestures pretty close to the official app. I never expected it to be quite as fluid as Spotify’s, but I hope this article will give you some idea of how to implement it. Anyway, lets get into it …
Breaking it down
The core of these gestures really boils down to two things. You have a pager view which enables you to swipe between the various sub categories, such as Playlists, Artists and Albums. And once you get to the boundaries of both sub categories, you have another pager view which lets you swipe between the two categories, Music and Podcasts.
At first glance, implementing the two can seem relatively simple. Using two horizontal scroll views, a combination of each child view’s .onAppear() function and a drag gesture together. However, I quickly realised that this was ambitious and not possible. Compared to a List (which we can’t use because of the divders), using a ScrollView means the .onAppear() methods of all child views get triggered when the view is first shown even if they are not first visible on the screen. Meaning there is no easy way to find out what child view is bieng shown. So scratch that …
Taking a closer look, having two seperate pager views is not a viable solution. You need the category to be linked to the subcategories, so they can share data, that seems obvious right? So you need some sort of nested pager view with which you could track when pages change and have access to the drag gesture for extra customisability. This led me to a deep search on the web and finally came across something that turned out to be the solution.
Introducing SwiftUIPager (creds to Fernando Moya de Rivas)…
SwiftUIPager provides a Pager component built with SwiftUI native components. Pager is a view that renders a scrollable…
I’m always a bit hesitant to use third party tools, but this seems to be really well maintained and I like the level of detail it brings. Plus, theres a nested pager view example. Bingo!
To start, let's look at the individual components and then we can piece them together.
This View will show the two categories we need, Music and Podcasts, and handle highlighting the selected one.
Using a binding which stores the current category index we can update the foreground colour dynamically and using .primary and .secondary means we can handle light and dark mode.
Then to trigger the change when we tap on a category, we use an .onTapGesture for both Text views.
The binding to nestedPages is to keep track of the current subcategory index for each category. We reset both when we tap on a category so that we always show the first sub category. This will become clearer later.
This view will show the sub categories depending on the category. It will use the same logic as the CategoryText view to highlight the selected sub category, and additonally will show the green indicator bar underneath the selected subcategory.
The indicator bar has two important features.
Firstly, it needs to be able to dynamically change its width to the size of the sub category. We can achieve this by using a VStack and the .fixedSize() modifier. This way the width of the VStack is the exact amount we need, in this case the length of the sub category.
Secondly, it needs to be able to slide from one sub category to the other when we swipe between views. We can achieve this by using the .offset(x:) modifier. This way we can just move the indicator along the x-axis when we catch the drag gesture later on.
Since we are just focussing on gestures, rather than content, we can create a common view that can be used for all our sub categories. If we were doing this for real, we would need different behaviour depending on the sub category selected. For our purpose, a ScrollView with rectangles of random colours, to simulate album/playlist covers, will suffice.
We need the .fixedSize(horizontal: true, vertical: false) modifer for later when we embed this in a pager view. This is because the pager view will need a GeometryReader which will cut off the text. This is a known SwiftUI solution as stated here.
Now we have created all the individual components we need we can combine them all together in a single view. This view will need to handle the nested pager view to get the interactions we want.
Before we implement the logic with the drag gesture, we can create a base view which will allow us to swipe between sub categories and switch categories.
To do this we can take the nested pager example I mentioned earlier as a template, and encorporate the components we created above.
There are some things to do with SwiftUIPager that I will leave you to understand by looking at the examples available and the README of the repo, but it’s fairly straightforward.
A @State variable needed which isn’t in the example is indicatorOffsets. This will store the current value of each sub category’s offset for their indicator bar. Although we are not altering them anywhere at the moment, they are needed for the SubCategoryText view we created earlier.
When the nestedPager is invoked via the Pager, we need to do some trickery to create the Binding variables needed. The current sub category index, and the correct indicator offset for the current category. These can then be used to create the view for each sub category. Which also contains a Pager view with the content of our MediaContextView we created.
Swipe to next category
Now we have the base structure, we can look at adding the functionality to switch categories when we swipe on the boundary of the subcategory views.
To handle changing category we will not want our nested pager to react when we swipe as normal, but instead the pager connected to the category. For us to do this, we can make use of one of the SwiftUIPager modifiers: .allowsDragging(value: Bool) and add it to our nested pager. This allows us to temporarily disable the pager whenever we are swiping at the boundary, allowing us to swipe to the next category instead. This is ideal, we just need to figure out when to change the bool …
Under the hood SwiftUIPager will already be catching DragGesture’s, therefore we can make use of the .simultaneousGesture() view modifier to catch when the user swipes, and not break the gestures pre-existing.
We can add the below view modifiers to our top level Pager.
We only want to disable the sub category dragging when we are swiping in the right direction on the boundary of a subcategory view. Therefore, to catch the direction of the DragGesture we use the .onChanged() modifier and use value.translation.width.If its positive then we are swiping right, else left. We can then add a conditional for the two possible cases. Either we are at Artists and swiping left to get from Music to Podcasts, or we are at Episodes and swiping right to get back to Music.
Now all we need is to set the sub category dragging back to true when we catch a change in the category page index. This can be done by using the .onPageChanged() modifier which is also supplied by the SwiftUIPager package.
Updating indicator offset
The last thing needed is to add the animation to the green indicator bars when we change sub category.
This can be achieved pretty easily by using value.translation.width again in the .onChanged modifier. However this time, we want the indicator bar to move in the opposite direction of the direction we swipe in, and we only need to move it in a proportionate amount. I set it to be divided by 10 as a rough ratio for the change in width but I’m sure you could make it more accurate. We also need to set the indicator offset back to 0 when the DragGesture has ended by using the .onEnded modifier. If we were using a @GestureState variable we could skip this, but we wouldn’t be able to pass the variable into other views like we need to do.
There are two cases we still need to think about. When we are at Playlists and swipe right we are unable to go anywhere, therefore don’t want the indicator bar to move. This is the same for when we are at Shows and swipe left. To handle this, we can check the category index, sub category index and direction of the drag gesture. Similar to what we used before swipe between categories. See the complete gesture below:
This should mean we now have a fully working Your Library view! To see the full code checkout my Github.
As mentioned at the start of the article, there are a few aspects which make this solution slightly inferior to Spotify. Firstly, the indicator bar sliding is not quite as smooth, but I expect an animation could fix this. I also noticed that sometimes when tapping to switch category, the MediaContentView is offset by a small amount. As we don’t configure this offset, I assume its a bug with SwiftUIPager but its not that noticeable.
If you enjoyed the article give me a follow (Archie Edwards)! Or if you have any questions please leave a comment down below! Thanks for reading 🙂
I also have a newsletter which is completely free where I write about swift related topics. Feel free to sign up using the link below! 👇