TECH

How to Make a Modified AppStore-like Animation

Updated: {{date}}

{{time}} min read

Contents

Summarize this article with AI

In the mobile world everybody knows how a great UI may increase user engagement, retention rates and overall delight from using your application. And it’s impossible to imagine any really cool design without beautiful animations. So today we’re going to build one on our own!

More specifically, we're going to share with you our insights on how to make the nice AppStore-like animation with some interesting modifications. Let’s get it started!

👨‍💻 Step 1: Setup a Layout

First, let’s make a simple UI for our demo app. Since we’re developing it for iOS devices, our interface should be similar to AppStore’s UI.

NOTE: We're not aiming to make a perfect, scalable layout. The main goal is just to make a cool animation.

Now let’s do some usual setup in our code:

// MARK: - UICollectionViewDelegates
    
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 10
}
    
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    return collectionView.dequeueReusableCell(withReuseIdentifier: "ExpandableCell", for: indexPath)
}
override func awakeFromNib() {
    super.awakeFromNib()
	
    configureAll()
}
    
// MARK: - Configuration
    
private func configureAll() {
    configureCell()
}
    
private func configureCell() {
    self.contentView.backgroundColor = .white
    self.contentView.layer.cornerRadius = 10
    self.contentView.layer.masksToBounds = true
    
    self.layer.masksToBounds = false
    self.layer.shadowColor = UIColor.black.cgColor
    self.layer.shadowOpacity = 0.3
    self.layer.shadowOffset = CGSize(width: 0, height: 0)
    self.layer.shadowRadius = self.contentView.layer.cornerRadius
}

At the moment our newly created demo app looks like this:

Finally, we have something to work with and are ready to create the outstanding AppStore-like animation. Let’s take a closer look at what should be done.

⚙️ Step 2: Developing the Animation

So what do we expect from our animation?

When you select a cell, this is what you should see:

  • The selected cell expands to the full screen;
  • Its corner radius equals 0;
  • The status bar is hidden;
  • The collection view’s isScrollEnabled is false.

When you tap again on the selected cell, the following things should happen:

  • The selected cell collapses to its initial frame;
  • The selected cell’s corner radius is back to its initial value;
  • The status bar is visible again;
  • The collection view’s isScrollEnabled is true.

So, if we translate this into the coding language, it will look similar to this:

// MARK: - Expanding/Collapsing Logic

func expand(in collectionView: UICollectionView) {
    initialFrame = self.frame
    initialCornerRadius = self.contentView.layer.cornerRadius
    
    self.contentView.layer.cornerRadius = 0
    self.frame = CGRect(x: 0, y: collectionView.contentOffset.y, width: collectionView.frame.width, height: collectionView.frame.height)
    
    layoutIfNeeded()
}

func collapse() {
    self.contentView.layer.cornerRadius = initialCornerRadius ?? self.contentView.layer.cornerRadius
    self.frame = initialFrame ?? self.frame
    
    initialFrame = nil
    initialCornerRadius = nil
    
    layoutIfNeeded()
}
private var expandedCell: ExpandableCell?
private var isStatusBarHidden = false

override var prefersStatusBarHidden: Bool {
    return isStatusBarHidden
}

// …

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    if collectionView.contentOffset.y < 0 ||
        collectionView.contentOffset.y > collectionView.contentSize.height - collectionView.frame.height {
        return
    }
    
    if let selectedCell = expandedCell {
        isStatusBarHidden = false

        selectedCell.collapse()

        collectionView.isScrollEnabled = true

        expandedCell = nil
    } else {
        isStatusBarHidden = true

        collectionView.isScrollEnabled = false

        let selectedCell = collectionView.cellForItem(at: indexPath)! as! ExpandableCell
        selectedCell.expand(in: collectionView)

        expandedCell = selectedCell
    }

    self.setNeedsStatusBarAppearanceUpdate()
}

Seems quite good. But where is the main part - our cool animation? It’s right here! 🙂

To build it, we’ll use UIViewPropertyAnimator. If you aren't familiar with it, we recommend you to go and check it out right now. It’s a truly awesome thing!

But let’s come back to our animation. To develop it, we will use UISpringTimingParameters, because it’s very similar to what Apple uses to create the animation in AppStore. Also, we’ll disable user interactions while the selected cell animates to avoid any unexpected behaviour. And then the code should be like the following:

// ...

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    if collectionView.contentOffset.y < 0 ||
        collectionView.contentOffset.y > collectionView.contentSize.height - collectionView.frame.height {
        return
    }
    
    
    let dampingRatio: CGFloat = 0.8
    let initialVelocity = CGVector.zero
    let springParameters = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: initialVelocity)
    let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: springParameters)
    
    
    self.view.isUserInteractionEnabled = false
    
    if let selectedCell = expandedCell {
        isStatusBarHidden = false
        
        animator.addAnimations {
            selectedCell.collapse()
        }
        
        animator.addCompletion { _ in
            collectionView.isScrollEnabled = true
            
            self.expandedCell = nil
        }
    } else {
        isStatusBarHidden = true
        
        collectionView.isScrollEnabled = false
        
        let selectedCell = collectionView.cellForItem(at: indexPath)! as! ExpandableCell
        
        expandedCell = selectedCell
        
        animator.addAnimations {
            selectedCell.expand(in: collectionView)
        }
    }
    
    
    animator.addAnimations {
        self.setNeedsStatusBarAppearanceUpdate()
    }

    animator.addCompletion { _ in
        self.view.isUserInteractionEnabled = true
    }
    
    animator.startAnimation()
}

Let’s see what we already done. That’s how cool we are:

Hmm, looks like we forgot about the fact that each next cell is higher at the layer level than the previous one. At that moment we thought: what if… we’ll solve this problem and add a cool feature at the same moment?

Namely, we’ll force visible cells (except the selected one) to hide from the screen. Eventually, it should seem that the selected cell pushes the remaining cells off the screen. Let’s implement it and check how it looks!

🛠️ Step 3: Adding a Modification to Our Animation

First, we will add a new property called hiddenCells. We need to remember which cells will be hidden, so that later we can show them back on the screen.

private var hiddenCells: [ExpandableCell] = []

And now imagine that our cells have “show” and “hide” methods. In this case the changes are really simple and should look similar to the snippet below:

if let selectedCell = expandedCell {
    isStatusBarHidden = false
    
    animator.addAnimations {
        selectedCell.collapse()
        
        for cell in self.hiddenCells {
            cell.show()
        }
    }
    
    animator.addCompletion { _ in
        collectionView.isScrollEnabled = true
        
        self.expandedCell = nil
        self.hiddenCells.removeAll()
    }
} else {
    isStatusBarHidden = true
    
    collectionView.isScrollEnabled = false
    
    let selectedCell = collectionView.cellForItem(at: indexPath)! as! ExpandableCell
    let frameOfSelectedCell = selectedCell.frame
    
    expandedCell = selectedCell
    hiddenCells = collectionView.visibleCells.map { $0 as! ExpandableCell }.filter { $0 != selectedCell }
    
    animator.addAnimations {
        selectedCell.expand(in: collectionView)
        
        for cell in self.hiddenCells {
            cell.hide(in: collectionView, frameOfSelectedCell: frameOfSelectedCell)
        }
    }
}

Also, let’s take a closer look at what should be done in “show” and “hide” methods.

To understand where exactly the hidden cells should move to, here is a small blueprint for you:

There's nothing complicated, so the code will look pretty simple, like the one below:

// MARK: - Showing/Hiding Logic

func hide(in collectionView: UICollectionView, frameOfSelectedCell: CGRect) {
    initialFrame = self.frame
    
    let currentY = self.frame.origin.y
    let newY: CGFloat
    
    if currentY < frameOfSelectedCell.origin.y {
        let offset = frameOfSelectedCell.origin.y - currentY
        newY = collectionView.contentOffset.y - offset
    } else {
        let offset = currentY - frameOfSelectedCell.maxY
        newY = collectionView.contentOffset.y + collectionView.frame.height + offset
    }
    
    self.frame.origin.y = newY
    
    layoutIfNeeded()
}

func show() {
    self.frame = initialFrame ?? self.frame
    
    initialFrame = nil
    
    layoutIfNeeded()
}

🏆 And Finally…

Let's see our final version!

We did a great job since it really looks nice!

We hope you enjoyed this article. You can also check the full project on our GitHub.

If you have any questions, we will gladly answer them. Also we’re open to any suggestions so feel free to share some. All this you can do by hitting the button below. Have a nice day! :)

Building Apps for EV, IoT, Fitness & Digital Health since 2017.

Need a Dev Team that gets things done?

Let's Talk

Stormotion client David Lesser, CEO from Numina

They were a delight to work with. And they delivered the product we wanted. Stormotion fostered an enjoyable work atmosphere and focused on delivering a bug-free solution.

David Lesser, CEO

Numina

Andrii Bondarenko

Andrii Bondarenko

Content Team Lead @ Stormotion

Content-Marketing Maestro @ Stormotion. Crafting compelling stories for a brighter world.

Andrii Bo...

Andrii Bondarenko

Andrii Bondarenko

Content Team Lead @ Stormotion

Content-Marketing Maestro @ Stormotion. Crafting compelling stories for a brighter world.

Andrii Bo...

Oleksii Bulavka

Oleksii Bulavka

CTO @ Stormotion

CTO and Co-Founder at Stormotion. JS-Evangelist with a PhD in Computer Science & System Analysis. Cr...

Oleksii Bulavka

Oleksii Bulavka

CTO @ Stormotion

CTO and Co-Founder at Stormotion. JS-Evangelist with a PhD in Computer Science & System Analysis. Cr...

Read also

Let's Build Something Great Together?

Drop us a message