Last active
May 5, 2020 16:54
-
-
Save roman-wb/68bd8c3cdd82e3b135ea72810a03dd80 to your computer and use it in GitHub Desktop.
iOS collection view paging with custom line spacing & insets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import UIKit | |
| extension UIView { | |
| func debugLayout(_ color: UIColor = .red) { | |
| layer.borderColor = color.cgColor | |
| layer.borderWidth = 1 | |
| } | |
| } | |
| final class CollectionViewCell: UICollectionViewCell { | |
| static let reuseIdentifier = String(describing: self) | |
| lazy var label: UILabel = { | |
| let label = UILabel() | |
| label.translatesAutoresizingMaskIntoConstraints = false | |
| label.textAlignment = .center | |
| addSubview(label) | |
| return label | |
| }() | |
| lazy var imageView: UIImageView = { | |
| let imageView = UIImageView() | |
| imageView.translatesAutoresizingMaskIntoConstraints = false | |
| imageView.contentMode = .scaleAspectFill | |
| imageView.layer.cornerRadius = 10 | |
| imageView.clipsToBounds = true | |
| addSubview(imageView) | |
| return imageView | |
| }() | |
| override init(frame: CGRect) { | |
| super.init(frame: frame) | |
| setupConstraints() | |
| } | |
| func setupConstraints() { | |
| NSLayoutConstraint.activate([ | |
| label.topAnchor.constraint(equalTo: topAnchor), | |
| label.leadingAnchor.constraint(equalTo: leadingAnchor), | |
| label.trailingAnchor.constraint(equalTo: trailingAnchor), | |
| label.bottomAnchor.constraint(equalTo: bottomAnchor), | |
| // imageView.centerXAnchor.constraint(equalTo: centerXAnchor), | |
| // imageView.centerYAnchor.constraint(equalTo: centerYAnchor), | |
| imageView.topAnchor.constraint(equalTo: topAnchor), | |
| imageView.leadingAnchor.constraint(equalTo: leadingAnchor), | |
| imageView.trailingAnchor.constraint(equalTo: trailingAnchor), | |
| imageView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
| ]) | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| } | |
| final class ViewController: | |
| UIViewController, | |
| UICollectionViewDelegate, | |
| UICollectionViewDataSource, | |
| UICollectionViewDelegateFlowLayout | |
| { | |
| let colors: [UIColor] = [.blue, .brown, .cyan, .green, .magenta] | |
| lazy var lineView: UIView = { | |
| let lineView = UIView() | |
| lineView.translatesAutoresizingMaskIntoConstraints = false | |
| view.addSubview(lineView) | |
| return lineView | |
| }() | |
| var heightConstraint1: NSLayoutConstraint! | |
| var heightConstraint2: NSLayoutConstraint! | |
| lazy var flowLayout: UICollectionViewFlowLayout = { | |
| let flowLayout = UICollectionViewFlowLayout() | |
| flowLayout.scrollDirection = .horizontal | |
| flowLayout.minimumLineSpacing = 10 | |
| flowLayout.sectionInset.left = 10 | |
| flowLayout.sectionInset.right = 10 | |
| return flowLayout | |
| }() | |
| lazy var collectionView: UICollectionView = { | |
| let collectionView = UICollectionView(frame: .zero, | |
| collectionViewLayout: flowLayout) | |
| collectionView.translatesAutoresizingMaskIntoConstraints = false | |
| collectionView.register( | |
| CollectionViewCell.self, | |
| forCellWithReuseIdentifier: CollectionViewCell.reuseIdentifier) | |
| collectionView.delegate = self | |
| collectionView.dataSource = self | |
| collectionView.backgroundColor = .white | |
| collectionView.decelerationRate = .fast | |
| collectionView.showsHorizontalScrollIndicator = false | |
| // collectionView.debugLayout() | |
| view.addSubview(collectionView) | |
| return collectionView | |
| }() | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| view.backgroundColor = .white | |
| lineView.backgroundColor = .blue | |
| setupConstraints() | |
| } | |
| func setupConstraints() { | |
| // portreit | |
| heightConstraint1 = collectionView.heightAnchor.constraint( | |
| equalTo: view.widthAnchor, | |
| multiplier: 0.8 * 0.562) | |
| // landscape | |
| heightConstraint2 = collectionView.heightAnchor.constraint( | |
| equalTo: view.heightAnchor, | |
| multiplier: 0.8 * 0.562) | |
| NSLayoutConstraint.activate([ | |
| lineView.topAnchor.constraint( | |
| equalTo: view.safeAreaLayoutGuide.topAnchor), | |
| lineView.bottomAnchor.constraint( | |
| equalTo: view.safeAreaLayoutGuide.bottomAnchor), | |
| lineView.widthAnchor.constraint(equalToConstant: 1), | |
| lineView.centerXAnchor.constraint(equalTo: view.centerXAnchor), | |
| collectionView.topAnchor.constraint( | |
| equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), | |
| collectionView.leadingAnchor.constraint( | |
| equalTo: view.safeAreaLayoutGuide.leadingAnchor), | |
| collectionView.trailingAnchor.constraint( | |
| equalTo: view.safeAreaLayoutGuide.trailingAnchor), | |
| ]) | |
| } | |
| override func viewDidLayoutSubviews() { | |
| super.viewDidLayoutSubviews() | |
| if UIDevice.current.orientation.isLandscape { | |
| heightConstraint1.isActive = false | |
| heightConstraint2.isActive = true | |
| } else { | |
| heightConstraint2.isActive = false | |
| heightConstraint1.isActive = true | |
| } | |
| view.layoutIfNeeded() | |
| let width = collectionView.bounds.width * 0.8 | |
| let height = collectionView.bounds.height | |
| flowLayout.itemSize = CGSize(width: width, height: height) | |
| } | |
| func collectionView(_ collectionView: UICollectionView, | |
| numberOfItemsInSection section: Int) -> Int | |
| { | |
| return colors.count | |
| } | |
| func collectionView(_ collectionView: UICollectionView, | |
| cellForItemAt indexPath: IndexPath) | |
| -> UICollectionViewCell | |
| { | |
| let cell = collectionView.dequeueReusableCell( | |
| withReuseIdentifier: CollectionViewCell.reuseIdentifier, | |
| for: indexPath) as! CollectionViewCell | |
| cell.label.text = "\(indexPath)" | |
| let index = indexPath.row % colors.count | |
| cell.label.backgroundColor = colors[index] | |
| if indexPath.row & 1 == 0 { | |
| cell.imageView.image = UIImage(named: "right-bunner-1")! | |
| } else { | |
| cell.imageView.image = UIImage(named: "right-bunner-2")! | |
| } | |
| // cell.debugLayout(.black) | |
| return cell | |
| } | |
| var lastCellIndex: Int = 0 | |
| var thresholdVeolicy: CGFloat = 0.8 | |
| func currentCellIndex() -> Int { | |
| let contentOffsetX = collectionView.contentOffset.x | |
| let itemWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing | |
| let boundWidth = collectionView.bounds.width | |
| let sectionInset = flowLayout.sectionInset | |
| let paddingSide = (boundWidth - itemWidth) / 2 | |
| var relativeOffsetX = contentOffsetX + paddingSide | |
| relativeOffsetX -= sectionInset.left / 2 + sectionInset.right / 2 | |
| relativeOffsetX /= itemWidth | |
| let index = Int(round(relativeOffsetX)) | |
| return index | |
| } | |
| func scrollViewDidScroll(_ scrollView: UIScrollView) { | |
| print("offsetX", scrollView.contentOffset.x) | |
| print("index", currentCellIndex()) | |
| } | |
| func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { | |
| lastCellIndex = currentCellIndex() | |
| } | |
| // // Example 1 (center or left positions) | |
| var currentVelocity: CGPoint = .zero | |
| func scrollViewWillEndDragging( | |
| _ scrollView: UIScrollView, | |
| withVelocity velocity: CGPoint, | |
| targetContentOffset: UnsafeMutablePointer<CGPoint>) | |
| { | |
| currentVelocity = velocity | |
| } | |
| func scrollViewDidEndDragging(_ scrollView: UIScrollView, | |
| willDecelerate decelerate: Bool) | |
| { | |
| var newCellIndex = currentCellIndex() | |
| if newCellIndex == lastCellIndex { | |
| if currentVelocity.x > thresholdVeolicy { | |
| let numberOfItems = collectionView(collectionView, | |
| numberOfItemsInSection: 0) | |
| newCellIndex = min(newCellIndex + 1, numberOfItems - 1) | |
| } else if currentVelocity.x < -thresholdVeolicy { | |
| newCellIndex = max(newCellIndex - 1, 0) | |
| } | |
| } | |
| // required async | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { [weak self] in | |
| guard let self = self else { return } | |
| // variant 1 (only for center position) | |
| // let path = IndexPath(row: newCellIndex, section: 0) | |
| // self.collectionView.scrollToItem(at: path, | |
| // at: .centeredHorizontally, | |
| // animated: true) | |
| // variant 2 (for left or center positions) | |
| let itemWidth = self.flowLayout.itemSize.width + self.flowLayout.minimumLineSpacing | |
| let boundWidth = self.collectionView.bounds.width | |
| var paddingSide = (boundWidth - itemWidth) / 2 | |
| // paddingSide -= self.flowLayout.minimumLineSpacing / 2 | |
| var x = CGFloat(newCellIndex) | |
| x *= self.flowLayout.itemSize.width + self.flowLayout.minimumLineSpacing | |
| // default center position, comment for left position | |
| // x -= paddingSide | |
| // calculate for less or greater bounds | |
| var maxX = self.collectionView.contentSize.width - self.flowLayout.itemSize.width | |
| maxX -= self.collectionView.bounds.width - self.flowLayout.itemSize.width | |
| let safeX = max(0, min(x, maxX)) | |
| let offset = CGPoint(x: safeX, y: 0) | |
| self.collectionView.setContentOffset(offset, animated: true) | |
| } | |
| } | |
| // // Example 2 (center) | |
| // func scrollViewWillEndDragging( | |
| // _ scrollView: UIScrollView, | |
| // withVelocity velocity: CGPoint, | |
| // targetContentOffset: UnsafeMutablePointer<CGPoint>) | |
| // { | |
| // // Stop scroll propagation | |
| // targetContentOffset.pointee = scrollView.contentOffset | |
| // | |
| // var newCellIndex = 0 | |
| // | |
| // if abs(velocity.x) > thresholdVeolicy { | |
| // newCellIndex = lastCellIndex | |
| // | |
| // if velocity.x > thresholdVeolicy { | |
| // let numberOfItems = collectionView(collectionView, | |
| // numberOfItemsInSection: 0) | |
| // newCellIndex = min(newCellIndex + 1, numberOfItems - 1) | |
| // } else if velocity.x < -thresholdVeolicy { | |
| // newCellIndex = max(newCellIndex - 1, 0) | |
| // } | |
| // } else { | |
| // newCellIndex = currentCellIndex() | |
| // } | |
| // | |
| // let path = IndexPath(row: newCellIndex, section: 0) | |
| // collectionView.scrollToItem(at: path, | |
| // at: .centeredHorizontally, | |
| // animated: true) | |
| // } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment