Prototyping floating burger menu with CAShapeLayer



Reproducing above effect is very simple with CAShapeLayer and CoreAnimation…

Intro

Since I wanted get back to blogging and learn more Swift, I’ve decided to implement some of Dribble top UIX interactions on iOS.

Let’s see how I’ve reproduced ‘Floating burger 2.0 by Eddie Lobanovskiy’

How to?

CAShapeLayer can be used to render CGPath, it has multitude of properties, but for the sake of this simple interaction, we only need 2:

  • strokeStart and strokeEnd, as they can be used to control how much of the path is actually drawn

If we keep line more inset in regards to the screen, we can express our whole path as a single Path:

That means we can use strokeStart and strokeEnd to trim the rendering to specific part for both states of our animation.

Now we just need to be careful when creating the path.

Line path

To be able to fully control how our shape is displayed by using this 2 properties, it’s better to manually compose our CGPath, instead of using functions like addArcWithCenter.

Fortunately this Path is just a line with a circle:

let path = UIBezierPath()
let radius: CGFloat = 20
let inset: CGFloat = 30
let lineLength = viewportWidth() - inset
let lineStart = (viewportWidth() - (lineLength - radius)) / 2
path.moveToPoint(CGPoint(x: lineStart, y: 0))
path.addLineToPoint(CGPoint(x: lineLength, y: 0))

let circleCenter = CGPoint(x:lineLength, y: -radius) 
var nextPoint = CGPointZero

let _ = (0..<360).map {
    nextPoint = CGPoint(x: CGFloat(sinf(toRadian($0))) * radius + circleCenter.x, y: CGFloat(cosf(toRadian($0))) * radius + circleCenter.y)
    path.addLineToPoint(nextPoint)
}


First we create the horizontal line, then we compose circle by using basic arithmetic’s sin/cos.

Because strokeEnd and strokeStart are in normalized coordinates (0-1), we need to normalize our line length.

let circleLength = 2.0 * CGFloat(M_PI) * radius
let totalLength = circleLength + lineLength - lineStart
let lineLengthNormalized = (lineLength - lineStart) / totalLength

for me that yields

lineLengthNormalized = 0.67833278554572718

Animation

For configuration

shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = lineLengthNormalized

We get our first animation state

For configuration

shapeLayer.strokeStart = lineLengthNormalized
shapeLayer.strokeEnd = 1

We get our collapsed state

We are only left with one task, animating between both states.

CoreAnimation with CABasicAnimation is all you need, both strokeStart and strokeEnd are expressed by the same animation type.

Given a function prototype like:

func animate(shape: CAShapeLayer, duration: CFTimeInterval, stroke: (start: CGFloat, end: CGFloat), headerAlpha: CGFloat)

The animation for each property looks like this:

//! 1
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = shape.strokeEnd
strokeEndAnimation.toValue = stroke.end
strokeEndAnimation.duration = duration
strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
strokeEndAnimation.fillMode = kCAFillModeBoth

//! 2
shape.strokeEnd = stroke.end
shape.addAnimation(strokeEndAnimation, forKey: strokeEndAnimation.keyPath

The only important bit here is how we ensure that our model matches presentation: CoreAnimation have 2 layers, model and presentationLayer.

When you perform CAAnimation you are only modyfing the presentation part.

Once the animation finishes you will see your object in the old configuration.

To keep our model and presentation in sync:

  • set fromValue to current layer value
  • before running animation set the layer value to our target toValue

Triggering both states on scroll is straightforward:

func scrollViewDidScroll(scrollView: UIScrollView) {
  let deadZone = (start: CGFloat(10), end: CGFloat(50))
  if (scrollView.contentOffset.y < deadZone.start && isCollapsed) {
      animateToOpen(headerShapeLayer, duration: 0.25)
  } else
      if (scrollView.contentOffset.y > deadZone.end && !isCollapsed) {
          animateToCollapsed(headerShapeLayer, duration: 0.25)
      }
  }

Conclusion

UIKit and CoreGraphics are very powerful, we can achieve a lot of great looking effects without a lot of work.

It’s worth investing your time into learning more about CoreGraphics. Make sure to read more about power of CALayer compositions.

I wanted to use Swift Playgrounds, but since I wanted User interaction I’ve used mine, they now support Swift as well as Objective-C.

Related:




If you hate writing repetitive code SourceryPro is here.

Want to optimize your development workflow? I'd love to help your team.