Efficiently load many views in a UIScrollView - ios

I have a UIScrollView that I am trying to use as an Image Viewer. For this I have paging enabled and I add "Slides" to the view for each Image, including a UIImageView and multiple labels and buttons. This works perfectly while I only have a few Slides to show, but I will need to have more than 100 of them, and I am running into really bad performance issues.
When I present the ViewController, and therefore set up the ScrollView, I get a good 10-15s of delay. Apparently loading this many views is a little much.
So I was wondering if any of you had an idea how I could make this more efficient.
I have tried making the array of Slides in the previous VC, and passing it, instead of creating it on the spot, that helped a bit, but not enough to make it feel acceptable, especially since changing device orientation will require me to set the ScrollView up again (because the Slides height/width will be off).
Here are the functions to set up the Slides, and to present them on the ScrollView:
func createSlides() -> [Slide] {
print("creating Slides")
let Essence = EssenceModel.Essence
var ImageArray = [Slide]()
var slide: Slide
var count = 0
for img in Essence{
count += 1
slide = Bundle.main.loadNibNamed("Slide", owner: self, options: nil)?.first as! Slide
slide.imageView.image = UIImage(named: img.imageUrl)
slide.isUserInteractionEnabled = true
slide.textLabel.text = img.description
slide.likeButton.imageView?.contentMode = .scaleAspectFit
slide.hero.id = img.heroID
slide.tag = count
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(showOrHide))
slide.imageView.addGestureRecognizer(tapGesture)
let dismissGesture = UITapGestureRecognizer(target: self, action: #selector(dismissVC))
slide.backButton.addGestureRecognizer(dismissGesture)
slide.backButton.isUserInteractionEnabled = true
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(swipedUp))
swipeUp.direction = .up
slide.addGestureRecognizer(swipeUp)
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(swipedDown))
swipeDown.direction = .down
slide.addGestureRecognizer(swipeDown)
let slideRecognizer = UITapGestureRecognizer(target: self, action: #selector(startSlideshow))
slide.slideButton.addGestureRecognizer(slideRecognizer)
slide.likeButton.imageView?.contentMode = .scaleAspectFit
slide.setupZoom()
ImageArray.append(slide)
}
count = 0
print(ImageArray.count)
return ImageArray
}
func setupSlideScrollView(slides : [Slide]) {
scrollView.subviews.forEach { $0.removeFromSuperview() }
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(slides.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(slides[i])
}
}
As I said, I am looking for ways of making this more efficient in any way so I can actually use it. Preferebly I would probably just load the Slide that I am on, the next and previous one, but I have no clue how I would go about doing that.
Here is also a Screenshot, so you can see what it looks like.

Would be better to do something like that:
1) Add UICollectionView
2) Add UITableViewCell with restorationID
3) Add relations to your controller/view
4) Set horizontal scroll direction: https://www.screencast.com/t/wmPiwVdY
5) And after that create logic something like that:
class ImageCollectionViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
private var images = [Slide]()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let imageCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageSlideCellRestorationID", for: indexPath) as? ImageSlideCell else {
return ImageSlideCell()
}
imageCell.image.image = UIImage(named: images[indexPath.row].imageUrl)
return imageCell
}
}
class ImageSlideCell: UICollectionViewCell {
#IBOutlet weak var image: UIImageView!
}

Instead using uiscroll and got a performance problems. You should using uicollectionview, scroll horizontal, custom cell with one image and buttons.

Related

How to fade items using reloadItems(at: [indexPath])

I have a view controller that contains a uicollectionview. Each collectionview cell contains a button that, when clicked, adds a new label within the cell. To expand the height of each cell I call reloadItems(at: [indexPath]).
Unfortunately calling reloadItems(at: [indexPath]) fades out the old label and fades in the new label, how do I prevent any labels from fading out?
The bug becomes even more apparent every time I click the addLabel button: a new label fades in but whatever previous labels had not been visible suddenly appear again and whatever labels used to be visible, magically turn invisible again.
reloadItems(at: [indexPath]) seems to toggle the alpha of each new label differently. I would like to resize and add new labels to the cell without having any labels disappear.
Here is my code:
ViewController
class ViewController: UIViewController {
weak var collectionView: UICollectionView!
var expandedCellIdentifier = "ExpandableCell"
var cellWidth:CGFloat{
return collectionView.frame.size.width
}
var expandedHeight : CGFloat = 200
var notExpandedHeight : CGFloat = 50
//the first Int gives the row, the second Int gives the amount of labels in the row
var isExpanded = [Int:Int]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<4 {
isExpanded[i] = 1
}
}
}
extension ViewController:UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return isExpanded.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: expandedCellIdentifier, for: indexPath) as! ExpandableCell
cell.indexPath = indexPath
cell.delegate = self
cell.setupCell = "true"
return cell
}
}
extension ViewController:UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if isExpanded[indexPath.row]! > 1{
let height = (collectionView.frame.width/10)
let newHeight = height * CGFloat(isExpanded[indexPath.row]!)
return CGSize(width: cellWidth, height: newHeight)
}else{
return CGSize(width: cellWidth, height: collectionView.frame.width/6 )
}
}
}
extension ViewController:ExpandedCellDeleg{
func topButtonTouched(indexPath: IndexPath) {
isExpanded[indexPath.row] = isExpanded[indexPath.row]! + 1
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIView.AnimationOptions.curveEaseInOut, animations: {
self.collectionView.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
}
}
Protocol
protocol ExpandedCellDeleg:NSObjectProtocol{
func topButtonTouched(indexPath:IndexPath)
}
ExpandableCell
class ExpandableCell: UICollectionViewCell {
weak var delegate:ExpandedCellDeleg?
public var amountOfIntervals:Int = 1
public var indexPath:IndexPath!
var setupCell: String? {
didSet {
print("cell should be setup!!")
}
}
let ivAddLabel: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = #imageLiteral(resourceName: "plus")
imageView.tintColor = .black
imageView.contentMode = .scaleToFill
imageView.backgroundColor = UIColor.clear
return imageView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
contentView.addSubview(ivAddLabel)
let name = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 18))
name.center = CGPoint(x: Int(frame.width)/2 , y: 20)
name.textAlignment = .center
name.font = UIFont.systemFont(ofSize: 16)
name.textColor = UIColor.black
name.text = "Fred"
contentView.addSubview(name)
ivAddLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -14).isActive = true
ivAddLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
ivAddLabel.widthAnchor.constraint(equalToConstant: 20).isActive = true
ivAddLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
ivAddLabel.layer.masksToBounds = true
ivAddLabel.isUserInteractionEnabled = true
let addGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ivAddLabelSelected))
ivAddLabel.addGestureRecognizer(addGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func ivAddLabelSelected(){
print("add button was tapped!")
if let delegate = self.delegate{
amountOfIntervals = amountOfIntervals + 1
let height = (20*amountOfIntervals)
let name = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 18))
name.center = CGPoint(x: Int(frame.width)/2, y: height)
name.textAlignment = .center
name.font = UIFont.systemFont(ofSize: 16)
name.textColor = UIColor.black
name.text = "newFred"
name.alpha = 0.0
contentView.addSubview(name)
UIView.animate(withDuration: 0.2, animations: { name.alpha = 1.0 })
delegate.topButtonTouched(indexPath: indexPath)
}
}
}
It's because you animate the new label
UIView.animate(withDuration: 0.2, animations: { name.alpha = 1.0 })
and in parallel reload the cell which creates a new cell/reuses existing and shows it, but also you wrap the reload into animation block which seems strange and useless:
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIView.AnimationOptions.curveEaseInOut, animations: {
self.collectionView.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
You need to remove both animations and just reload the cell. If you need a nice animation of cell expansion you need to implement collection layout which will handle all states - start, intermediate, end of the animation. It's hard.
Try to use suggested in other answer "UICollectionView Self Sizing Cells with Auto Layout" if it will not help, then either forgot the idea of animation or implement custom layout.
I'd suggest you read into self-sizing UICollectionViewCells (e.g. UICollectionView Self Sizing Cells with Auto Layout) and UIStackView (e.g. https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-swift-3/).
You should use a UIStackView, with constraints to top and bottom edge of your cells contentView.
Then you can add your Labels as managedSubviews to your stackView. This will add the labels with animation.
With self-sizing cell you do not need to reloadItems and it should work as you expect.

How do I make new image views movable?

I have been trying to piece together a project that contains a collection view that scrolls on the side, and when a cell is tapped it will add a new image view to the scene. I would like this new image to be draggable.
My code currently shows the collection view and when tapped adds a new image (which I call stickers).
I haven't quite figured out how to make it place the correct sticker yet, but my first goal is to make sure what appears can be moved.
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionView: UICollectionView!
let stickers: [UIImage] = [
UIImage(named: "cow")!,
UIImage(named: "chicken")!,
UIImage(named: "pig")!,
UIImage(named: "cow")!,
UIImage(named: "chicken")!,
UIImage(named: "pig")!,
UIImage(named: "cow")!,
UIImage(named: "chicken")!,
UIImage(named: "pig")!,
]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return stickers.count
}
var activeSticker = UIImage(named: "cow")
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
cell.stickerImage.image = stickers[indexPath.item]
cell.backgroundColor = UIColor(white: 1, alpha: 0.9)
cell.translatesAutoresizingMaskIntoConstraints = false
cell.contentMode = .scaleAspectFill
cell.clipsToBounds = true
cell.layer.cornerRadius = 7
let addSticker = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
addSticker.addTarget(self, action: #selector(addStickerTapped), for: UIControl.Event.touchUpInside)
activeSticker = cell.stickerImage.image
cell.addSubview(addSticker)
return cell
}
#IBAction func addStickerTapped() -> Void {
print("Hello Sticker Button")
let image = activeSticker //UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: 100, y: 100, width: 100, height: 200)
imageView.contentMode = .scaleAspectFit
imageView.isUserInteractionEnabled = true
self.view.addSubview(imageView)
//Imageview on Top of View
self.view.bringSubviewToFront(imageView)
}
}
To move the imageView around inside self.view you can use UIPanGestureRecognizer. Add the gesture recognizer, for example, in viewDidLoad:
class ViewController: UIViewController {
var selectedImageView: UIImageView?
override func viewDidLoad() {
super.viewDidLoad()
addPanGestureRecognizer()
}
func addPanGestureRecognizer() {
let pan = UIPanGestureRecognizer(target: self, action: #selector(moveImageView(_:)))
// set up and optimize pan gesture options here if you need to
self.view.addGestureRecognizer(pan)
}
#objc func moveImageView(_ sender: UIPanGestureRecognizer) {
// assign one of your image views to selectedImageView to ensure you only move one image view at a time
// for example in func addStickerTapped() you could assign selectedImageView = imageView
guard let selectedImageView = selectedImageView else {
return
}
switch sender.state {
case .changed, .ended:
selectedImageView.center = selectedImageView.center.offset(by: sender.translation(in: self.view))
sender.setTranslation(.zero, in: self.view)
default:
break
}
}
}
extension CGPoint {
func offset(by point: CGPoint) -> CGPoint {
return CGPoint(x: self.x + point.x, y: self.y + point.y)
}
}

How do I focus UIView?

I want to implement functions like radio buttons. More specifically, I want to implement the ability to select only one UIView from several UIViews. This is similar to the Focus Engine on tvOS.
While searching of relevant this, I noticed that UIKit supports Focus-based Navigation. But I am not sure if this supports exactly what I want. There is also a lack of additional relevant examples.
I would like to hear some help and advice on related features. Is the Focus-based Navigation suitable for the purpose I was pursuing? And are there any other good ways to implement the functionality I want to implement?
#Paulw Thank you for your kind help.
The following steps solved the problem!
I have used a simple way that effects a specified UIView among multiple UIViews.
import UIKit
class ViewController: UIViewController {
var selectView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
selectView = self.view
let viw = UIView(frame: CGRect(x: 100, y: 100, width: 150, height: 150))
viw.backgroundColor = UIColor.white
viw.layer.cornerRadius = 10
self.view.addSubview(viw)
let objectView = ObjectView()
objectView.frame.size = CGSize(width: 150, height: 150)
objectView.backgroundColor = UIColor.clear
self.view.addSubview(objectView)
let tapObject = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
objectView.addGestureRecognizer(tapObject)
let tapObjects = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
viw.addGestureRecognizer(tapObjects)
let tapRootView = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
self.view.addGestureRecognizer(tapRootView)
}
#objc func handleTap(sender: UITapGestureRecognizer) {
if sender.state == .ended {
if selectView != self.view {
selectView?.layer.shadowColor = UIColor.clear.cgColor
}
selectView = sender.view
if selectView != self.view {
sender.view?.layer.shadowOffset = .zero
sender.view?.layer.shadowOpacity = 0.5
sender.view?.layer.shadowColor = UIColor.black.cgColor
}
}
}
}

Change touch receiver while scrolling a scroll view

Apps like Apple's maps app or Google maps use scrollable bottom sheet overlays to present additional content. While this behavior is not too difficult to rebuild, I struggle to implement one important feature:
When there is a scroll view embedded inside the bottom sheet, then the user can scroll it to the top but then – instead of bouncing off at the top – the bottom sheet starts scrolling down instead of the table view.
Here's an example video of what I mean:
Example Video:
This is a nice user experience as there is no interruption in the scrolling and it's what I expect as a user: It's as if once the content scroll view has reached its top the gesture receiver is automatically handed over the super scroll view.
In order to achieve this behavior, I see three different approaches:
I track the content scroll view's contentOffset in the scroll view's scrollViewDidScroll(_:) delegate method. Then I do
if contentScrollView.contentOffset.y < 0 {
contentScrollView.contentOffset.y = 0
}
to keep the content scroll view from scrolling above the top of its content. Instead, I pass the y distance that it would have scrolled to the super scroll view which scrolls the whole bottom sheet.
I find a way to change the receiver of the scrolling (pan) gesture recognizer from the content scroll view to the super scroll view as soon as the content scroll view has scrolled to its top.
I handle everything inside the super scroll view. It asks its content view controller through a delegate protocol if it wants to handle the touches and only if it doesn't (because its content scroll view has reached the top) the super scroll view scrolls by itself.
While I have managed to implement the first variant (it's what you see in the video), I'd strongly prefer to use approach 2 or 3. It's a much cleaner way to have the view controller that controls the bottom sheet manage all the scrolling logic without exposing its internals.
Unfortunately, I haven't found a way to somehow split the pan gesture into two components (one that controls the receiver scroll view and one that controls another scroll view)
Any ideas on how to achieve this kind of behavior?
I am very interested in this question and I hope by providing how I would implement it, it does not stifle an answer that might show how to truly pass around the responder. The trick I think which I put in the comments is keeping track of the touches. I forgot about how scrollview gobbles those up but you can use a UIPanGesture. See if this is close to what you are looking for. The only case I ran into that might take more thought is using the scroll to dismiss the bottom view. Most of this code is setup to get a working scrollview in the view. I think property animations might be best to make it interruptible or even my personal fav Facebook Pop animations. To keep it simple I just used UIView animations. Let me know if this solves what you are looking for. The code is below and here is the result
. The scrollview remains scrollable and active. I animate the frames but updating constraints could work as well.
import UIKit
class ViewController: UIViewController{
//setup
var items : [Int] = []
lazy var tableView : UITableView = {
let tv = UITableView(frame: CGRect(x: 0, y: topViewHeight, width: self.view.frame.width, height: self.view.frame.height))
tv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
tv.delegate = self
tv.dataSource = self
tv.layer.cornerRadius = 4
return tv
}()
lazy var topView : UIView = {
let v = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: topViewHeight))
v.backgroundColor = .green
v.autoresizingMask = [.flexibleWidth,.flexibleHeight]
return v
}()
let cellIdentifier = "ourCell"
//for animation
var isAnimating = false
var lastOffset : CGPoint = .zero
var startingTouch : CGPoint?
let topViewHeight : CGFloat = 500
var isShowing : Bool = false
let maxCollapse : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
for x in 0...100{
items.append(x)
}
// Do any additional setup after loading the view, typically from a nib.
self.view.addSubview(topView)
self.view.addSubview(tableView)
self.tableView.reloadData()
let pan = UIPanGestureRecognizer(target: self, action: #selector(moveFunction(pan:)))
pan.delegate = self
self.view.addGestureRecognizer(pan)
}
#objc func moveFunction(pan:UIPanGestureRecognizer) {
let point:CGPoint = pan.location(in: self.view)
switch pan.state {
case .began:
startingTouch = point
break
case .changed:
processMove(touchPoint:point.y)
break
default:
processEnding(currentPointY: point.y)
break
}
}
}
extension ViewController : UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : UITableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
}
cell.textLabel?.text = "\(items[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 30
}
}
extension ViewController : UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isAnimating == true{
scrollView.contentOffset = lastOffset
return
}
lastOffset = scrollView.contentOffset
}
}
extension ViewController : UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
extension ViewController{
func processMove(touchPoint:CGFloat){
if let start = startingTouch{
if touchPoint <= topViewHeight && start.y > topViewHeight{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if touchPoint >= self.maxCollapse && isShowing == true && start.y < self.maxCollapse{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if isShowing == true && self.tableView.contentOffset.y <= 0{
//this is the only one i am slightly unsure about
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}
}
self.isAnimating = false
}
func processEnding(currentPointY:CGFloat){
startingTouch = nil
if isAnimating{
if currentPointY < topViewHeight/2{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.maxCollapse, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = true
}
}else{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.topViewHeight, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = false
}
}
}
self.isAnimating = false
}
}

Swipe to delete on CollectionView

I'm trying to replicate the swipe to delete functionality of iOS. I know it's instantly available on a tableview, but the UI that I need to build benefits from a Collection View. Therefor I need a custom implementation where I would be using a swipe up gesture. Luckily, that's something that I managed to implement myself, however I'm having a hard time figuring out how I need to setup the swipe to delete / tap to delete / ignore functionality.
The UI currently looks like this:
So I'm using the following collectionview:
func buildCollectionView() {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 0;
layout.minimumLineSpacing = 4;
collectionView = UICollectionView(frame: CGRect(x: 0, y: screenSize.midY - 120, width: screenSize.width, height: 180), collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(VideoCell.self, forCellWithReuseIdentifier: "videoCell")
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.contentInset = UIEdgeInsetsMake(0, 20, 0, 30)
collectionView.backgroundColor = UIColor.white()
collectionView.alpha = 0.0
//can swipe cells outside collectionview region
collectionView.layer.masksToBounds = false
swipeUpRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.deleteCell))
swipeUpRecognizer.delegate = self
collectionView.addGestureRecognizer(swipeUpRecognizer)
collectionView.isUserInteractionEnabled = true
}
My custom videocell contains one image and below that there is the delete button. So if you swipe the image up the delete button pops up. Not sure if this is the right way on how to do it:
class VideoCell : UICollectionViewCell {
var deleteView: UIButton!
var imageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
deleteView = UIButton(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
deleteView.contentMode = UIViewContentMode.scaleAspectFit
contentView.addSubview(deleteView)
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
imageView.contentMode = UIViewContentMode.scaleAspectFit
contentView.addSubview(imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And I'm using the following logic:
func deleteCell(sender: UIPanGestureRecognizer) {
let tapLocation = sender.location(in: self.collectionView)
let indexPath = self.collectionView.indexPathForItem(at: tapLocation)
if velocity.y < 0 {
//detect if there is a swipe up and detect it's distance. If the distance is far enough we snap the cells Imageview to the top otherwise we drop it back down. This works fine already.
}
}
But the problem starts there. As soon as my cell is outside the collectionview bounds I can't access it anymore. I still want to swipe it further to remove it. I can only do this by swiping on the delete button, but I want the Imageview above it to be swipeable as well. Or if I tap the image outside the collectionview it should slide back into the line and not delete it.
If I increase the collectionview bounds I can prevent this problem but than I can also swipe to remove outside the cell's visible height. This is caused by the tapLocation that is inside the collectionview and detects an indexPath. Something that I don't want. I want the swipe up only to work on a collectionview's cell.
Also the button and the image interfere with each other because I cannot distinguish them. They are both in the same cell so that's why I'm wondering if I should have the delete button in the cell at all. Or where should I place it otherwise? I could also make two buttons out of it and disable user interaction depending on state, but not sure how that would end up.
So, if you want the swipes gesture recogniser to continue recording movement when they are outside of their collection view, you need to attach it to the parent of the collection view, so it's bounded to the full area where the user can swipe.
That does mean that you will get swipes for things outside the collection view, but you can quite easily ignore those using any number of techniques.
To register delete button taps, you'll need to call addTarget:action:forControlEvents: on the button
I would keep the cell as you have it, with the image and the button together. It will be much easier to manage, and they belong together.
To manage moving the image up and down, I would look at using a transform, or an NSLayoutConstraint. Then you just have to adjust one value to make it move up and down in sync with the user swipes. No messing with frames.
For my own curiosity's sake I tried to make a replicate of what you're trying to do, and got it to work somehow good. It differs from yours in the way I setup the swipe gestures as I didn't use pan, but you said you already had that part, and haven't spend too much time on it. Pan is obviously the more solid solution to make it interactive, but takes a little longer to calculate, but the effect and handling of it, shouldn't differ much from my example.
To resolve the issue not being able to swipe outside the cell I decided to check if the point was in the swiped rect, which is twice the height of the non-swiped rect like this:
let cellFrame = activeCell.frame
let rect = CGRectMake(cellFrame.origin.x, cellFrame.origin.y - cellFrame.height, cellFrame.width, cellFrame.height*2)
if CGRectContainsPoint(rect, point) {
// If swipe point is in the cell delete it
let indexPath = myView.indexPathForCell(activeCell)
cats.removeAtIndex(indexPath!.row)
myView.deleteItemsAtIndexPaths([indexPath!])
}
I created a demonstration with comments: https://github.com/imbue11235/swipeToDeleteCell
I hope it helps you in anyway!
If you want to make it mare generic:
Make a costume Swipeable View:
import UIKit
class SwipeView: UIView {
lazy var label: UILabel = {
let label = UILabel()
label.textColor = .black
label.backgroundColor = .green
return label
}()
let visableView = UIView()
var originalPoint: CGPoint!
var maxSwipe: CGFloat! = 50 {
didSet(newValue) {
maxSwipe = newValue
}
}
#IBInspectable var swipeBufffer: CGFloat = 2.0
#IBInspectable var highVelocity: CGFloat = 300.0
private let originalXCenter: CGFloat = UIScreen.main.bounds.width / 2
private var panGesture: UIPanGestureRecognizer!
public var isPanGestureEnabled: Bool {
get { return panGesture.isEnabled }
set(newValue) {
panGesture.isEnabled = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupGesture()
}
private func setupViews() {
addSubview(visableView)
visableView.addSubview(label)
visableView.edgesToSuperview()
label.edgesToSuperview()
}
private func setupGesture() {
panGesture = UIPanGestureRecognizer(target: self, action: #selector(swipe(_:)))
panGesture.delegate = self
addGestureRecognizer(panGesture)
}
#objc func swipe(_ sender:UIPanGestureRecognizer) {
let translation = sender.translation(in: self)
let newXPosition = center.x + translation.x
let velocity = sender.velocity(in: self)
switch(sender.state) {
case .changed:
let shouldSwipeRight = translation.x > 0 && newXPosition < originalXCenter
let shouldSwipeLeft = translation.x < 0 && newXPosition > originalXCenter - maxSwipe
guard shouldSwipeRight || shouldSwipeLeft else { break }
center.x = newXPosition
case .ended:
if -velocity.x > highVelocity {
center.x = originalXCenter - maxSwipe
break
}
guard center.x > originalXCenter - maxSwipe - swipeBufffer, center.x < originalXCenter - maxSwipe + swipeBufffer, velocity.x < highVelocity else {
center.x = originalXCenter
break
}
default:
break
}
panGesture.setTranslation(.zero, in: self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SwipeView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
The embed swappable view in UICollectionViewCell:
import UIKit
import TinyConstraints
protocol DeleteCellDelegate {
func deleteCell(_ sender : UIButton)
}
class SwipeableCell: UICollectionViewCell {
lazy var deleteButton: UIButton = {
let button = UIButton()
button.backgroundColor = .red
button.addTarget(self, action: #selector(didPressedButton(_:)), for: .touchUpInside)
button.titleLabel?.text = "Delete"
return button
}()
var deleteCellDelegate: DeleteCellDelegate?
#objc private func didPressedButton(_ sender: UIButton) {
deleteCellDelegate?.deleteCell(sender)
print("delete")
}
let swipeableview: SwipeView = {
return SwipeView()
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(deleteButton)
addSubview(swipeableview)
swipeableview.edgesToSuperview()
deleteButton.edgesToSuperview(excluding: .left, usingSafeArea: true)
deleteButton.width(bounds.width * 0.3)
swipeableview.maxSwipe = deleteButton.bounds.width
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
A sample ViewController:
import UIKit
import TinyConstraints
class ViewController: UIViewController, DeleteCellDelegate {
func deleteCell(_ sender: UIButton) {
let indexPath = IndexPath(item: sender.tag, section: 0)
items.remove(at: sender.tag)
collectionView.deleteItems(at: [indexPath])
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: view.bounds.width, height: 40)
layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .yellow
cv.isPagingEnabled = true
cv.isUserInteractionEnabled = true
return cv
}()
var items = ["1", "2", "3"]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.edgesToSuperview(usingSafeArea: true)
collectionView.register(SwipeableCell.self, forCellWithReuseIdentifier: "cell")
let panGesture = UIPanGestureRecognizer()
view.addGestureRecognizer(panGesture)
panGesture.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SwipeableCell
cell.backgroundColor = .blue
cell.swipeableview.label.text = items[indexPath.item]
cell.deleteButton.tag = indexPath.item
cell.deleteCellDelegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}

Resources