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: