CollectionViewPagingLayout

A simple but highly customizable UICollectionViewLayout for UICollectionView.

Layout Designer


SnapshotTransformView

ScaleTransformView

StackTransformView

Custom implementations

About

This is a simple but powerful framework that lets you make complex layouts for your UICollectionView.
The implementation is quite simple. Just a custom UICollectionViewLayout that gives you the ability to apply transforms to the cells.
No UICollectionView inheritance or anything like that.
For more details, see How to use

Installation

This framework doesn't contain any external dependencies.

CocoaPods

# Podfile
use_frameworks!

target 'YOUR_TARGET_NAME' do
    pod 'CollectionViewPagingLayout'
end

Replace YOUR_TARGET_NAME and then, in the Podfile directory, type:

$ pod install

Carthage

Add this to Cartfile

github "CollectionViewPagingLayout"

and then, in the Cartfile directory, type:

$ carthage update

Swift Package Manager

using Xcode:

File > Swift Packages > Add Package Dependency

Manually

Just add all the files under Lib directory to your project

How to use

Using Layout Designer

There is a macOS app to make it even easier for you to build your custom layout.
It allows you to tweak many options and see the result in real-time.
It also generates the code for you. So, you can copy it to your project.

You can purchase the app from App Store and support this repository,
or you can build it yourself from the source.
Yes, the macOS app is open-source too!.

Manually

  • First, make sure you imported the framework
import CollectionViewPagingLayout
  • Set up your UICollectionView as you always do (you need a custom class for cells)
  • Set the layout for your collection view:
    (in most cases you want a paging effect so enable that too)
let layout = CollectionViewPagingLayout()
collectionView.collectionViewLayout = layout
collectionView.isPagingEnabled = true // enabling paging effect

Note: Go to Prepared Transformable Protocols if you want to use prepared effects! to make a custom effect contiune.

  • Now, you just need to conform your cell class to TransformableView and start implementing your custom transforms.
    for instance:
class YourCell: UICollectionViewCell { /*...*/ }
extension YourCell: TransformableView {
  func transform(progress: CGFloat) {
    // apply changes on any view of your cell
  }
}

As you see above, you get a progress value. Use that to apply any changes you want.

progress is a float value that represents the current position of your cell in the collection view.
When it's 0 that means the current position of the cell is exactly in the center of your collection view.
the value could be negative or positive and that represents the distance to the center of your collection view.
for instance 1 means the distance between the center of the cell and the center of your collection view is equal to your collection view width.

you can start with a simple transform like this:

extension YourCell: TransformableView {
    func transform(progress: CGFloat) {
        let transform = CGAffineTransform(translationX: bounds.width/2 * progress, y: 0)
        let alpha = 1 - abs(progress)

        contentView.subviews.forEach { $0.transform = transform }
        contentView.alpha = alpha
    }
}
  • Don't forget to set numberOfVisibleItems, by default it's null and that means all of the cells will be loaded in the memory.
layout.numberOfVisibleItems = ...

Prepared Transformable Protocols

There are some prepared transformable protocols to make it easier to use this framework.
Using them is simple. You only need to conform your UICollectionViewCell to the protocol.
You can use the options property to tweak it as you want.
There are three types:

  • ScaleTransformView (orange previews)
  • SnapshotTransformView (green previews)
  • StackTransformView (blue previews)
    These protocols are highly customizable, you can make tons of different effects using them.
    Here is a simple example for ScaleTransformView which gives you a simple paging with scaling effect:
extension YourCell: ScaleTransformView {
    var scaleOptions = ScaleTransformViewOptions(
        minScale: 0.6,
        scaleRatio: 0.4,
        translationRatio: CGPoint(x: 0.66, y: 0.2),
        maxTranslationRatio: CGPoint(x: 2, y: 0),
    )
}

There is an "options" property for each of these protocols where you can customize the effect, check the struct to find out what each parameter does.
A short comment on the top of each parameter explains what that does.
ScaleTransformView -> ScaleTransformViewOptions
SnapshotTransformView -> SnapshotTransformViewOptions
StackTransformView -> StackTransformViewOptions

See the examples in the samples app.
Check here to see used options for each: /PagingLayoutSamples/Modules/Shapes/ShapeCell/

Target view

You may wonder how does it find out the subview in your cell to apply transforms on.
If you check the transformable protocols, you find the target view for each. like ScaleTransformView.scalbleView.

The default value is the first subview of "contentView":

public extension ScaleTransformView where Self: UICollectionViewCell {
    
    /// Default `scalableView` for `UICollectionViewCell` is the first subview of
    /// `contentView` or the content view itself if there is no subview
    var scalableView: UIView {
        contentView.subviews.first ?? contentView
    }
}

If that's not what you want, you can implement it.

Customize Prepared Transformables

Yes, you can customize them or even combine them.
To do that, implement TransformableView.transform function and call the transformable function manually, like this:

extension LayoutTypeCollectionViewCell: ScaleTransformView {
    
    func transform(progress: CGFloat) {
        applyScaleTransform(progress: progress)
        // customize views here, like this:
        titleLabel.alpha = 1 - abs(progress)
        subtitleLabel.alpha = titleLabel.alpha
    }

}

As you see, applyScaleTransform applies the scale transforms and right after that we change the alpha for titleLabel and subtitleLabel.
To find the public function(s) of each protocol check the definition of that.

Other features

Control current page

You can control the current page by the following functions of CollectionViewPagingLayout:

  • func setCurrentPage(_ page: Int, animated: Bool = true)
  • func goToNextPage(animated: Bool = true)
  • func goToPreviousPage(animated: Bool = true)

These are safe wrappers around setting the ContentOffset of UICollectionview.
You can get the current page by a public variable CollectionViewPagingLayout.currentPage.
Listen to the changes via CollectionViewPagingLayout.delegate:

public protocol CollectionViewPagingLayoutDelegate: class {
    func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int)
}

Select Item At

As explained in the Limitations, you can't use collectionview's didSelectItemAt for more than one cell.
But, you can use this instead:

  • Implement TransformableView.selectableView and pass the view that you want to be selectable (by default it's the first subview of UICollectionViewCell.contentView)
  • Call layout.configureTapOnCollectionView() AFTER setting the layout for you collection view.
  • That's it. Now you get similar functionality by using CollectionViewPagingLayout.delegate instead of CollectionView.delegate
  • The method is func collectionViewPagingLayout(_ layout: CollectionViewPagingLayout, didSelectItemAt indexPath: IndexPath)

Limitations

  • Specify the number of visible cells:

You need to specify the number of visible cells.
Since this layout gives you the flexibility to show the next and previous cells,
By default, it loads all of the cells in the collectionview's frame, which means iOS keeps all of them in the memory.
Based on your design, you can specify the number of cells that you need to show.

  • didSelectItemAt:

The way that this library works is by putting all of the cells in the collectionview's frame and applying transforms on the target-view
(StackTransformView.cardView, ScaleTransformView.scalableView and SnapshotTransformView.targetView).

So, you can use func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) but if you have multiple cells on screen only one of them is selectable! (because the others are below it).

You can implement func zPosition(progress: CGFloat) -> Int to specify which cell should be on the top.

This also means you can't handle any gesture for multiple cells.
But, there is a built-in solution to this, see Select Item At

  • It doesn't support RTL layouts

GitHub

https://github.com/amirdew/CollectionViewPagingLayout