Creating WhatsApp-like animations in iOS

How to create a fluid and consistent UITabBar and UIToolBar animation like in WhatsApp for your app.

WhatsApp’s transition from a UITabBar to a UIToolBar.

Sample implementation.

Since I started developing iOS apps, I have been paying more attention to how other extremely successful apps implement UI transitions so that I can implement them in my own ones.

Recently I was developing an app which had a UICollectionView embedded in a UIViewController , which in turn was embedded in a UICollectionViewController , and I wanted to be able to hide the tab bar at the bottom of the screen to show a tool bar when in edit mode.

The view hierarchy in question.

One of the apps I use the most is WhatsApp, but until now I had not tried to dissect how it manages its UI transitions. I noticed that the way it handles the transition and position of the UIToolBar at the bottom of the screen when in edit mode fit perfectly what I wanted to implement.

In this article, I will show you how I implemented it in my app and explain my design decisions. It turns out that doing the above is not trivial and took me a while to figure out. I had to write custom code and too much trial and error. So I hope that this article and the sample project makes your life easier if you want to implement the same.

Analysis

First, we should check how WhatsApp does things:

WhatsApp’s animation slowed down to 10% its original speed.

From what I can tell, WhatsApp fades the UITabBar in or out and at the same time moves the UIToolBar up or down depending on the editing mode.

Hence our plan of action is to do exactly the above.

Planning

These are our requirements:

We should allow the UINavigationBar to use iOS 11’s large titles

to use iOS 11’s large titles We should hide the UIToolBar when it’s not being used

when it’s not being used We should do the same for the UITabBar

Attack

This is where I was completely wrong. I thought that implementing what we discussed above would have been a piece of cake, but it turned out to be extremely convoluted.

Why?

It turns out that using the default animation and system for large titles screws up any simple positioning code your might write for your UIToolBar .

The naive approach

The first thing I tried was to use the UINavigationController ’s default UIToolBar and change its position according to the editing state of the app.

However, as I said above, that doesn’t really work if you are using large titles.

Notice how the UIToolBar gets moved when the large title is animated.

Moreover, I discovered that I needed to manually resize the bounds property of the collection view if I wanted it to cover the whole space between the moved UIToolBar and the UITabBar .

The solution

In order to circumvent that, we need to add a UIToolBar manually rather than using the default one provided by the UINavigationController object associated with our view controller.

First we create it lazily:

private lazy var toolBar: UIToolbar = {

return UIToolbar()

}()

and then set up its constraints in viewDidLoad:

private var toolBarConstraints = [NSLayoutConstraint]() ...

override func viewDidLoad() {

super.viewDidLoad()



...



navBar.prefersLargeTitles = true



navigationController!.isToolbarHidden = true



view.addSubview(toolBar)

toolBar.translatesAutoresizingMaskIntoConstraints = false

toolBarConstraints.append(contentsOf: [

toolBar.leftAnchor.constraint(equalTo: self.view.leftAnchor),

toolBar.rightAnchor.constraint(equalTo: self.view.rightAnchor),

toolBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),

toolBar.topAnchor.constraint(equalTo: tabBar.topAnchor)

])

}

We then create some computed properties which we use throughout the sample view controller to setup the custom UIToolBar correctly:

private var screenHeight: CGFloat {

return UIScreen.main.bounds.height

}



private var toolBarYPos: CGFloat {

if currentState == .normal {

return screenHeight

}

// Ideally check for other states here, but since we only have two, keep it

// simple; would do something like: else if currentState == .edit {

else {

return screenHeight - toolBar.frame.height

}

}

Finally, we use these computed properties to position the UIToolBar and/or the UITabBar correctly in cases. So when the user taps the Edit or Done buttons:

The animation we wanted.

and this is achieved (partially, but mostly) with this code:

@IBAction func onEditBtnTouchUp(_ sender: Any) {

// Switch to the editing mode

if currentState == .normal {

currentState = .edit



fade(tabBar, toAlpha: 0, withDuration: 0.2, andHide: true)

UIView.animate(withDuration: 0.2, animations: {

// Set edit to done

self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self,

action: #selector(self.onDoneBtnTouchUp))

// Fade away + btn

self.plusBtn.isEnabled = false

self.plusBtn.tintColor = UIColor.clear



// Position the toolbar

self.toolBar.frame.origin.y = self.toolBarYPos

})

}

}



@objc func onDoneBtnTouchUp(_ sender: Any) {

// Switch to normal state

if currentState == .edit {

currentState = .normal



fade(tabBar, toAlpha: 1, withDuration: 0.2, andHide: false)

UIView.animate(withDuration: 0.2, animations: {

// Set edit to done

self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self,

action: #selector(self.onEditBtnTouchUp))



// Fade in + btn

self.plusBtn.isEnabled = true

self.plusBtn.tintColor = nil



// Position the toolbar

self.toolBar.frame.origin.y = self.toolBarYPos

})

}

Conclusions

The final result should look something like this:

I have setup a repository with the sample project so that you can go and read the full code:

I recommend checking it out, since I deliberately missed some details in the article for the sake of brevity.

It took me a long time to figure this out, because the interplay between the different UIKit elements did not work as I expected.

The main takeaways when developing more advanced UI interactions in Swift on iOS are: