I have a UICollectionView. If I touch a cell, it triggers a segue. If "trash" or "save" is enabled, then users should be able to touch cells to add to an array that is processed for the corresponding action.
When trash/save is enabled, the segue triggers instead of allowing multiple selection. How do I do this so that I can have 2 modes: 1 for segues and 1 for multiple selection.
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
if (self.saveEnabled == YES) {
NSArray *itemsToDelete = [self.collectionView indexPathsForSelectedItems];
[self.itemsArray addObjectsFromArray:itemsToDelete];
}
else if (self.trashEnabled == YES) {
NSArray *itemsToDelete = [self.collectionView indexPathsForSelectedItems];
[self.itemsArray addObjectsFromArray:itemsToDelete];
}
else{
[self performSegueWithIdentifier:#"collectionUnwind" sender:self];
}
}
The key step is that you need return false in shouldPerformSegueWithIdentifier when in multi-selection mode.
Here is how I do this in swift:
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
return !self.collectionView.allowsMultipleSelection
}
And I switch between single-select and multi-select mode with long press, you can use button to do the same.
func setupLongPressGesture() {
let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress))
longPressGesture.minimumPressDuration = 1.0 // 1 second press
longPressGesture.delegate = self
self.collectionView.addGestureRecognizer(longPressGesture)
}
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .began {
self.labelState.text = "multiple selection enabled"
} else if gestureRecognizer.state == .ended {
self.collectionView.allowsMultipleSelection = !self.collectionView.allowsMultipleSelection
}
}
I refer to the tutorial here: https://www.appcoda.com/ios-collection-view-tutorial/
Not sure of what you really want. Tell us exactly what you do (buttons) and in which order to delete (or save) multiple cells.
By the way did you enable multiple selection on your collection view ?
[self.collectionView setAllowsMultipleSelection:YES];
EDIT :
In your storyboard, just add a segue with "collectionUnwind" identifier from your current ViewController (and not the cells of its CollectionView) to the new ViewController you want to push. If you link it to the cells, Xcode will assume you need the new ViewController to be pushed on every cell selection.
Your current code should do the rest.
Related
I wrote a UITableView extension that would allow the reordering of cells after holding the tableview.
So the tableView goes edit mode as expected but what I would like to do is to add a Done button in the navigation bar of a viewController, when the tableView is being edited and that would end the edit mode when tapped.
How can I show/hide this button in viewController according to if the tableView being edited or not in UITableView extension?
that is my extension:
import UIKit
extension UITableView {
func addLongPressToTableView() {
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressGesture(sender:)))
longPress.minimumPressDuration = 0.8 // optional
self.addGestureRecognizer(longPress)
}
#objc func onLongPressGesture(sender: UILongPressGestureRecognizer) {
if (sender.state == .began) {
self.isEditing = true
self.setEditing(true, animated: false)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
}
}
// ViewController, hide/show editButtonItem (done button?)
// navigationItem.rightBarButtonItems = [addButton, editButtonItem]
The UITableViewDelegate provides support for coordinating editing. Use the following two delegate methods to respond to changes in the editing state.
func tableView(UITableView, willBeginEditingRowAt: IndexPath)
func tableView(UITableView, didEndEditingRowAt: IndexPath?)
I am writing an iOS card game. I display the player's cards in a collection view. The player can select one or more cards by tapping on them, and then press a deal button to deal the selected cards.
I want to allow the user to use multiple fingers to select multiple cards at once. For example, if the user wants to select 2 cards, he just needs to tap the two cards at the same time, with two fingers, and they will both be selected. It seems like that by default, UICollectionView does not allow this. When I tap with 2 fingers, only one of the cards will be selected, even though the isMultipleTouchEnabled property in UIView is already set to true.
Note that I am not asking about how to allow a user to select multiple items in a collection view. I can and did already do that with allowsMultipleSelection = true. What I am asking is how to allow the user to select 2 cells with 2 fingers (or n cells with n fingers).
I found this question, but that seems to be about how to show a border around a cell when it is selected.
I also looked into the documentation of UICollectionView but I found no property that controls this.
First let's understand exactly what the problem is. The collectionView has a bunch of UIGestureRecognisers attached to it (for pan, touch, zoom, etc). Each recogniser has the same state machine of possible->recognised-> changed-> ended/failed. Each recogniser has a clear start beginning and end. Once the tap gesture has started in one location it is not going to begin in another location. When a person 1) touches down point A 2) touches down point B 3) touches up point A 4) touches up point B that the gesture completely ignores point B because it is "focused" on point A.
The second problem is that if you touch at two points at the exact same time the method of tapGesture.location(in: view) will give you the average of those two location.
However we solve this the first step is to disable the collectionView tapGesture - it is not doing what we want :
self.collectionView.allowsMultipleSelection = true
self.collectionView.allowsSelection = false;
Next we are going to add our own tap gestures to each cell individually. This is explicitly NOT recommend by apple ("You should always attach your gesture recognizers to the collection view itself—not to a specific cell or view."1) but it will work:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
cell.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(didTap(tapper:))))
...
return cell;
}
#objc func didTap(tapper:UIGestureRecognizer) {
if let cell = tapper.view as? UICollectionViewCell{
if let index = collectionView.indexPath(for: cell) {
if collectionView.indexPathsForSelectedItems?.contains(index) ?? false {
collectionView.deselectItem(at: index, animated: true)
cell.isSelected = false
}else{
collectionView.selectItem(at: index, animated: true, scrollPosition: [])
cell.isSelected = true
}
}
}
}
You could add multiple gesture recognizers for the number of touches you want to support:
collectionView.allowsMultipleSelection = true
// allowing up to 5 finger touches, increase if you want for more :)
for i in 2...5 {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
gestureRecognizer.numberOfTouchesRequired = i
gestureRecognizer.delegate = self
collectionView.addGestureRecognizer(gestureRecognizer)
}
, and have the controller to look for the cells that were touched:
#objc private func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
// perform the action only after the touch ended
guard gestureRecognizer.state == .ended else { return }
for i in 0..<gestureRecognizer.numberOfTouches {
let location = gestureRecognizer.location(ofTouch: i, in: collectionView)
// if we have a cell at that point, toggle the selection
if let indexPath = collectionView.indexPathForItem(at: location) {
if collectionView.indexPathsForSelectedItems?.contains(indexPath) == true {
collectionView.deselectItem(at: indexPath, animated: true)
} else {
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
}
}
}
}
I've been trying this for hours with no luck. I have a UICollectionView collectionView. The collection view is basically a list with the last cell always being a cell with a big plus sign to add another item. I've enabled reordering with the following. What I'd like for it to do is when I start the interactive movement, the plus sign cell goes away, and then when the user is done editing, it appears again. This is a basic version of the code I have:
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
...
self.collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
removeAddCell()
case UIGestureRecognizerState.Changed:
case UIGestureRecognizerState.Ended:
...
collectionView.endInteractiveMovement()
replaceAddCell()
default:
collectionView.cancelInteractiveMovement()
}
}
func removeAddCell(){
print("Reloading data - removing add cell")
data_source.popLast()
self.collectionView.reloadData()
}
func replaceAddCell(){
print("Reloading data - replacing add cell")
data_source.append("ADD BUTTON")
self.collectionView.reloadData()
}
It's very rough pseudocode, but I can't even get the simplest version of this to work. With the code I have, it gives me the dreaded "Fatal error: unexpectedly found nil while unwrapping an Optional values" on the line where I reference the UICollectionViewCell after removing the items from the data source.
If anyone who has done something like this could share their approach I'd really appreciate it! Thank you!
-Bryce
You can do something like this:
func longPressed(sender: UILongPressGestureRecognizer) {
let indexPath = NSIndexPath(forItem: items.count - 1, inSection: 0)
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! YourCollectionViewCell
switch sender.state {
case .Began:
UIView.animateWithDuration(0.3, animations: {
cell.contentView.alpha = 0
})
case .Ended:
UIView.animateWithDuration(0.3, animations: {
cell.contentView.alpha = 1
})
default: break
}
}
this way it gradually disappears instead of abruptly.
I've done something like this. The data source for the collection view tracks a BOOL to determine whether or not to show the Add Item Cell. And call insertItemsAtIndexPaths: and deleteItemsAtIndexPaths: to animate the Add Item Cell appearing and disappearing. I actually use a Edit button to toggle the modes. But you can adapt this code to use your gesture recognizer.
basic code:
self.editing = !self.editing; // toggle editing mode, BOOL that collection view data source uses
NSIndexPath *indexPath = [self indexPathForAddItemCell];
if (!self.editing) { // editing mode over, show add item cell
if (indexPath) {
[self.collectionView insertItemsAtIndexPaths:#[indexPath]];
}
}
else { // editing mode started, delete add item cell
if (indexPath) {
[self.collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
}
I have a custom UITableView cell and I want to add a long press gesture recognizer to it. Currently, I'm doing it as so:
longPressGesture.minimumPressDuration = 1.0
longPressGesture.addTarget(self, action: "testFeedback")
cell.addGestureRecognizer(longPressGesture)
I'm doing it programmatically because I could not find a good way to detect which cell was tapped within an IBAction. However, I'm having a hard time getting this to work I want to pass a parameter through the selector. I am not opposed to doing this in storyboards, but would appreciate some guidance on it.
Thanks!
testFeedback function should look like this
func testFeedback(gestureRecognizer:UIGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.Ended) {
var point = gestureRecognizer.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(point)
{
println(indexPath.row) /// long press ended
}
}
else if (gestureRecognizer.state == UIGestureRecognizerState.Began){
/// long press started
}
}
I have view controller with UITableView. Height of table view is static and == height of screen. Number of rows in table can change and sometimes it might be a situation when number of rows is less than number of rows which can be visible, so appears some clear area on bottom of table. Can i detect tap on it without gesture recognizer? Are there some delegate methods of UITableView?
All of the supplied answers, including the accepted answer, add a UITapGestureRecognizer to the tableView; while this will work, I've found that this gesture recognizer can interfere with row taps and triggering didSelectRowAtIndexPath in a somewhat unpredictable/nondeterministic way.
If you want to detect taps in the "blank space" as well as in rows, I highly suggest adding a background view to your table, and add the gesture recognizer there:
let tap = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
self.tableView.backgroundView = UIView()
self.tableView.backgroundView?.addGestureRecognizer(tap)
Yes, there are delegate methods, such as:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
However, this will only tell you if a tap occurs on an existing row. If you want to capture taps on the empty space below the rows (or on a section header) you will need to use a gesture recognizer. You can do something like this:
// in viewDidLoad or somewhere similar
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tableTapped:)];
[self.tableView addGestureRecognizer:tap];
//.........
- (void)tableTapped:(UITapGestureRecognizer *)tap
{
CGPoint location = [tap locationInView:self.tableView];
NSIndexPath *path = [self.tableView indexPathForRowAtPoint:location];
if(path)
{
// tap was on existing row, so pass it to the delegate method
[self tableView:self.tableView didSelectRowAtIndexPath:path];
}
else
{
// handle tap on empty space below existing rows however you want
}
}
EDIT: for an alternative approach, consider Connor Neville's approach from his answer on this post and add the gesture recognizer to the table's background.
Thanks to #Stonz2, swift version:
Swift 4
let tap = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
self.tableView.addGestureRecognizer(tap)
#objc func tableTapped(tap:UITapGestureRecognizer) {
let location = tap.location(in: self.tableView)
let path = self.tableView.indexPathForRow(at: location)
if let indexPathForRow = path {
self.tableView(self.tableView, didSelectRowAt: indexPathForRow)
} else {
// handle tap on empty space below existing rows however you want
}
}
Swift 3
let tap = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
self.tableView.addGestureRecognizer(tap)
func tableTapped(tap:UITapGestureRecognizer) {
let location = tap.location(in: self.tableView)
let path = self.tableView.indexPathForRow(at: location)
if let indexPathForRow = path {
self.tableView(self.tableView, didSelectRowAt: indexPathForRow)
} else {
// handle tap on empty space below existing rows however you want
}
}
Swift 2:
let tap = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
self.tableView.addGestureRecognizer(tap)
func tableTapped(tap:UITapGestureRecognizer) {
let location = tap.locationInView(self.tableView)
let path = self.tableView.indexPathForRowAtPoint(location)
if let indexPathForRow = path {
self.tableView(self.tableView, didSelectRowAtIndexPath: indexPathForRow)
} else {
// handle tap on empty space below existing rows however you want
}
}
You don't need gesture recogniser. You should impliment <UITableViewDelegate> in your ViewController.h and then add this method to your ViewController.m:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
where indexPath is index of clicked row/column.