Created
March 18, 2022 10:30
-
-
Save shawn-frank/f3abb323beecda28522daff3e21360f7 to your computer and use it in GitHub Desktop.
A small example of using a UIPanGestureRecognizer to swipe up and bring a UICollectionView interaction. This was created as a demo to this StackOverflow question: https://stackoverflow.com/q/71304657/1619193
This file contains 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
// | |
// SwipeCollectionView.swift | |
// TestApp | |
// | |
// Created by Shawn Frank on 01/03/2022. | |
// | |
import UIKit | |
class SwipeCollectionView: UIViewController | |
{ | |
private var collectionView: UICollectionView! | |
private let imageView = UIImageView() | |
private let overlayView = UIView() | |
private let colors: [UIColor] = [.red, | |
.systemBlue, | |
.orange, | |
.systemTeal, | |
.purple, | |
.systemYellow] | |
private var cvBottomAnchor: NSLayoutConstraint? | |
private var isCarouselShowing = false | |
private var previousSwipeLocation: CGPoint? | |
private let cellWidth: CGFloat = 100 | |
private let collectionViewHeight: CGFloat = 185 | |
private let reuseIdentifier = "cell" | |
lazy var button: UIButton = { | |
let button = UIButton(type: .custom) | |
button.translatesAutoresizingMaskIntoConstraints = false | |
button.setTitleColor(.red, for: .normal) | |
button.setTitle("Push", for: .normal) | |
button.backgroundColor = .blue | |
button.contentHorizontalAlignment = .fill | |
button.contentVerticalAlignment = .fill | |
button.contentMode = .scaleAspectFill | |
button.contentEdgeInsets = UIEdgeInsets(top: .leastNormalMagnitude, | |
left: .leastNormalMagnitude, | |
bottom: .leastNormalMagnitude, | |
right: .leastNormalMagnitude) | |
return button | |
}() | |
override func viewDidLoad() | |
{ | |
super.viewDidLoad() | |
configureNavigationBar() | |
configureOverlayView() | |
configureCollectionView() | |
configureButton() | |
} | |
private func configureButton() | |
{ | |
view.addSubview(button) | |
view.addConstraints([ | |
button.centerYAnchor.constraint(equalTo: view.centerYAnchor), | |
button.centerXAnchor.constraint(equalTo: view.centerXAnchor) | |
]) | |
} | |
private func configureNavigationBar() | |
{ | |
title = "Swipe CV" | |
let appearance = UINavigationBarAppearance() | |
// Color of the nav bar background | |
appearance.backgroundColor = .white // primary black for you | |
navigationController?.navigationBar.standardAppearance = appearance | |
navigationController?.navigationBar.scrollEdgeAppearance = appearance | |
} | |
private func configureOverlayView() | |
{ | |
imageView.image = UIImage(named: "dog") | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
imageView.contentMode = .scaleAspectFill | |
overlayView.backgroundColor = .black | |
overlayView.translatesAutoresizingMaskIntoConstraints = false | |
view.addSubview(imageView) | |
view.addSubview(overlayView) | |
// Auto layout pinning the image view and overlay view | |
// to the main container view | |
view.addConstraints([ | |
imageView.leadingAnchor | |
.constraint(equalTo: view.leadingAnchor), | |
imageView.topAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), | |
imageView.trailingAnchor | |
.constraint(equalTo: view.trailingAnchor), | |
imageView.bottomAnchor | |
.constraint(equalTo: view.bottomAnchor), | |
overlayView.leadingAnchor | |
.constraint(equalTo: view.leadingAnchor), | |
overlayView.topAnchor | |
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), | |
overlayView.trailingAnchor | |
.constraint(equalTo: view.trailingAnchor), | |
overlayView.bottomAnchor | |
.constraint(equalTo: view.bottomAnchor) | |
]) | |
// We will observe a swipe gesture to check if it is a swipe | |
// upwards and then react accordingly | |
let swipeGesture = UIPanGestureRecognizer(target: self, | |
action: #selector(didSwipe(_:))) | |
overlayView.addGestureRecognizer(swipeGesture) | |
} | |
private func configureCollectionView() | |
{ | |
collectionView = UICollectionView(frame: .zero, | |
collectionViewLayout: createLayout()) | |
collectionView.backgroundColor = .clear | |
collectionView.register(UICollectionViewCell.self, | |
forCellWithReuseIdentifier: reuseIdentifier) | |
collectionView.translatesAutoresizingMaskIntoConstraints = false | |
collectionView.showsHorizontalScrollIndicator = false | |
// Add some padding for the content on the left | |
collectionView.contentInset = UIEdgeInsets(top: 0, | |
left: 15, | |
bottom: 0, | |
right: 0) | |
collectionView.dataSource = self | |
collectionView.delegate = self | |
overlayView.addSubview(collectionView) | |
// Collection View should start below the screen | |
// We need to persist with this constraint so we can change it later | |
let bottomAnchor = overlayView.safeAreaLayoutGuide.bottomAnchor | |
cvBottomAnchor | |
= collectionView.bottomAnchor.constraint(equalTo: bottomAnchor, | |
constant: collectionViewHeight) | |
// Collection View starts as hidden and will be animated in swipe up | |
collectionView.alpha = 0.0 | |
// Add collection view constraints | |
overlayView.addConstraints([ | |
collectionView.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor), | |
cvBottomAnchor!, | |
collectionView.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor), | |
collectionView.heightAnchor.constraint(equalToConstant: collectionViewHeight) | |
]) | |
} | |
private func createLayout() -> UICollectionViewFlowLayout | |
{ | |
let layout = UICollectionViewFlowLayout() | |
layout.scrollDirection = .horizontal | |
layout.itemSize = CGSize(width: cellWidth, height: collectionViewHeight) | |
layout.minimumInteritemSpacing = 20 | |
return layout | |
} | |
@objc | |
private func didSwipe(_ gesture: UIGestureRecognizer) | |
{ | |
if !isCarouselShowing | |
{ | |
let currentSwipeLocation = gesture.location(in: view) | |
if gesture.state == .began | |
{ | |
// record the swipe location when we start the pan gesture | |
previousSwipeLocation = currentSwipeLocation | |
} | |
// On swipe continuation, verify the swipe is in the upward direction | |
if gesture.state == .changed, | |
let previousSwipeLocation = previousSwipeLocation, | |
currentSwipeLocation.y < previousSwipeLocation.y | |
{ | |
isCarouselShowing = true | |
revealCollectionView() | |
} | |
} | |
} | |
// Animate the y position of the collection view and the alpha | |
private func revealCollectionView() | |
{ | |
// We need to set the top constraint (y position) | |
// to be somewhere above the screen plus some padding | |
cvBottomAnchor?.constant = 0 - 75 | |
UIView.animate(withDuration: 0.25) { [weak self] in | |
// animate change in constraints | |
self?.overlayView.layoutIfNeeded() | |
// reveal the collection view | |
self?.collectionView.alpha = 1.0 | |
} completion: { (finished) in | |
// do something | |
} | |
} | |
} | |
extension SwipeCollectionView: UICollectionViewDataSource | |
{ | |
func collectionView(_ collectionView: UICollectionView, | |
numberOfItemsInSection section: Int) -> Int | |
{ | |
return colors.count | |
} | |
func collectionView(_ collectionView: UICollectionView, | |
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell | |
{ | |
let cell | |
= collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, | |
for: indexPath) | |
cell.layer.cornerRadius = 20 | |
cell.clipsToBounds = true | |
cell.contentView.backgroundColor = colors[indexPath.item] | |
return cell | |
} | |
} | |
extension SwipeCollectionView: UICollectionViewDelegate | |
{ | |
func collectionView(_ collectionView: UICollectionView, | |
didSelectItemAt indexPath: IndexPath) | |
{ | |
overlayView.backgroundColor | |
= colors[indexPath.item].withAlphaComponent(0.5) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment