By Sergii Ganushchak and Roman Sherbakov
Every time we design screens that feature friend lists or contact lists, we face the problem of choosing between list view and grid view. Although list view usually provides more details about each user or contact, grid view allows more users or contacts to appear on the screen at the same time.
Sometimes, you can’t say for sure which variant is best for a particular use case. That’s why we designed a UI that allows users to switch between list and grid views on the fly and choose the most convenient display type for themselves.
Figure 1: Two ways to view an UI on a mobile device
In addition to usernames and profile pictures, list view also provides information about posts, comments, and likes. A list view can include any information you need while browsing your friends list.
Grid view displays only profile pictures and usernames. This lets us fit more profiles on one screen. Grid view is useful when you’re looking for a specific user and don’t need any additional information.
We created design mockups for both list and grid views using Sketch. As soon as the mockups were ready, I used Principle to create a smooth transition between the two display types.
Contact Display Switch Animation Use Cases
You can use our Contact Display Switch for:
- Social networking apps
- Dating apps
- Email clients
- Any other app that features list of friends or contacts
Furthermore, the DisplaySwitcher component that we created based on the idea of Contact Display Switch animation is not limited to friends lists and contact lists; it can work with any other content. It’s up to your imagination!
Developing a DisplaySwitcher Component
First, we’ll tell you how you can use our DisplaySwitcher component in your own iOS project. Then, we’ll look under the hood and see how the animated transition between two collection view layouts works.
How to Use It
To begin, you need to create two layouts—one for displaying a list and another for displaying a grid:
private lazy var listLayout = BaseLayout(staticCellHeight: listLayoutStaticCellHeight, nextLayoutStaticCellHeight: gridLayoutStaticCellHeight, layoutState: .ListLayoutState) private lazy var gridLayout = BaseLayout(staticCellHeight: gridLayoutStaticCellHeight, nextLayoutStaticCellHeight: listLayoutStaticCellHeight, layoutState: .GridLayoutState)
Parameters:
- staticCellHeight: The height of the current cell
- nextLayoutStaticCellHeight: The height of the next layout’s cell
- layoutState: The layout state (list or grid)
After the layouts are ready, you need to set the current layout for the collection view (in our case, that’s listLayout) and set the current layout using CollectionViewLayoutState enum:
collectionView.collectionViewLayout = listLayout private var layoutState: CollectionViewLayoutState = .ListLayoutState
Next, override two required methods of the collection view datasource:
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
And also, override one method of the collection view delegate:
func collectionView(collectionView: UICollectionView, transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, newLayout toLayout: UICollectionViewLayout) -> UICollectionViewTransitionLayout { let customTransitionLayout = TransitionLayout(currentLayout: fromLayout, nextLayout: toLayout) return customTransitionLayout }
At this point, return the TransitionLayout instance. This means that you are going to use a custom transition. You can find more info on this method here.
Finally, you must make layout changes for some events (like pressing a button) using the TransitionManager class instance:
let transitionManager: TransitionManager if layoutState == .ListLayoutState { layoutState = .GridLayoutState transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: gridLayout, layoutState: layoutState) } else { layoutState = .ListLayoutState transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: listLayout, layoutState: layoutState) } transitionManager.startInteractiveTransition()
Parameters:
- animationDuration: Time duration of the transition
- collectionView: Current collection view
- destinationLayout: The layout you’re switching to
- layoutState: The state of the layout you’re switching to
That’s it! Now you know how to use our component!
Going Under the Hood
We use five classes to implement our DisplaySwitcher:
- BaseLayout is a class that deals with building layouts and overrides the UICollectionViewLayout methods for calculations of the required contentOffset when switching from one layout to another.
- BaseLayoutAttributes is a class for adding custom attributes.
- TransitionLayout is a class that defines the custom attributes.
- TransitionManager is a class that uses TransitionLayout and deals with the transition between layouts according to preset time durations.
- RotationButton is a custom class that inherits from UIButton, and is used for a button that animates transition between the layouts.
Let’s explore these classes in more detail.
BaseLayout
In the BaseLayout class, we use methods for building list and grid layouts. But, what’s most interesting here is the contentOffset calculation that should be defined after the transition to a new layout.
First, save the contentOffset of the layout you are switching from:
override func prepareForTransitionFromLayout(oldLayout: UICollectionViewLayout) { previousContentOffset = NSValue(CGPoint: collectionView!.contentOffset) return super.prepareForTransitionFromLayout(oldLayout) }
Then, calculate the contentOffset for the new layout in the targetContentOffsetForProposedContentOffset method:
override func targetContentOffsetForProposedContentOffset (proposedContentOffset: CGPoint) -> CGPoint { let previousContentOffsetPoint = previousContentOffset?.CGPointValue() let superContentOffset = super.targetContentOffsetForProposedContentOffset (proposedContentOffset) if let previousContentOffsetPoint = previousContentOffsetPoint { if previousContentOffsetPoint.y == 0 { return previousContentOffsetPoint } if layoutState == CollectionViewLayoutState. ListLayoutState { let offsetY = ceil(previousContentOffsetPoint.y + (staticCellHeight * previousContentOffsetPoint.y / nextLayoutStaticCellHeight) + cellPadding) return CGPoint(x: superContentOffset.x, y: offsetY) } else { let realOffsetY = ceil((previousContentOffsetPoint.y / nextLayoutStaticCellHeight * staticCellHeight / CGFloat(numberOfColumns)) - cellPadding) let offsetY = floor(realOffsetY / staticCellHeight) * staticCellHeight + cellPadding return CGPoint(x: superContentOffset.x, y: offsetY) } } return superContentOffset }
And then, clear the value of the variable in the finalizeLayoutTransition method:
override func finalizeLayoutTransition() { previousContentOffset = nil super.finalizeLayoutTransition() }
BaseLayoutAttributes
In the BaseLayoutAttributes class, a few custom attributes are added:
var transitionProgress: CGFloat = 0.0 var nextLayoutCellFrame = CGRectZero var layoutState: CollectionViewLayoutState = .ListLayoutState
transitionProgress is the current value of the animation transition that varies between 0 and 1. It’s needed for calculating constraints in the cell (see example on GitHub).
nextLayoutCellFrame is a property that returns the frame of the layout you switch to. It’s also used for the cell layout configuration during the process of transition.
layoutState is the current state of the layout.
TransitionLayout
The TransitionLayout class overrides two UICollectionViewLayout methods, layoutAttributesForElementsInRect and layoutAttributesForItemAtIndexPath, where we set properties values for the last BaseLayoutAttributes.
TransitionManager
The TransitionManager class uses the UICollectionView‘s startInteractiveTransitionToCollectionViewLayout method, where you point the layout it must switch to:
func startInteractiveTransition() { UIApplication.sharedApplication() .beginIgnoringInteractionEvents() transitionLayout = collectionView.startInteractiveTransitionToCollectionViewLayout (destinationLayout, completion: { success, finish in if success && finish { self.collectionView.reloadData() UIApplication.sharedApplication() .endIgnoringInteractionEvents() } }) as! TransitionLayout transitionLayout.layoutState = layoutState createUpdaterAndStart() }
The CADisplayLink class is used to control animation duration. This class helps calculate the animation progress depending on the animation duration preset:
private func createUpdaterAndStart() { start = CACurrentMediaTime() updater = CADisplayLink(target: self, selector: Selector("updateTransitionProgress")) updater.frameInterval = 1 updater.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) } dynamic func updateTransitionProgress() { var progress = (updater.timestamp - start) / duration progress = min(1, progress) progress = max(0, progress) transitionLayout.transitionProgress = CGFloat(progress) transitionLayout.invalidateLayout() if progress == finishTransitionValue { collectionView.finishInteractiveTransition() updater.invalidate() } }
That’s it! Use our DisplaySwitcher in any way you like! Check it out on GitHub.
And, here’s our Contact Display Switch animation on Dribbble.
About the Authors
Sergii Ganushchak is a mobile UX/UI designer. He loves his family, his job, and his bike. You can follow Sergii on Dribbble, where he posts his latest works, and on Twitter.
Roman Sherbakov is an iOS developer at Yalantis.