How to create the rubberband effect? - ios

What is a full example of how to implement the rubber banding effect? How can I implement this?
I have tried the following: However, I have been unsuccessful in finding how to implement this.
Currently I have created a card view which can be pulled up to a certain point, however currently when you reach the max there is a sudden halt, which I would like to change to a rubber band effect.
Here is the code I have been using to try and add this:
enum SheetLevel{
case top, bottom, middle
}
protocol BottomSheetDelegate {
func updateBottomSheet(frame: CGRect)
}
class BottomSheetViewController: UIViewController{
#IBOutlet var panView: UIView!
#IBOutlet weak var tableView: UICollectionView!
// #IBOutlet weak var collectionView: UICollectionView! //header view
var lastY: CGFloat = 0
var pan: UIPanGestureRecognizer!
var bottomSheetDelegate: BottomSheetDelegate?
var parentView: UIView!
var initalFrame: CGRect!
var topY: CGFloat = 80 //change this in viewWillAppear for top position
var middleY: CGFloat = 400 //change this in viewWillAppear to decide if animate to top or bottom
var bottomY: CGFloat = 600 //no need to change this
let bottomOffset: CGFloat = 64 //sheet height on bottom position
var lastLevel: SheetLevel = .middle //choose inital position of the sheet
var disableTableScroll = false
//hack panOffset To prevent jump when goes from top to down
var panOffset: CGFloat = 0
var applyPanOffset = false
//tableview variables
var listItems: [Any] = []
var headerItems: [Any] = []
override func viewDidLoad() {
super.viewDidLoad()
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
pan.delegate = self
self.panView.addGestureRecognizer(pan)
self.tableView.panGestureRecognizer.addTarget(self, action: #selector(handlePan(_:)))
//Bug fix #5. see https://github.com/OfTheWolf/UBottomSheet/issues/5
//Tableview didselect works on second try sometimes so i use here a tap gesture recognizer instead of didselect method and find the table row tapped in the handleTap(_:) method
let tap = UITapGestureRecognizer.init(target: self, action: #selector(handleTap(_:)))
tap.delegate = self
tableView.addGestureRecognizer(tap)
//Bug fix #5 end
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.initalFrame = UIScreen.main.bounds
self.topY = round(initalFrame.height * 0.5)
self.middleY = initalFrame.height * 0.6
self.bottomY = initalFrame.height - bottomOffset
self.lastY = self.middleY
bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == tableView else {return}
if (self.parentView.frame.minY > topY){
self.tableView.contentOffset.y = 0
}
}
//this stops unintended tableview scrolling while animating to top
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView == tableView else {return}
if disableTableScroll{
targetContentOffset.pointee = scrollView.contentOffset
disableTableScroll = false
}
}
//Bug fix #5. see https://github.com/OfTheWolf/UBottomSheet/issues/5
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
let p = recognizer.location(in: self.tableView)
//Commented below to prevenet error.. *** apr 2 guillermo
// let index = tableView.indexPathForRow(at: p)
// //WARNING: calling selectRow doesn't trigger tableView didselect delegate. So handle selected row here.
// //You can remove this line if you dont want to force select the cell
// tableView.selectRow(at: index, animated: false, scrollPosition: .none)
}//Bug fix #5 end
#objc func handlePan(_ recognizer: UIPanGestureRecognizer) {
// var x = topY
// var c = 0.55
// var d = view.frame.height
// var formula = (1.0 - (1.0 / ((x * c / d) + 1.0))) * d
let dy = recognizer.translation(in: self.parentView).y
print(recognizer.translation(in: self.parentView).y, " This si dy")
switch recognizer.state {
case .began:
applyPanOffset = (self.tableView.contentOffset.y > 0)
case .changed:
print(".changed here")
if self.tableView.contentOffset.y > 0{
panOffset = dy
return
}
if self.tableView.contentOffset.y <= 0 {
if !applyPanOffset{panOffset = 0}
let maxY = max(topY, lastY + dy - panOffset)
let y = min(bottomY, maxY)
print(y, ".inside if let thindfahfvdsgjafjsda8", maxY)
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: y)
bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: y))
}
if self.parentView.frame.minY > topY{
print(self.tableView.contentOffset.y, " Thsi is taht y vakue thing")
self.tableView.contentOffset.y = 0
}
case .failed, .ended, .cancelled:
panOffset = 0
print(".failed and enededh rhere")
//bug fix #6. see https://github.com/OfTheWolf/UBottomSheet/issues/6
if (self.tableView.contentOffset.y > 0){
return
}//bug fix #6 end
self.panView.isUserInteractionEnabled = false
self.disableTableScroll = self.lastLevel != .top
self.lastY = self.parentView.frame.minY
self.lastLevel = self.nextLevel(recognizer: recognizer)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.9, options: .curveEaseOut, animations: {
print("Animation!!!!!")
switch self.lastLevel {
case .top:
print("in this thanaagaggagagagagagagalanfg")
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.topY)
// self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.topY))
// self.tableView.contentInset.bottom = 50
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
case .middle:
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.middleY)
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.middleY))
case .bottom:
// self.panView.frame = self.initalFrame.offsetBy(dx: 0, dy: self.bottomY)
self.bottomSheetDelegate?.updateBottomSheet(frame: self.initalFrame.offsetBy(dx: 0, dy: self.bottomY))
}
}) { (_) in
print("Someghtifgnshdfgbk")
self.panView.isUserInteractionEnabled = true
self.lastY = self.parentView.frame.minY
}
default:
break
}
}
func nextLevel(recognizer: UIPanGestureRecognizer) -> SheetLevel{
let y = self.lastY
let velY = recognizer.velocity(in: self.view).y
if velY < -200{
return y > middleY ? .middle : .top
}else if velY > 200{
return y < (middleY + 1) ? .middle : .bottom
}else{
if y > middleY {
return (y - middleY) < (bottomY - y) ? .middle : .bottom
}else{
return (y - topY) < (middleY - y) ? .top : .middle
}
}
}
}
extension BottomSheetViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SimpleTableCell", for: indexPath) as! SimpleTableCell
let model = SimpleTableCellViewModel(image: nil, title: "Title \(indexPath.row)", subtitle: "Subtitle \(indexPath.row)")
cell.configure(model: model)
return cell
}
}
extension BottomSheetViewController: UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

What you need to do is exponentially decrease the length that user is adding by dragging. There are different formula for this such as using sqrt, log10, and etc.
Here's what I've done:
And here's the code:
import UIKit
class ViewController: UIViewController {
lazy private var box : UIView = {
let view = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height-300, width: UIScreen.main.bounds.width, height: 300))
view.backgroundColor = .red
return view
}()
private var panGesture : UIPanGestureRecognizer!
private var boxOriginY : CGFloat = 300.0
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(box)
panGesture = UIPanGestureRecognizer(target: self, action: #selector(pullUp(with:)))
box.addGestureRecognizer(panGesture)
}
#objc private func pullUp(with pan: UIPanGestureRecognizer) {
let yTranslation = pan.translation(in: self.view).y
if pan.state == .changed {
let distance = CGFloat(sqrt( Double(-yTranslation) ) * 10) // times 10 to make it smoother
let newHeight : CGFloat = boxOriginY + distance
box.frame = CGRect(x: 0,
y: UIScreen.main.bounds.height-(newHeight),
width: UIScreen.main.bounds.width,
height: newHeight)
}
if pan.state == .ended {
if pan.state == UIPanGestureRecognizer.State.ended {
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.box.frame = CGRect(x: 0, y: UIScreen.main.bounds.height-300, width: UIScreen.main.bounds.width, height: 300)
})
}
}
}
}

Related

how to make collapsible/expandable views in swift

I created a collapsible/expandable form on android. please see GIF below
https://giphy.com/gifs/zVvcKtgT9QTaa1O29O
I'm trying to create something similar on ios, so far, i've already created the bottom sheet as seen below
Looking at this GIF https://gfycat.com/dismalbronzeblowfish, you'd notice i'm able to expand and collapse the views, but there's a big gap where the view used to be, the expected behavior is that the space collapses also with an animation
Below is the code for the bottom sheet
class BottomSheetViewController: UIViewController {
// holdView can be UIImageView instead
#IBOutlet weak var holdView: UIView!
#IBOutlet weak var left: UIButton!
#IBOutlet weak var right: UIButton!
#IBOutlet weak var pickupView: UIView!
#IBOutlet weak var deliveryView: UIView!
#IBOutlet weak var deliverydetailsView: UIView!
#IBOutlet weak var pickupDetailsVIew: UIControl!
let fullView: CGFloat = 100
var partialView: CGFloat {
return UIScreen.main.bounds.height - 300
}
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
let pickupTapGesture = UITapGestureRecognizer(target: self, action: #selector(pickupButton))
let deliveryTapGesture = UITapGestureRecognizer(target: self, action: #selector(deliveryButton))
pickupView.addGestureRecognizer(pickupTapGesture)
deliveryView.addGestureRecognizer(deliveryTapGesture)
pickupView.setBorder(radius: 5, color: .black)
deliveryView.setBorder(radius: 5, color: .black)
roundViews()
deliverydetailsView.isHidden = true
pickupDetailsVIew.isHidden = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.6, animations: { [weak self] in
let frame = self?.view.frame
let yComponent = self?.partialView
self?.view.frame = CGRect(x: 0, y: yComponent!, width: frame!.width, height: frame!.height)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func rightButton(_ sender: AnyObject) {
print("clicked")
}
#objc func pickupButton(_ sender: UITapGestureRecognizer) {
print("tap")
if pickupDetailsVIew.isHidden {
expand(pickupDetailsVIew)
collapse(deliverydetailsView)
} else {
collapse(pickupDetailsVIew)
}
}
#objc func deliveryButton(_ sender: UITapGestureRecognizer) {
print("tap")
if deliverydetailsView.isHidden {
expand(deliverydetailsView)
collapse(pickupDetailsVIew)
} else {
collapse(deliverydetailsView)
// if deliveryView.isHidden && pickupDetailsVIew.isHidden {
//
// }
}
}
func expand(_ view: UIView) {
view.isHidden = false
}
func collapse(_ view: UIView) {
view.isHidden = true
}
// #IBAction func close(_ sender: AnyObject) {
// UIView.animate(withDuration: 0.3, animations: {
// let frame = self.view.frame
// self.view.frame = CGRect(x: 0, y: self.partialView, width: frame.width, height: frame.height)
// })
// }
#objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
let y = self.view.frame.minY
if ( y + translation.y >= fullView) && (y + translation.y <= partialView ) {
self.view.frame = CGRect(x: 0, y: y + translation.y, width: view.frame.width, height: view.frame.height)
recognizer.setTranslation(CGPoint.zero, in: self.view)
}
if recognizer.state == .ended {
var duration = velocity.y < 0 ? Double((y - fullView) / -velocity.y) : Double((partialView - y) / velocity.y )
duration = duration > 1.3 ? 1 : duration
UIView.animate(withDuration: duration, delay: 0.0, options: [.allowUserInteraction], animations: {
if velocity.y >= 0 {
self.view.frame = CGRect(x: 0, y: self.partialView, width: self.view.frame.width, height: self.view.frame.height)
} else {
self.view.frame = CGRect(x: 0, y: self.fullView, width: self.view.frame.width, height: self.view.frame.height)
}
}, completion: nil)
}
}
func roundViews() {
view.layer.cornerRadius = 5
holdView.layer.cornerRadius = 3
// left.layer.cornerRadius = 10
// right.layer.cornerRadius = 10
// left.layer.borderColor = UIColor(red: 0, green: 148/225, blue: 247.0/255.0, alpha: 1).cgColor
// left.layer.borderWidth = 1
view.clipsToBounds = true
}
func prepareBackgroundView(){
// let blurEffect = UIBlurEffect.init(style: .dark)
// let visualEffect = UIVisualEffectView.init(effect: blurEffect)
// let bluredView = UIVisualEffectView.init(effect: blurEffect)
// bluredView.contentView.addSubview(visualEffect)
//
// visualEffect.frame = UIScreen.main.bounds
// bluredView.frame = UIScreen.main.bounds
//
// view.insertSubview(bluredView, at: 0)
}
}
I need some help/pointers in the right direction from anyone who has done this before, or who knows how to do this
Thank you

Reverse animation when button is pressed for second time in Table View

I know this has been asked before but did could not figure-out how to achieve this in the shortest possible way in my case.
When I click on "Add" button (see GIF below), the animation function animates "imageView" (in this case the galaxy image) to fly to cart i.e. "notificationButton". Also "Add" button changes to "Remove" and button color changes from black to red (see GIF below). Fine with that.
Now, when I click the button for the second time i.e deselect it i.e. make it to default state, everything reverses, but the image still flies to the cart !
Now, I want to reverse the animation flying imageView animation to its original position when I push the button back to its default position second time, and again original flying animation, if the button is pushed again as many times as I want.
Though I have added complete ProductViewController code here but you skip everything and look at the last extension ProductViewController
I know this most likely has two steps -
i) Identifying that "buttonHandlerAddToCart" button is pressed second time i.e. from selected/isselected step to default step.
ii) Reversing the animation function "func animation" in ProductViewController.
How to go about it?
Relevant code:
SSBadgeButton:-
import UIKit
class SSBadgeButton: UIButton {
var badgeLabel = UILabel()
var badge: String? {
didSet {
addBadgeToButon(badge: badge)
}
}
public var badgeBackgroundColor = UIColor.red {
didSet {
badgeLabel.backgroundColor = badgeBackgroundColor
}
}
public var badgeTextColor = UIColor.white {
didSet {
badgeLabel.textColor = badgeTextColor
}
}
public var badgeFont = UIFont.systemFont(ofSize: 12.0) {
didSet {
badgeLabel.font = badgeFont
}
}
public var badgeEdgeInsets: UIEdgeInsets? {
didSet {
addBadgeToButon(badge: badge)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addBadgeToButon(badge: nil)
}
func addBadgeToButon(badge: String?) {
badgeLabel.text = badge
badgeLabel.textColor = badgeTextColor
badgeLabel.backgroundColor = badgeBackgroundColor
badgeLabel.font = badgeFont
badgeLabel.sizeToFit()
badgeLabel.textAlignment = .center
let badgeSize = badgeLabel.frame.size
let height = max(18, Double(badgeSize.height) + 5.0)
let width = max(height, Double(badgeSize.width) + 10.0)
var vertical: Double?, horizontal: Double?
if let badgeInset = self.badgeEdgeInsets {
vertical = Double(badgeInset.top) - Double(badgeInset.bottom)
horizontal = Double(badgeInset.left) - Double(badgeInset.right)
let x = (Double(bounds.size.width) - 10 + horizontal!)
let y = -(Double(badgeSize.height) / 2) - 10 + vertical!
badgeLabel.frame = CGRect(x: x, y: y, width: width, height: height)
} else {
let x = self.frame.width - CGFloat((width / 2.0))
let y = CGFloat(-(height / 2.0))
badgeLabel.frame = CGRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height))
}
badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2
badgeLabel.layer.masksToBounds = true
addSubview(badgeLabel)
badgeLabel.isHidden = badge != nil ? false : true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addBadgeToButon(badge: nil)
fatalError("init(coder:) has not been implemented")
}
}
The ProductViewController code:
import UIKit
class ProductViewController: UIViewController, UITableViewDataSource,
UITableViewDelegate {
let notificationButton = SSBadgeButton()
let rightbarbuttonimage = UIImage(named:"ic_cart")
fileprivate var cart = Cart()
let scrollView = UIScrollView()
let sections = ["Section A", "Section B","Section C", "Section D","Section E","Section F","Section G","Section H", "Section I","Section J","Section K","Section L"]
let rowspersection = [2,3,1,2,2,3,3,1,4,2,1,2]
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
self.tableView.backgroundColor = UIColor.gray
//Add and setup scroll view
self.tableView.addSubview(self.scrollView)
self.scrollView.translatesAutoresizingMaskIntoConstraints = false;
//Constrain scroll view
self.scrollView.leadingAnchor.constraint(equalTo: self.tableView.leadingAnchor, constant: 20).isActive = true;
self.scrollView.topAnchor.constraint(equalTo: self.tableView.topAnchor, constant: 20).isActive = true;
self.scrollView.trailingAnchor.constraint(equalTo: self.tableView.trailingAnchor, constant: -20).isActive = true;
self.scrollView.bottomAnchor.constraint(equalTo: self.tableView.bottomAnchor, constant: -20).isActive = true;
// customising rightBarButtonItems as notificationbutton
notificationButton.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
notificationButton.setImage(UIImage(named: "ic_cart")?.withRenderingMode(.alwaysTemplate), for: .normal)
notificationButton.badgeEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: notificationButton)
//following register is needed because I have rightbarbuttonitem customised as uibutton i.e. notificationbutton
notificationButton.addTarget(self, action: #selector(self.registerTapped(_:)), for: .touchUpInside)
}
#objc func registerTapped(_ sender: UIButton) {
self.performSegue(withIdentifier: "showCart", sender: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Workaround to avoid the fadout the right bar button item
self.navigationItem.rightBarButtonItem?.isEnabled = false
self.navigationItem.rightBarButtonItem?.isEnabled = true
//Update cart if some items quantity is equal to 0 and reload the product table and right button bar item
cart.updateCart()
//self.navigationItem.rightBarButtonItem?.title = "Checkout (\(cart.items.count))"
notificationButton.badge = String(cart.items.count)// making badge equal to no.ofitems in cart
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// this segue to transfer data
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showCart" {
if let cartViewController = segue.destination as? CartViewController {
cartViewController.cart = self.cart
}
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return productMap.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return productMap[section]?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let product = productMap[indexPath.section]![indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductTableViewCell") as! ProductTableViewCell
cell.imageView?.image = product.imagename
cell.delegate = self as CartDelegate
cell.setButton(state: self.cart.contains(product: product))
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch(section) {
case 0: return "Section A"
case 1: return "Section B"
case 2: return "Section C"
case 3: return "Section D"
case 4: return "Section E"
case 5: return "Section F"
case 6: return "Section G"
case 7: return "Section H"
case 8: return "Section I"
case 9: return "Section J"
case 10: return "Section K"
case 11: return "Section L"
default: return ""
}
}
}
extension ProductViewController: CartDelegate {
// MARK: - CartDelegate
func updateCart(cell: ProductTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
let product = productMap[indexPath.section]![indexPath.row]
//Update Cart with product
cart.updateCart(with: product)
// self.navigationItem.rightBarButtonItem?.title = "Checkout (\(cart.items.count))"
notificationButton.badge = String(cart.items.count) // making badge equal to noofitems in cart
}
}
***// Most relevant code begins here -***
extension ProductViewController {
#IBAction func buttonHandlerAddToCart(_ sender: UIButton) {
let buttonPosition : CGPoint = sender.convert(sender.bounds.origin, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: buttonPosition)!
let cell = tableView.cellForRow(at: indexPath) as! ProductTableViewCell
let imageViewPosition : CGPoint = cell.imageView!.convert(cell.imageView!.bounds.origin, to: self.view)
let imgViewTemp = UIImageView(frame: CGRect(x: imageViewPosition.x, y: imageViewPosition.y, width: cell.imageView!.frame.size.width, height: cell.imageView!.frame.size.height))
imgViewTemp.image = cell.imageView!.image
animation(tempView: imgViewTemp)
}
func animation(tempView : UIView) {
self.view.addSubview(tempView)
UIView.animate(
withDuration: 1.0,
animations: {
tempView.animationZoom(scaleX: 1.5, y: 1.5)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
tempView.animationZoom(scaleX: 0.2, y: 0.2)
tempView.animationRoted(angle: CGFloat(Double.pi))
tempView.frame.origin.x = self.notificationButton.frame.origin.x
tempView.frame.origin.y = self.notificationButton.frame.origin.y
}, completion: { _ in
tempView.removeFromSuperview()
UIView.animate(withDuration: 1.0, animations: {
self.notificationButton.animationZoom(scaleX: 1.4, y: 1.4)
}, completion: {_ in
self.notificationButton.animationZoom(scaleX: 1.0, y: 1.0)
})
})
}
)
}
}
extension UIView{
func animationZoom(scaleX: CGFloat, y: CGFloat) {
self.transform = CGAffineTransform(scaleX: scaleX, y: y)
}
func animationRoted(angle : CGFloat) {
self.transform = self.transform.rotated(by: angle)
}
}
I have also included ProductTableViewCell code, just in case:
import UIKit
protocol CartDelegate {
func updateCart(cell: ProductTableViewCell)
}
class ProductTableViewCell: UITableViewCell {
weak var myParent:ProductViewController?
#IBOutlet weak var imagename: UIImageView!
#IBOutlet weak var addToCartButton: UIButton!
var delegate: CartDelegate?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
addToCartButton.layer.cornerRadius = 5
addToCartButton.clipsToBounds = true
}
func setButton(state: Bool) {
addToCartButton.isUserInteractionEnabled = true
addToCartButton.isSelected = state
addToCartButton.backgroundColor = (!addToCartButton.isSelected) ? .blue : .red
}
#IBAction func addToCart(_ sender: Any) {
setButton(state: !addToCartButton.isSelected)
self.delegate?.updateCart(cell: self)
}
}
Edit: on #aheze's request :
struct Product: Equatable {
let imagename: UIImage
}
var productMap = [
0: [ Product(imagename:#imageLiteral(resourceName: "blue")), Product( imagename:#imageLiteral(resourceName: "CakeImage")) ]
1: [ Product(imagename:#imageLiteral(resourceName: "vectorlogo")), Product(imagename:#imageLiteral(resourceName: "PeasImge")), Product(imagename:#imageLiteral(resourceName: "castle"))],
2: [ Product( imagename:#imageLiteral(resourceName: "scoobydoo")),Product(imagename:#imageLiteral(resourceName: "ufo"))] ,
3: [ Product( imagename:#imageLiteral(resourceName: "wolfsky")),Product( imagename:#imageLiteral(resourceName: "universe")) ],
4: [ Product(imagename:#imageLiteral(resourceName: "werewolf")),Product( imagename:#imageLiteral(resourceName: "galaxy")) ]
]
Edit 2: class Cart, on #aheze's request:
import Foundation
class Cart {
var items : [CartItem] = []
}
extension Cart {
var totalQuantity : Int {
get { return items.reduce(0) { value, item in
value + item.quantity
}
}
}
func updateCart(with product: Product) {
if !self.contains(product: product) {
self.add(product: product)
} else {
self.remove(product: product)
}
}
func updateCart() {
for item in self.items {
if item.quantity == 0 {
updateCart(with: item.product)
}
}
}
func add(product: Product) {
let item = items.filter { $0.product == product }
if item.first != nil {
item.first!.quantity += 1
} else {
items.append(CartItem(product: product))
}
}
func remove(product: Product) {
guard let index = items.firstIndex(where: { $0.product == product }) else { return}
items.remove(at: index)
}
func contains(product: Product) -> Bool {
let item = items.filter { $0.product == product }
return item.first != nil
}
}
Further information you need, feel free...
Does this work? (the gif was too big)
https://imgur.com/a/jrcwEWv
I made separate functions for adding and removing from the cart.
extension ProductViewController: CartDelegate {
// MARK: - CartDelegate
func updateCart(cell: ProductTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
let product = productMap[indexPath.section]![indexPath.row]
/// `var selectedIndexPaths = [IndexPath]()` defined inside `ProductViewController`, to keep track of the selected products
if selectedIndexPaths.contains(indexPath) {
if let index = selectedIndexPaths.firstIndex(of: indexPath) {
selectedIndexPaths.remove(at: index)
removeProductFromCart(indexPath: indexPath)
}
} else {
selectedIndexPaths.append(indexPath)
addProductToCart(indexPath: indexPath)
}
// addProductToCart(indexPath: indexPath)
/// **I commented this out because I don't have the code for `Cart`**
//Update Cart with product
// cart.updateCart(with: product)
// self.navigationItem.rightBarButtonItem?.title = "Checkout (\(cart.items.count))"
// notificationButton.badge = String(cart.items.count) // making badge equal to noofitems in cart
}
}
func addProductToCart(indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ProductTableViewCell {
if let imageView = cell.imagename {
let initialImageViewFrame = imageView.convert(imageView.frame, to: self.view)
let targetImageViewFrame = self.notificationButton.frame
let imgViewTemp = UIImageView(frame: initialImageViewFrame)
imgViewTemp.clipsToBounds = true
imgViewTemp.contentMode = .scaleAspectFill
imgViewTemp.image = imageView.image
self.view.addSubview(imgViewTemp)
UIView.animate(withDuration: 1.0, animations: {
imgViewTemp.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}) { _ in
UIView.animate(withDuration: 0.5, animations: {
imgViewTemp.transform = CGAffineTransform(scaleX: 0.2, y: 0.2).rotated(by: CGFloat(Double.pi))
imgViewTemp.frame = targetImageViewFrame
}) { _ in
imgViewTemp.removeFromSuperview()
UIView.animate(withDuration: 1.0, animations: {
self.notificationButton.transform = CGAffineTransform(scaleX: 1.4, y: 1.4)
}, completion: {_ in
self.notificationButton.transform = CGAffineTransform.identity
})
}
}
}
}
}
func removeProductFromCart(indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ProductTableViewCell {
if let imageView = cell.imagename {
let initialImageViewFrame = self.notificationButton.frame
let targetImageViewFrame = imageView.convert(imageView.frame, to: self.view)
let imgViewTemp = UIImageView(frame: initialImageViewFrame)
imgViewTemp.clipsToBounds = true
imgViewTemp.contentMode = .scaleAspectFill
imgViewTemp.image = imageView.image
self.view.addSubview(imgViewTemp)
var initialTransform = CGAffineTransform.identity
initialTransform = initialTransform.scaledBy(x: 0.2, y: 0.2)
initialTransform = initialTransform.rotated(by: CGFloat(Double.pi))
UIView.animate(withDuration: 0.5, animations: {
self.notificationButton.animationZoom(scaleX: 1.4, y: 1.4)
imgViewTemp.transform = initialTransform
}) { _ in
UIView.animate(withDuration: 1, animations: {
self.notificationButton.animationZoom(scaleX: 1, y: 1)
imgViewTemp.transform = CGAffineTransform.identity
imgViewTemp.frame = targetImageViewFrame
}) { _ in
imgViewTemp.removeFromSuperview()
}
}
}
}
}
Some things that you should fix:
Instead of using imagename (the image view that you added to your table view cell), you used cell.imageView! which is the built-in image view that all cells have. Don't use this.
Inside ProductTableViewCell, you should make a separate property for keeping track of selected/not selected state instead of using UIButton's isSelected. This way, you won't run into unwanted behavior when changing the button color (currently, a red rectangle will appear behind the button's text for a moment)
If you're combining transforms, you should do this:
var initialTransform = CGAffineTransform.identity
initialTransform = initialTransform.scaledBy(x: 0.2, y: 0.2)
initialTransform = initialTransform.rotated(by: CGFloat(Double.pi))
tempView.transform = initialTransform
instead of:
tempView.animationZoom(scaleX: 0.2, y: 0.2)
tempView.animationRoted(angle: CGFloat(Double.pi))
Here's the full project (added some more comments too).

Rotate collectionView in circle following user direction

I am trying create collectionView with circuler layout and I want the collectionView to rotate in circle as the user swipe his finger on screen round in whatever direction. I found the circle layout for collectionView here is what I have done so far
to rotate this collectionView I have wrote this code
add gesture to collectionView
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
here is the gestureReader and animation methods
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
print(distance)
if start.x > end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
self.collectionView.transform = CGAffineTransform.identity
self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
}) { (true) in
//
}
}
First the animation is not working properly and second when collectionView gets rotated this is what I get
Question1 : What else do I need to add to make this animation smooth and follow user's finger?
Question2 : I want the collectionViewcells to stay as before animation, how can I achieve this, please help
Thanks in advance
I show you an example here. The decor View S1View is a subclass of UICollectionViewCell with the identifier "background".
The code is not hard to understand but tedious to put together. How to control animator is another story.
class TestCollectionViewLayout: UICollectionViewLayout {
lazy var dataSource : UICollectionViewDataSource? = {
self.collectionView?.dataSource
}()
var layouts : [IndexPath: UICollectionViewLayoutAttributes?] = [:]
var itemNumber : Int {
return dataSource!.collectionView(collectionView!, numberOfItemsInSection: 0)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?{
var itemArray = (0..<itemNumber).map{ self.layoutAttributesForItem(at: IndexPath.init(row: $0, section: 0))!}
itemArray.append(self.layoutAttributesForDecorationView(ofKind:"background"
, at: IndexPath.init(row: 0, section: 0)))
return itemArray
}
override var collectionViewContentSize: CGSize { get{
return self.collectionView?.frame.size ?? CGSize.zero
}
}
lazy var dynamicAnimator = {UIDynamicAnimator(collectionViewLayout: self)}()
private func updateCurrentLayoutAttributesForItem(at indexPath: IndexPath, current: UICollectionViewLayoutAttributes?) -> UICollectionViewLayoutAttributes?{
return current
}
private func initLayoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
let layoutAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let center = (collectionView?.center)!
let angle = (CGFloat(indexPath.row) / CGFloat(itemNumber) * CGFloat.pi * 2)
layoutAttributes.center = CGPoint.init(x: center.x + cos(angle) * CGFloat(radius) , y: center.y + sin(angle) * CGFloat(radius) )
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: 100, height: 100 )
if let decorator = self.decorator {
let itemBehavior =
UIAttachmentBehavior.pinAttachment(with: layoutAttributes, attachedTo: decorator, attachmentAnchor: layoutAttributes.center)
dynamicAnimator.addBehavior(itemBehavior)
layouts[indexPath] = layoutAttributes
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
guard let currentLayout = layouts[indexPath] else {
return initLayoutAttributesForItem(at:indexPath)}
return currentLayout
}
private let radius = 200
private var decorator: UICollectionViewLayoutAttributes?
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes{
guard let decorator = self.decorator else {
let layoutAttributes = UICollectionViewLayoutAttributes.init(forDecorationViewOfKind: elementKind, with: indexPath)
layoutAttributes.center = (self.collectionView?.center)!
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: radius, height: radius)
self.decorator = layoutAttributes
return layoutAttributes
}
return decorator
}
lazy var s: UIDynamicItemBehavior = {
let decorator = self.decorator!
let s = UIDynamicItemBehavior.init(items: [decorator])
s.angularResistance = 1
dynamicAnimator.addBehavior(s)
return s
}()
func rotate(_ speed: CGFloat){
guard let decorator = self.decorator else {return}
s.addAngularVelocity(speed, for: decorator)
}
}
class TestCollectionViewController: UICollectionViewController {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
else if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
if start.x < end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
(collectionView.collectionViewLayout as? TestCollectionViewLayout).map{
$0.rotate(angle / 100)
}
// UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
// self.collectionView.transform = CGAffineTransform.identity
// self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
// }) { (true) in
// //
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (Timer) in
// self.rotate()
// }
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = TestCollectionViewLayout()
collectionView.collectionViewLayout.register(UINib.init(nibName: "S1View", bundle: nil) , forDecorationViewOfKind: "background")
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
}
var data: [Int] = [1,2,3,4,5,6,7]
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}
Maybe this tutorial will help: https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
Your first problem is that you are rotating the whole collection view. Think of it like you are putting those circles on a piece of paper and then rotating that piece of paper. You don't want to rotate the whole collection view. You might not want to rotate the circles around a point because then the rotation affects the image and text in the circle. You just want to change the circle's position in a circular movement.
If the UICollectionView isn't working, you could ditch it and use regular UIViews and position them in a circular pattern (These functions should help: https://gist.github.com/akhilcb/8d03f1f88f87e996aec24748bdf0ce78). Once you have the views laid out in a circle then you just need to update the angle for each view as the user drags their finger. Store the previous angle on the view and add to it whatever you want when the user drags their finger. Little bit of trial and error and it shouldn't be too bad.
Update
The main reason to use collection views is if you have a lot of items and you need to reuse views like a list. If you don't need to reuse views then using a UICollectionView can be pain to understand, customize and change things. Here is a simple example of using regular views that rotate around a circle using a UIPanGestureRecognizer input.
Example:
import UIKit
class ViewController: UIViewController {
var rotatingViews = [RotatingView]()
let numberOfViews = 8
var circle = Circle(center: CGPoint(x: 200, y: 200), radius: 100)
var prevLocation = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...numberOfViews {
let angleBetweenViews = (2 * Double.pi) / Double(numberOfViews)
let viewOnCircle = RotatingView(circle: circle, angle: CGFloat(Double(i) * angleBetweenViews))
rotatingViews.append(viewOnCircle)
view.addSubview(viewOnCircle)
}
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
view.addGestureRecognizer(panGesture)
}
#objc func didPan(panGesture: UIPanGestureRecognizer){
switch panGesture.state {
case .began:
prevLocation = panGesture.location(in: view)
case .changed, .ended:
let nextLocation = panGesture.location(in: view)
let angle = circle.angleBetween(firstPoint: prevLocation, secondPoint: nextLocation)
rotatingViews.forEach({ $0.updatePosition(angle: angle)})
prevLocation = nextLocation
default: break
}
}
}
struct Circle {
let center: CGPoint
let radius: CGFloat
func pointOnCircle(angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
func angleBetween(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let firstAngle = atan2(firstPoint.y - center.y, firstPoint.x - center.x)
let secondAnlge = atan2(secondPoint.y - center.y, secondPoint.x - center.x)
let angleDiff = (firstAngle - secondAnlge) * -1
return angleDiff
}
}
class RotatingView: UIView {
var currentAngle: CGFloat
let circle: Circle
init(circle: Circle, angle: CGFloat) {
self.currentAngle = angle
self.circle = circle
super.init(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
center = circle.pointOnCircle(angle: currentAngle)
backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePosition(angle: CGFloat) {
currentAngle += angle
center = circle.pointOnCircle(angle: currentAngle)
}
}
Circle is a struct that just holds the center of all the views, how far apart you want them (radius), and helper functions for calculating the angles found in the GitHub link above.
RotatingViews are the views that rotate around the middle.

Collection view cells overlap animation in Bottomsheet

I need to apply animation like the bottom sheet where Collection View cells overlap each other when closed, and wide opened when bottom sheet is opened. I have achieved the bottom sheet with Collection View with pagination, but need to apply animation that shows only current cell and other cells behind it when bottom sheet is closed.
Here is my Bottom Sheet Class, and some images as an example what I need to achieve:
class ScrollableBottomSheetViewController: UIViewController {
#IBOutlet var pageControl: UIPageControl!
#IBOutlet var collection: UICollectionView!
#IBOutlet var headerView: UIView!
let fullView: CGFloat = 0//50
var partialView: CGFloat {
return UIScreen.main.bounds.height - 50
}
let collectionMargin = CGFloat(32)
let itemSpacing = CGFloat(10)
let itemHeight = CGFloat(333)
var itemWidth = CGFloat(0)
var currentItem = 5
override func viewDidLoad() {
super.viewDidLoad()
self.definesPresentationContext = true
self.collection.delegate = self
self.collection.dataSource = self
self.collection.register(UINib(nibName:"AdsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "default")
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(ScrollableBottomSheetViewController.panGesture))
gesture.delegate = self
view.addGestureRecognizer(gesture)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
//CGFloat(219) //
itemWidth = UIScreen.main.bounds.width - collectionMargin * 2.0
print(itemWidth)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: itemWidth, height: itemHeight)
layout.headerReferenceSize = CGSize(width: collectionMargin, height: 0)
layout.footerReferenceSize = CGSize(width: collectionMargin, height: 0)
layout.minimumLineSpacing = itemSpacing
layout.scrollDirection = .horizontal
collection!.collectionViewLayout = layout
collection?.decelerationRate = UIScrollViewDecelerationRateFast
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = Float(itemWidth + itemSpacing)
let targetXContentOffset = Float(targetContentOffset.pointee.x)
let contentWidth = Float(collection!.contentSize.width )
var newPage = Float(self.pageControl.currentPage)
if velocity.x == 0 {
newPage = floor( (targetXContentOffset - Float(pageWidth) / 2) / Float(pageWidth)) + 1.0
} else {
newPage = Float(velocity.x > 0 ? self.pageControl.currentPage + 1 : self.pageControl.currentPage - 1)
if newPage < 0 {
newPage = 0
}
if (newPage > contentWidth / pageWidth) {
newPage = ceil(contentWidth / pageWidth) - 1.0
}
}
self.pageControl.currentPage = Int(newPage)
let point = CGPoint (x: CGFloat(newPage * pageWidth) + itemSpacing, y: targetContentOffset.pointee.y)
targetContentOffset.pointee = point
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.6, animations: { [weak self] in
let frame = self?.view.frame
let yComponent = self?.partialView
self?.view.frame = CGRect(x: 0, y: yComponent!, width: frame!.width, height: frame!.height)// - 100)
})
}
#objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
let y = self.view.frame.minY
if (y + translation.y >= fullView) && (y + translation.y <= partialView) {
self.view.frame = CGRect(x: 0, y: y + translation.y, width: view.frame.width, height: view.frame.height)
recognizer.setTranslation(CGPoint.zero, in: self.view)
}
if recognizer.state == .ended {
var duration = velocity.y < 0 ? Double((y - fullView) / -velocity.y) : Double((partialView - y) / velocity.y)
duration = duration > 1.3 ? 1 : duration
UIView.animate(withDuration: duration, delay: 0.0, options: [.allowUserInteraction], animations: {
if velocity.y >= 0 {
self.view.frame = CGRect(x: 0, y: self.partialView, width: self.view.frame.width, height: self.view.frame.height)
} else {
self.view.frame = CGRect(x: 0, y: self.fullView, width: self.view.frame.width, height: self.view.frame.height)
}
}, completion: { [weak self] _ in
if velocity.y < 0 {
self?.collection.isScrollEnabled = true
}
})
}
}
func prepareBackgroundView() {
let blurEffect = UIBlurEffect.init(style: .dark)
let visualEffect = UIVisualEffectView.init(effect: blurEffect)
let bluredView = UIVisualEffectView.init(effect: blurEffect)
bluredView.contentView.addSubview(visualEffect)
visualEffect.frame = UIScreen.main.bounds
bluredView.frame = UIScreen.main.bounds
view.insertSubview(bluredView, at: 0)
}
}
extension ScrollableBottomSheetViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "default", for: indexPath)
return cell
}
}
extension ScrollableBottomSheetViewController: UIGestureRecognizerDelegate {
// Solution
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
let direction = gesture.velocity(in: view).y
let y = view.frame.minY
if (y == fullView && collection.contentOffset.x == 0 && direction > 0) || (y == partialView) {
collection.isScrollEnabled = false
} else {
collection.isScrollEnabled = true
}
return false
}
}

Subview to TabBarController is blocking tab bar items in iOS

I'm working on something like the draggable video player in youtube app. I've been able to make the swipe down work and I'm adding the video player view as a subview to the Tab bar controller which is the root view of my application. The issue is when I swipe down to minimise the video, I'm not able to select two tab bar items underneath the video player. How do I solve this?
In the image, the Live and Me tabs are not selectable unless I remove the video player from the screen.
My view hierarchy is UITabBarController > UINavigationController > UIViewController so i subclassed UINavigationController:
class CFNavigationController: UINavigationController, CFVideoPlayerControllerDelegate {
//MARK: Properties
lazy var videoPlayerViewController: CFVideoPlayerController = {
let pvc: CFVideoPlayerController = self.storyboard?.instantiateViewController(withIdentifier: "videoPlayer") as! CFVideoPlayerController
pvc.view.frame = CGRect.init(origin: self.hiddenOrigin, size: UIScreen.main.bounds.size)
pvc.delegate = self
return pvc
}()
let statusView: UIView = {
let st = UIView.init(frame: UIApplication.shared.statusBarFrame)
st.backgroundColor = UIColor.white
st.alpha = 0.15 // 0.15
return st
}()
let hiddenOrigin: CGPoint = {
let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
let x = -UIScreen.main.bounds.width
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}()
let minimizedOrigin: CGPoint = {
let x = UIScreen.main.bounds.width/2 - 10
let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 60
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}()
let fullScreenOrigin = CGPoint.init(x: 0, y: 0)
//Methods
func animatePlayView(toState: stateOfVC) {
switch toState {
case .fullScreen:
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: UIViewAnimationOptions.beginFromCurrentState, animations: {
self.videoPlayerViewController.view.frame.origin = self.fullScreenOrigin
})
case .minimized:
UIView.animate(withDuration: 0.3, animations: {
self.videoPlayerViewController.view.frame.origin = self.minimizedOrigin
})
case .hidden:
UIView.animate(withDuration: 0.3, animations: {
self.videoPlayerViewController.view.frame.origin = self.hiddenOrigin
self.videoPlayerViewController.videoPlayerView.player?.replaceCurrentItem(with: nil)
})
}
}
func positionDuringSwipe(scaleFactor: CGFloat) -> CGPoint {
let width = UIScreen.main.bounds.width * 0.5 * scaleFactor
let height = width * 9 / 16
let x = (UIScreen.main.bounds.width - 10) * scaleFactor - width
let y = (UIScreen.main.bounds.height - 10 ) * scaleFactor - height
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}
//MARK: Delegate methods
func didMinimize() {
self.animatePlayView(toState: .minimized)
}
func didMaximize(){
self.animatePlayView(toState: .fullScreen)
}
func didEndedSwipe(toState: stateOfVC){
self.animatePlayView(toState: toState)
}
func swipeToMinimize(translation: CGFloat, toState: stateOfVC){
switch toState {
case .fullScreen:
self.videoPlayerViewController.view.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
case .hidden:
// self.videoPlayerViewController.view.frame.origin.x = UIScreen.main.bounds.width/2 - abs(translation) - 10
self.videoPlayerViewController.removeFromParentViewController()
case .minimized:
self.videoPlayerViewController.view.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
}
}
//MARK: ViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.view.addSubview(self.videoPlayerViewController.view)
self.tabBarController?.view.bringSubview(toFront: self.videoPlayerViewController.view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
// Give View Controllers the power of their own rotation
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return visibleViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations
}
open override var shouldAutorotate: Bool {
return visibleViewController?.shouldAutorotate ?? super.shouldAutorotate
} }
Here is how I add it to as subview:
self.tabBarController?.view.addSubview(self.videoPlayerViewController.view)
self.tabBarController?.view.bringSubview(toFront: self.videoPlayerViewController.view)

Resources