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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. Add menuContainer as a subview of scroller 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. :]
  2. Next, pin the top and bottom of menuContainer to the same edges of the scroll view.
  3. 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
}
  1. Similar to installMenuContainer, you add detailContainer as a subview to the scroll view.
  2. The top, bottom and right edges pin to their respective scroll view edges. The leading edge of detailContainer joins to menuContainer.
  3. 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:

  1. The menu container height == the scroll view height
  2. The detail container’s trailing edge pins to the menu container’s leading edge
  3. The menu container’s width == 80
  4. The detail container’s width == the scroll view’s width
  5. The external detail and menu container’s edges anchor to the scroller’s edges

3D sidebar animation

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.

3D sidebar animation

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:

  1. This method takes a view controller, installs it in a UINavigationController then sets the visual style of the navigation bar.
  2. The most important part of view controller containment is addChild(nav). This installs the UINavigationController as a child view controller of RootViewController. 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.

3D sidebar animation

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:

  1. In the first code fragment, you declared a protocol that interested parties can adopt. Inside MenuViewController, you declare a weak delegate property. Using weak in protocol references helps avoid creating a retain cycle.
  2. Next, you implement the UITableViewDelegate method tableView(_:didSelectRowAt:) to pass the selected MenuItem to the delegate.
  3. The last statement is a cosmetic action to deselect the cell and remove its highlight.