Interaction in Evernote

Allsome
3 min readApr 2, 2016

Never forget the amazing experience when I first launched Evernote iOS App, gorgeous design and intuitive interaction run through the application, and I was also impressed by the products in the Evernote Market after further exploration, they are all exquisite and elegant (though a little bit pricey). However, a recent search online shows that the company is on the decline. Anyhow I wish the great team nothing but success as they move forward.

Let me explain how to implement the impressive effect in the above GIF.

the

Spring Effect

In order to let each cell move as user Slide on the screen, I choose UICollectionView other than UITableView, we need a custom layout class inherits from UICollectionViewFlowLayout, the first key action is to override `shouldInvalidateLayoutForBoundsChange` function and return true.

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}

Only when user slide to the top or bottom can trigger the effect, so we need to override layoutAttributesForElementsInRect function and figure out each cell’s frame according to collectionView ’s contentOffset, that’s it!

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let offsetY = self.collectionView!.contentOffset.y
let attrsArray = super.layoutAttributesForElementsInRect(rect)
let collectionViewFrameHeight = self.collectionView!.frame.size.height
let collectionViewContentHeight = self.collectionView!.contentSize.height
let ScrollViewContentInsetBottom = self.collectionView!.contentInset.bottom
let bottomOffset = offsetY + collectionViewFrameHeight - collectionViewContentHeight - ScrollViewContentInsetBottom
let numOfItems = self.collectionView!.numberOfSections()

for attr:UICollectionViewLayoutAttributes in attrsArray! {
if (attr.representedElementCategory == UICollectionElementCategory.Cell) {
var cellRect = attr.frame;
if offsetY <= 0 {
let distance = fabs(offsetY) / SpringFactor;
cellRect.origin.y += offsetY + distance * CGFloat(attr.indexPath.section + 1);
}else if bottomOffset > 0 {
let distance = bottomOffset / SpringFactor;
cellRect.origin.y += bottomOffset - distance * CGFloat(numOfItems - attr.indexPath.section)
}
attr.frame = cellRect;
}
}
return attrsArray;
}

Transition Effect

We need a custom transition class that is named as EvernoteTransition and conforms to both UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate, an object of the class serve as transitioningDelegate of next view controller of the transition. And then implement two key function of UIViewControllerAnimatedTransitioning: transitionDuration and animateTransition, the former one is intended to set the animation’s duration, the latter one is used to deploy the details of the animation. Finally return the object in the implementation of animationControllerForPresentedController and animationControllerForDismissedController.

func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresent = true
return self
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.isPresent = false
return self
}

Gesture

Add an object of UIScreenEdgePanGestureRecognizer for the next view controller, remember its direction is left. We need an object of UIPercentDrivenInteractiveTransition and named interactionController, update the percent object in the gesture’s selector. In the end, return interactionController in the function interactionControllerForDismissal of UIViewControllerTransitioningDelegate.

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
self.isPresent = false
return interactionController
}
func handlePanGesture(recognizer:UIScreenEdgePanGestureRecognizer) {
let view = panViewController.view
if recognizer.state == UIGestureRecognizerState.Began {
panViewController.dismissViewControllerAnimated(true, completion: { () -> Void in })
} else if recognizer.state == UIGestureRecognizerState.Changed {
let translation = recognizer.translationInView(view)
let d = fabs(translation.x / CGRectGetWidth(view.bounds))
interactionController.updateInteractiveTransition(d)
} else if recognizer.state == UIGestureRecognizerState.Ended {
if recognizer.velocityInView(view).x > 0 { interactionController.finishInteractiveTransition()
} else {
interactionController.cancelInteractiveTransition() listViewController.presentViewController(panViewController, animated: false, completion: { () -> Void in })
}
interactionController = UIPercentDrivenInteractiveTransition()
}
}

So far the mission is completed! Check out the source code in GitHub

Note:

The GIF on the top is from Dribbble, I’m very sorry for unauthorised using it without owner’s permission. I will delete the file immediately if making any unexpected inconvenience to you.

--

--

Allsome

Now a iOS developer, maybe a director in the future.