Slide-In List Animation for iOS Apps

Slide-In List Animation for iOS Apps

Create a custom iOS control to duplicate the slide-in list effect from the most recent Bandcamp app.

Go to the profile of  Jonathan Banks
Jonathan Banks
18 min read

This article will provide sample code and step-by-step guidance to re-create the colorful slide-in list animation found in the current Bandcamp app.


The Bandcamp iOS app offers up excellent design and animation effects throughout its interface. In particular, one animation stands out that's tied to search button functionality. After the user taps the search button, a colorful scrolling list populated with various genres of music animates into place. Here's a recording of that animation:

It is a slick animation effect, reminiscent of a colorful fan opening up.  Simple but impactful effects like this will make an app stand out amongst the pack.  Users love delightful animations as long as effort is expended to ensure they don't get in the way of the performance of the app.

How did the Bandcamp developers make this animation? It turns out that it doesn't take a lot of complex code to simulate the effect. Here's a demonstration of the end result of a custom version of the animation, what is built by following along with this tutorial:

There's a slight difference from the Bandcamp animation. Long-press a row and ripple effects spawn across the other buttons. Good artists copy, great artists steal, and then add their unique spin.

Project Code

Download the source code for the entire project from the site repo on Alternatively, follow along step-by-step with this article and manually add code to the project.

The assumption is that the reader has enough experience to setup an XCode project, create segues between UIViewControllers, connect class variables to code in Interface Builder, and more. In other words, handling the basics is not part of this tutorial.

Project Overview

Think through the requirements, controls, and objects needed to recreate the animated list. Two UIViewControllers are necessary for this demonstration because holding a row triggers a ripple effect and tapping a row sends the user to a new screen.

  • The main UIViewController is the entry point into the app and holds the custom control animated list.
  • The second UIViewController displays the background color and word from the row the user selected.

It is necessary to strategize when designing the way the animated list will be used to make the code reusable.

Ideally, the control should be reusable and customizable in other projects. Think from the perspective of external developers adding the class to their code. The question that arises is whether or not those developers would want the control to scroll?

It's possible that the Bandcamp developers extended UITableView and added their animations to its time-tested capabilities and scrolling efficiencies. Still, that sounds like a lot of work. In most cases, presenting a small isolated list to the user that doesn't require scrolling is adequate. The second option is far easier to code than the first. It also crosses the finish line faster while keeping the codebase simple.

It's decided then, the non-scrolling route is preferable for this demonstration.

Basic Setup

Go ahead and do the following:

  1. Drag a second UIViewController into Interface Builder
  2. Create a file that extends UIViewController and name it TransitionViewController When a user taps a row this class will be triggered.
  3. Assign that UIViewController as an instance of TransitionViewController in the Identity Inspector in Interface Builder.
  4. Drag a segue between the main UIViewController and the TransitionViewController, click on it, and assign it the name segueToTransition in the Attribute Inspector in Interface Builder.
  5. Create another file named SlideButtonsView that extends UIView. This is the main custom class for the animated list.
  6. Create a file named Extensions. This will hold a few extensions to UIColor we'll use to generate the a palatte of colors for the rows.
  7. Create a file named SlideButtonViewDelegate. The delegate will be implemented in the main UIViewController instance created in the TransitionViewController and then called from there to grab and set its background color and title from the row the user has selected.

At this point, the basic project is setup, all the necessary files are created, and coding can begin.


Coding the main custom animated control will be the biggest challenge so tackle that first. Paste the following code into the SlideButtonsView file:

 import UIKit

class SlideButtonsView: UIView {

   var ROW_HEIGHT:CGFloat = 50.0
   var OFFSET:CGFloat = 10.0
   var buttons:[UIButton]!
   var titles:[String]!
   var colors:[String]!
   var triggerSegue: (()->())?
   var selectedTitle:String?
   var selectedColor:String?

Breaking down the purpose of these class variables:

  • ROW_HEIGHT: size of each of the animated rows.
  • OFFSET: distance between rows as they're animated into place. Increase or decrease this value to make the animated effect more/less pronounced.
  • buttons:[UIButton]: reference mapping from row to button. Required to do the ripple effect animation. Each row is an instance of UIButton for ease of touch interaction.
  • titles:[String]: labels for each row.
  • colors:[String]: hex string values for the colors in each row. These will be used in conjunction with the extension for UIColor that transforms the strings into color values.
  • triggerSegue: (()->())?: closure that communicates to the parent UIViewController the user tapped one of the rows in the list. Signals to trigger the segue to the TransitionViewController
  • selectedTitle: title of the user selected row.
  • selectedColor: string value of the user selected row color

Weak Vs. Strong References
Note that the buttons array maintains strong references to each button. For a "production" version of the control, this should instead be an array of pointers using weak references from a NSPointerArray. Why though?

For example, code is added that permits end users to delete and add a row to the control. If code is not added to remove that row from the buttons:[UIButton] array on deletion - there would be a memory leak. Over time, with lots of user additions and deletions, the app will crash.

Paste the following code under the variables you just created to handle the initializers for the class:

override init(frame: CGRect) {
    super.init(frame: frame)


init(titles:[String], colors: [String], rowHeight:CGFloat, initialGapBetweenButtons:CGFloat) {

    super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: CGFloat(rowHeight * CGFloat(titles.count))))

    self.titles = titles
    self.colors = colors
    self.ROW_HEIGHT = rowHeight
    self.OFFSET = initialGapBetweenButtons

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

The initialization code overrides the coder initializer so Interface Builder can adjust the custom control. There is also a custom initializer added that passes in parameters for titles, colors, rowHeight, and initialGapBetweenButtons. This allows manual initialization and customization of the control from the UIViewController.

Next, implement the setupButtons method that's called from the initializers. The method configures each row in the list and positions all the components for animation. Go ahead and paste the following code under the initializers:

 Sets up the rows in the list, positioning them for animation.
private func setupButtons(){
    buttons = [UIButton]()
    // Base number of rows/buttons on number of titles
    for (count,title) in titles.enumerated(){

        let button = UIButton.init(frame: CGRect.init(x: 0, y: 0, width: CGFloat(frame.width), height: CGFloat(ROW_HEIGHT)))
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17.0)
        button.titleLabel?.text = title
        button.setTitle(title, for: .normal)
        button.contentHorizontalAlignment = .left
        button.titleEdgeInsets = UIEdgeInsets.init(top: 0.0, left: 10.0, bottom: 0.0, right: 0.0)
        button.titleLabel?.textColor = UIColor.white.withAlphaComponent(1.0)
        //Set row to dark gray In case fewer colors are provided than titles
        button.backgroundColor = count < colors.count ? UIColor.init(hexString:colors[count]) : UIColor.darkGray


         // Position each row/button a little further apart than the previous loops button spacing = CGPoint.init(x: CGFloat(frame.width/2), y: (CGFloat(count + 1) * (ROW_HEIGHT + OFFSET)) - (ROW_HEIGHT/2))
        button.alpha = 0.0

        // Add the triggers for user interaction
        let tgr = UITapGestureRecognizer.init(target: self, action: #selector(selectRowAndAnimate(_:)))

        let lgr = UILongPressGestureRecognizer.init(target: self, action: #selector(enlargeButton(_:)))



At a high-level, the code initializes the buttons array used to track each row. It then loops through the titles array, creating a UIButton for each row and configuring color, title, alignment, etc.

The method broken down into more detail:

  • Sets the background color of the UIButton to the corresponding value in the colors array. If the developer provides fewer colors than titles, make the row dark grey.
  • withAlphaComponent ensures the title text color has full opacity even on modification of the alpha levels of the button.
  • Adds a UITapGestureRecognizer and UILongPressGestureRecognizer to each UIButton. The tap gesture will trigger the selector pointing to the selectRowAndAnimate method. The long-press gesture triggers the enlargeButton method for the ripple effect.
  • Sets each UIButton alpha value to 0.0 to make them invisible before triggering the main animation.

Worth a more detailed discussion is the line that positions the UIButton center: = CGPoint.init(x: CGFloat(frame.width/2), y: (CGFloat(count + 1) * (ROW_HEIGHT + OFFSET)) - (ROW_HEIGHT/2))

Spread each UIButton farther apart from the next to make the animation effect more prominent. It'll appear like the lower rows are "flying in" from progressively further distances off the screen. Achieve the effect as follows, based on the loop:

  • Set each UIButton's frame to span the entire width of the superview to ensure the center CGPoint is center of the custom UIView's frame. .

  • Adjust the y value of the center CGPoint to a greater degree each time the loop executes. Take the current loop count and multiply it with the ROW_HEIGHT + OFFSET value to get its near-final position, a y-value at the top of the row. Then, subtract 1/2 the ROW_HEIGHT to get the midpoint and position the button center.

The increasing offset between rows will add a nice polish to the final animation.

Tap Gesture Method

The next task is to implement the selector methods for each button. These will be triggered from the UITapGestureRecognizer or UILongPressGestureRecognizer.

Let's start with the tap gesture. Paste the following code into the SlideButtonsView:

Trigger when user taps to select a row in the list.  Calls the closure specified in
 ViewController to trigger a segue to the TransitionViewController

 - Parameter sender: The tap gesture recognizer set on each row of the list

@objc func selectRowAndAnimate(_ sender:UITapGestureRecognizer){
    if let button = sender.view as? UIButton{
        if let titleText = button.titleLabel?.text, let bkColor = button.backgroundColor?.toHexString(){
            selectedColor = bkColor
            selectedTitle = titleText



Tapping one of the rows should trigger a segue into the secondary TransitionViewController. That UIViewController needs to know the color and title of the row selected for display purposes. It must set the local class variables selectedColor and selectedTitle.

The delegate class variable in TransitionViewController grabs these values via the protocol method calls.

Long Press Method

The UILongPressGestureRecognizer method is triggered by the user from each row in the list. Paste the following code for the enlargeButton method into the SlideButtonsView:

 Trigger when there's a long press on a specific row. Generates the enlarge animation on
 press and hold and the "ripple" effect when released

 - Parameter sender: the long press gesture recognizer set on each row of the list
@objc func enlargeButton(_ sender:UILongPressGestureRecognizer){

    // Enlarge the selected row while user holds it
    if sender.state == .began{
        if let button = sender.view as? UIButton{
            UIView.animate(withDuration: 0.3, animations: {
                button.transform = CGAffineTransform.init(scaleX: 1.02, y: 1.02)
            }, completion:{ _ in


     // Start the ripple animation when user lets go
    }else if sender.state == .ended{
        if let button = sender.view as? UIButton{

             // Used to provide increasing delay between UIView.animate calls
            var downDelay = 0.1
            var upDelay = 0.1

            UIView.animate(withDuration: 0.3, animations: {
                // reduce the button back to normal size
                button.transform = CGAffineTransform.identity
            }, completion:{_ in

                // Get the index of the selected button in the buttons array
                if let pressedButtonIndex = self.buttons.firstIndex(of: button){
                    // Ripple Down from selected button
                    // Start effect one past selected row to the end of the buttons array
                    for downIndex in Int(pressedButtonIndex + 1)..<self.buttons.count{

                        UIView.animate(withDuration: 0.5, delay: downDelay, animations: {
                            self.buttons[downIndex].transform = CGAffineTransform.init(scaleX: 1.04, y: 1.04)
                        }, completion:{_ in
                            self.buttons[downIndex].transform = CGAffineTransform.identity
                        // Increase the delay between animation trigger
                        downDelay += 0.05

                    if pressedButtonIndex != 0{

                        // Ripple Up from selected button
                        // Start effect from one less than selected row down to 0th element of
                        // the buttons array
                        for upIndex in (0...Int(pressedButtonIndex - 1)).reversed(){

                            UIView.animate(withDuration: 0.5, delay: upDelay, animations: {
                                self.buttons[upIndex].transform = CGAffineTransform.init(scaleX: 1.04, y: 1.04)
                            }, completion: {_ in
                                self.buttons[upIndex].transform = CGAffineTransform.identity

                             // Increase the delay between animation trigger
                            upDelay += 0.05



Lots of code in this method but it's not as complicated as it appears. In summary, the method checks the state of the incoming UILongPressGestureRecognizer to see if it's beginning or has ended to trigger the respective animation.

Pressing and holding will begin the gesture and enlarge the row that's engaged. Releasing that row will end the gesture and trigger the ripple effect.

Breaking it into two parts:

Enlarge the Row

If the gesture is beginning, use the CGAffineTransform to enlarge the row touched by the user. The row will shrink back to standard size when the gesture is in ended state (detailed in the next section).

if sender.state == .began{
    if let button = sender.view as? UIButton{
        UIView.animate(withDuration: 0.3, animations: {
            button.transform = CGAffineTransform.init(scaleX: 1.02, y: 1.02)
        }, completion:{ _ in   })

 // Start the ripple animation when user lets go

The completion closure is left empty as an exercise. Options could include notifying another object, triggering a new animation, substituting a new color for the row, or any number of other possibilities.

Ripple Effect

The more complex animation is the "ripple" effect when the user releases the UIButton. Long-pressing a button will trigger a larger to smaller size animation of all surrounding UIButtons in both directions. This will generate a "wave" or "ripple" type visual as if the user caused a disturbance in the list.

Traverse the list of UIButtons in a loop to add the ripple animations but there is a catch. Adding the animations to each UIButton will trigger it instantly. There will be no delay involved and all UIButtons will expand and contract milliseconds apart. This will result in all the buttons on both sides of the selected button enlarging and contracting at once.

That's not the desired visual. To do it the proper way we'll need to introduce an increasing delay on each subsequent UIButton animation as the loop unrolls.

This is why there are downDelay and upDelay local variables in the method. They set a progressively longer delay to the animation for each row that's encountered in the loop.

Breaking down the code after sender.state == .ended:

  • Animate the UIButton back to its original size using the identity CGAffineTransform
  • On completion, get the index of the UIButton that was long-pressed to mark the starting point of the "ripple". This button is ignored when adding animations to each row to maintain the illusion of the pressed button triggering the ripple.
  • There are two loops to process the "up" ripple effect and the "down" ripple effect, each iteration of the loop will add another 5/100's of a second to the delay in each direction.
    • Down Ripple: start the loop one past the currently long-pressed UIButton, adding a CGAffineTransform to scale the UIButton larger then back to original size with the identity transform.
    • Up Ripple: Only run if the currently pressed UIButton is greater than zero, otherwise it'll be a seg fault out of bounds error. Start the loop one button before the currently long-pressed UIButton by using a loop from 0 that's reversed, applying the same CGAffineTransform to scale larger then back to identity, or original size.

Bandcamp/Main List Animation

Next, we'll implement the central animation that replicates the Bandcamp app's row fly-in effect.

External UIViewControllers should call this animation when the timing is right, from the perspective of the caller's lifecycle. For the demo app, it's called from the UIViewController's viewDidAppear method. The animations should not start from viewDidLoad or viewWillAppear since those run before the end-user can see them.

Paste the following code into SlideButtonsView:

 Performs the main animation when the list is first shown to end user
func animateButtons(){
    for count in 0..<buttons.count{

       let button = buttons[count]
          // Enlarge each button, animate its alpha from invisible to visible, shift its position
        UIView.animate(withDuration: 0.3, delay:Double(count) * 0.05, animations: {
            button.transform = CGAffineTransform.init(scaleX: 1.02, y: 1.02)
                button.alpha = 0.8
                // Move the button back so it's directly adjacent to the one above it by
                // subtracting the offset assigned when initially positioning it
                button.layer.position = CGPoint(x:button.frame.midX, y:button.frame.midY - CGFloat(CGFloat(count) * self.OFFSET))
        }, completion:{_ in
            UIView.animate(withDuration: 0.3, animations: {
                button.transform = CGAffineTransform.identity



This code is less complex than the ripple effect code. One reason is each button is already positioned in its animation spot inside the setupButtons method call. The increasing gap between buttons is set and needs to be shrunk.

The method in a bit more detail:

  • An important animation tweak is making a shifting delay to the slide-in animation effect for each UIButton. We're taking advantage of the loop count to space out the delay for each button further down the list by calling Double(count) * 0.05.
  • The code inside the main UIView.animate call:
    • Scales the UIButton slightly larger with a transform
    • Animates the alpha up from 0.0 to 0.8 to "fade in" each row as it's sliding in.
    • Sets the layer position by reversing the previous calculation in setupButtons to space each UIButton away from each other.
    • Calls the layoutIfNeeded method to force an update.

The resetButtons method is the final requirement for the SlideButtonsView. The parent UIViewController calls this method from viewWillAppear. If it's called sooner, the user will see it reset the positioning of each button row to prepare for triggering the slide-in animation again. Not the desired effect.

Paste the following code into your SlideButtonsView class:

 Repositions each row with offset, should be called prior to animateButtons
func resetButtons(){

    for (count,button) in buttons.enumerated(){
         // give each row an increasing offset from the previous = CGPoint.init(x: CGFloat(frame.width/2), y: (CGFloat(count + 1) * (ROW_HEIGHT + OFFSET)) - (ROW_HEIGHT/2))
        button.alpha = 0.0


The method resets each UIButton to the original shifted and spaced out position the were in during control initialization.


The TransitionViewController uses a delegate to grab the selected color and title from the main UIViewController. That UIViewController implements the methods of the protocol itself.

Create that protocol and the stubs for the methods therein. Go ahead and add the following code to the SlideButtonViewDelegate file:

import UIKit

protocol SlideButtonViewDelegate{
    func getSelectedColor()->String
    func getSelectedTitle()->String


This UIViewController is very simple since it just displays the background color and text from the row the user selected. Paste this code into your TransitionViewController file:

class TransitionViewController: UIViewController {

    @IBOutlet var backgroundView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    var slideDelegate:SlideButtonViewDelegate!
    override func viewDidLoad() {
        self.backgroundView.backgroundColor = UIColor.init(hexString:slideDelegate.getSelectedColor())
        self.titleLabel?.text = slideDelegate.getSelectedTitle()


    @IBAction func returnToMain(_ sender: Any) {
        dismiss(animated: true) 


The background color of the UIViewController and text for the titleLabel are set by calling the SlideButtonViewDelegate methods, getSelectedColor and getSelectedTitle, in the viewDidLoad method.

The slideDelegate variable is set to point to the main UIViewController that's implemented the SlideButtonViewDelegate protocol.

Main UIViewController

Next on the agenda is the implementation of the main UIViewController that serves as the entrypoint for the app. Note: the downloadable project does not rename this class from the generic ViewController. Suggested to refactor that for clarity.

The main UIViewController instantiates the SlideButtonsView, positions it within its own view, and invokes the key animations at the right times in its lifecycle.

Paste the following into your ViewController to create the class variables

var slideButtonsView: SlideButtonsView!
var slideButtonViewTitles:[String] = ["humpback whale", "killer whale", "pilot whale","blue whale","chimpanzee","orangutan","african elephant", "sea otter", "bottlenose dolphin","gorilla","walrus","narwhal","california sea lion", "polar bear", "octopus"]
var slideButtonViewColors:[String] = ["D98A36","D95A36","F84C3E","E02D53", "F725BC", "D524ED", "8B15D6", "6419F7", "180BE0","0D3EFC", "006AE6", "00B8FE", "0CE4E8", "0DFFC0", "00E868"]

These define the data that'll be passed to the SlideButtonsView when the custom control is instantiated.

It makes sense to refactor this code to be more like a UITableView, with a datasource and delegate protocol, so these data structures don't have to be passed around and everything is decoupled. Saving that for a future exercise.

Next, instantiate the custom control in viewDidLoad:

override func viewDidLoad() {
    // Initialize the instance
    slideButtonsView = SlideButtonsView.init(titles: slideButtonViewTitles, colors: slideButtonViewColors, rowHeight: 50, initialGapBetweenButtons: 10)
     // assign the callback segue to trigger the transition to TransitionViewController
     // when a user selects a row in SlideButtonsView
    slideButtonsView.triggerSegue = {
        self.performSegue(withIdentifier: "segueToTransition", sender: self)


    // Set the center of the SlideButtonsView to dead center = CGPoint.init(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)


There's several things going on in viewDidLoad:

  • Create an instance of the SlideButtonsView
  • Assign the closure to that instance that's used to trigger performSegue. Recall that the segue transition to the TransitionViewController needs to happen when a user selects a row in the custom control. The closure is used to perform that communication.
  • Add the newly created instance as a subview to the main UIViewController's view.
  • Position the SlideButtonsView center to the exact center of the main UIViewController.

The next step is to implement some of the lifecycle methods and deal with the segue. Add the following code:

override func viewWillAppear(_ animated: Bool) {

    // Reset the position of the rows before the user can see
    // so the animation is ready to be invoked again

override func viewDidAppear(_ animated: Bool) {
    // Invoke the animation once the VC is visible

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if let tvc = segue.destination as? TransitionViewController{
        // Set the SlideButtonViewDelegate of the TransitionVC to self so the
        // transitioned-to VC can acqire the color and title of the selected row
        tvc.slideDelegate = self

Breaking that down:

  • viewWillAppear: Bringing the main UIViewController to the top of the stack ( visible to the end user) makes it necessary to reset the positioning of each row so the slide-in animation can start fresh. This must happen before calling viewDidAppear.
    • If the resetButtons method is not called from viewWillAppear, the rows will continue to maintain their previous shifted position as the start point for the new shift. This would result in them animating into each other until there's only one row totat. Not desired.
  • viewDidAppear: called first. The rows are reset to their starting position. Now call animateButtons to trigger the slide-in animation since it's visible to the user.
  • prepare: set the SlideButtonViewDelegate that's owned by the TransitionViewController to the main UIViewController so the correct color and title text can be set.

How can the main UIViewController be set as a delegate since there isn't an implemented a protocol? Correct that by implementing the SlideButtonViewDelegate delegate code in the main UIViewController.

Add the following code at the end of the ViewController file:

extension ViewController: SlideButtonViewDelegate{

    func getSelectedColor() -> String {
        return self.slideButtonsView.selectedColor!

    func getSelectedTitle() -> String {
        return self.slideButtonsView.selectedTitle!

These methods will return the selectedColor and selectedTitle values from the SlideButtonsView respectively. Recall, these values were set in the SlideButtonsView selectRowAndAnimate method and store the latest color chosen by the user.


There is one final set of methods to implement in the Extensions file. Paste the following code in there:

import UIKit

extension UIColor {
    convenience init(hexString:String) {
        let hexString:String = hexString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        let scanner = Scanner(string: hexString)
        if (hexString.hasPrefix("#")) {
            scanner.scanLocation = 1
        var color:UInt32 = 0
        let mask = 0x000000FF
        let r = Int(color >> 16) & mask
        let g = Int(color >> 8) & mask
        let b = Int(color) & mask
        let red   = CGFloat(r) / 255.0
        let green = CGFloat(g) / 255.0
        let blue  = CGFloat(b) / 255.0
        self.init(red:red, green:green, blue:blue, alpha:1)
    func toHexString() -> String {
        var r:CGFloat = 0
        var g:CGFloat = 0
        var b:CGFloat = 0
        var a:CGFloat = 0
        getRed(&r, green: &g, blue: &b, alpha: &a)
        let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0
        return String(format:"#%06x", rgb)
    func rgb() -> [CGFloat]? {
        var fRed : CGFloat = 0
        var fGreen : CGFloat = 0
        var fBlue : CGFloat = 0
        var fAlpha: CGFloat = 0
        if self.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) {
            return [fRed, fGreen, fBlue, fAlpha]
        } else {
            // Could not extract RGBA components:
            return nil

Programmatically add colors to your code using strings that set the hex value of the color with this extension. You can also extract the hex value from a color with the toHexString method.

Final Thoughts

The code for the app is now complete. Go ahead and run the app and marvel at the beauty of that colorful animation.

Here are a few final thoughts before wrapping up:

  • Keep in mind that the OFFSET and ROW_HEIGHT variables in SlideButtonsView adjust spacing and fine-tune the animation to taste.
  • Adjusting the delay values and CGAffineTransform scaling in each of the UIView.animate calls provides more control.
  • The Bandcamp animated list is scrollable, indicating it's either an extended UITableView or something like we've implemented here but embedded in a UIScrollView. It could also be completely custom, built from scratch.
  • The control would be infinitely more usable if the user could scroll a larger list of items. Scrolling was not added in this demo but it would be a great exercise for to undertake.

Apple makes developing beautiful animations a simple and straightforward process on this platform. The evidence to that point is in the limited amount of code required to make this fairly complex Bandcamp sliding list clone.

It didn't take very long to code this and that's a testament to the powerful animation classes of the iOS SDK. Of course, the hardware is no slouch, transforming the code into the smoothest renderings on almost any handheld device.

Shameless Bandcamp Plug from the Writer

One final thought that is completely unrelated to programming. Make sure to support your favorite bands by buying their albums. The artists are not paid appropriately by any of the existing streaming services. To those that say, "they can make money touring", I expect your next job to have you on the road 300 days a year so you too can afford to pay your bills.

If you want to have a thriving music scene you must support musicians. If you don't, you're going to see music end up as extinct as dinosaurs. Bandcamp is my first source for purchasing albums now that many medium to larger artists have releases on the service. I'll check if the release is offered there before resorting to the competing alternatives or even getting a physical CD.

I get every format I need from Bandcamp. They have 320kbps downloads for mobile, the option to download FLAC versions so you can keep the highest quality versions, and unlimited streaming in their app. Overall, it's also known for being artist-friendly, which is a real positive. I hope more musicians move to it as it becomes cheaper and easier to record and release albums outside the influence of the record companies.

One last plea. Make sure you support your favorite bands by buying their albums, merchandise, and, of course, seeing them live!