Create a Cool 3D Sidebar Menu Animation
In this tutorial, you’ll learn how to manipulate CALayer properties on views in order to create a cool 3D sidebar animation. By Warren Burton.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Create a Cool 3D Sidebar Menu Animation
30 mins
- Getting Started
- Restructuring Your Storyboard
- Deleting the Old Structure
- Adding a New Root Container
- Adding Identifiers to View Controllers
- Creating Contained View Controllers
- Creating a Scroll View
- Creating Containers
- Adding Contained View Controllers
- Reconnect Menu and Detail Views
- Creating a Delegate Protocol
- Implementing the MenuDelegate Protocol
- Controlling the Scroll View
- Adding a Menu Button
- Creating a Hamburger View
- Installing the Hamburger View
- Adding Perspective to the Menu
- Manipulating the Menu Layer
- Rotating the Burger Button
- Where to Go From Here?
Creating Containers
Now, you’re going to create UIView
instances that will act as containers for MenuViewController
and DetailViewController
. You’ll then add them to the scroll view.
Add these properties at the top of RootViewController
:
let menuWidth: CGFloat = 80.0
var menuContainer = UIView(frame: .zero)
var detailContainer = UIView(frame: .zero)
Next, add this method to RootViewController
:
func installMenuContainer() {
// 1
scroller.addSubview(menuContainer)
menuContainer.translatesAutoresizingMaskIntoConstraints = false
menuContainer.backgroundColor = .orange
// 2
menuContainer.leadingAnchor.constraint(equalTo: scroller.leadingAnchor)
.isActive = true
menuContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
.isActive = true
menuContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
.isActive = true
// 3
menuContainer.widthAnchor.constraint(equalToConstant: menuWidth)
.isActive = true
menuContainer.heightAnchor.constraint(equalTo: scroller.heightAnchor)
.isActive = true
}
Here’s what you’re doing with this code:
- Add
menuContainer
as a subview ofscroller
and give it a temporary color. Using off-brand colors while developing is a good way to see how your work is going during development. :] - Next, pin the top and bottom of
menuContainer
to the same edges of the scroll view. - Finally, set the width to a constant value of 80.0, and pin the height of the container to the height of the scroll view.
Next, add the following method to RootViewController
:
func installDetailContainer() {
//1
scroller.addSubview(detailContainer)
detailContainer.translatesAutoresizingMaskIntoConstraints = false
detailContainer.backgroundColor = .red
//2
detailContainer.trailingAnchor.constraint(equalTo: scroller.trailingAnchor)
.isActive = true
detailContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
.isActive = true
detailContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
.isActive = true
//3
detailContainer.leadingAnchor
.constraint(equalTo: menuContainer.trailingAnchor)
.isActive = true
detailContainer.widthAnchor.constraint(equalTo: scroller.widthAnchor)
.isActive = true
}
- Similar to
installMenuContainer
, you adddetailContainer
as a subview to the scroll view. - The top, bottom and right edges pin to their respective scroll view edges. The leading edge of
detailContainer
joins tomenuContainer
. - Finally, the width of the container is always the same as the width of the scroll view.
For UIScrollView
to scroll its content, it needs to know how big that content is. You can do that either by using the contentSize
property of UIScrollView
or by defining the size of the content implicitly.
In this case, the content size is implicitly defined by five things:
- The menu container height == the scroll view height
- The detail container’s trailing edge pins to the menu container’s leading edge
- The menu container’s width == 80
- The detail container’s width == the scroll view’s width
- The external detail and menu container’s edges anchor to the scroller’s edges
The last thing to do is to use these two methods. Add these lines at the end of viewDidLoad()
:
installMenuContainer()
installDetailContainer()
Build and run your app to see some candy colored wonder. You can drag the content to hide the orange menu container. Already, you can see the finished product starting to form.
Adding Contained View Controllers
You’re building up the stack of views you’ll need to create your interface. The next step is to install MenuViewController
and DetailViewController
in the containers you’ve created.
You’ll still want to have a navigation bar, because you want a place to put a menu reveal button. Add this extension to the end of RootViewController.swift:
extension RootViewController {
func installInNavigationController(_ rootController: UIViewController)
-> UINavigationController {
let nav = UINavigationController(rootViewController: rootController)
//1
nav.navigationBar.barTintColor = UIColor(named: "rw-dark")
nav.navigationBar.tintColor = UIColor(named: "rw-light")
nav.navigationBar.isTranslucent = false
nav.navigationBar.clipsToBounds = true
//2
addChild(nav)
return nav
}
}
Here’s what’s going on in this code:
- This method takes a view controller, installs it in a
UINavigationController
then sets the visual style of the navigation bar. - The most important part of view controller containment is
addChild(nav)
. This installs theUINavigationController
as a child view controller ofRootViewController
. This means that events like a trait change as a result of rotation or split view on iPad can propagate down the hierarchy to the children.
Next, add this method to the same extension after installInNavigationController(_:)
to help install MenuViewController
and DetailViewController
:
func installFromStoryboard(_ identifier: String,
into container: UIView)
-> UIViewController {
guard let viewController = storyboard?
.instantiateViewController(withIdentifier: identifier) else {
fatalError("broken storyboard expected \(identifier) to be available")
}
let nav = installInNavigationController(viewController)
container.embedInsideSafeArea(nav.view)
return viewController
}
This method instantiates a view controller from the storyboard, warning the developer of a break in the storyboard.
The code then places the view controller inside a UINavigationController
and embeds that navigation controller inside the container.
Next, add these properties in the main class to keep track of MenuViewController
and DetailViewController
:
var menuViewController: MenuViewController?
var detailViewController: DetailViewController?
Then insert these lines at the end of viewDidLoad()
:
menuViewController =
installFromStoryboard("MenuViewController",
into: menuContainer) as? MenuViewController
detailViewController =
installFromStoryboard("DetailViewController",
into: detailContainer) as? DetailViewController
In this fragment, you instantiate MenuViewController
and DetailViewController
and keep a reference to them because you’ll need them later.
Build and run the app and you’ll see that the menu is visible, although a little skinnier than before.
The buttons don’t cause DetailViewController
to update because that segue no longer exists. You’ll fix that in the next section.
You’ve finished the view containment section of the tutorial. Now you can move onto the really fun stuff. :]
Reconnect Menu and Detail Views
Before you went on your demolition rampage, selecting a table cell in MenuViewController
triggered a segue that passed the selected MenuItem
to DetailViewController
.
It was cheap and it got the job done, but there’s a small problem. The pattern requires MenuViewController
to know about DetailViewController
.
That means that MenuViewController
has a tight binding to DetailViewController
. What happens if you no longer want to use DetailViewController
to show the results of your menu choice?
As good developers, you should seek to reduce the amount of tight binding in your system. You’ll set up a new pattern now.
Creating a Delegate Protocol
The first thing to do is to create a delegate protocol in MenuViewController
, which will allow you to communicate menu selection changes.
Locate MenuViewController.swift in the Project navigator and open the file.
Since you are no longer using a segue, you can go ahead and delete prepare(for:sender:)
.
Next, add this protocol definition above the MenuViewController
class declaration:
protocol MenuDelegate: class {
func didSelectMenuItem(_ item: MenuItem)
}
Next, insert the following code inside the body of MenuViewController
:
//1
weak var delegate: MenuDelegate?
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
//2
let item = datasource.menuItems[indexPath.row]
delegate?.didSelectMenuItem(item)
//3
DispatchQueue.main.async {
tableView.deselectRow(at: indexPath, animated: true)
}
}
Here’s what this code does:
- In the first code fragment, you declared a protocol that interested parties can adopt. Inside
MenuViewController
, you declare aweak
delegate
property. Usingweak
in protocol references helps avoid creating a retain cycle. - Next, you implement the
UITableViewDelegate
methodtableView(_:didSelectRowAt:)
to pass the selectedMenuItem
to thedelegate
. - The last statement is a cosmetic action to deselect the cell and remove its highlight.