Experiencing a weird interaction flaw with my UI for my tableview cell. I implemented a long press gesture:
func handleLongPress(sender:UILongPressGestureRecognizer!) {
var myCharacters: SelectedCharacter?
let localLongPress = sender as UILongPressGestureRecognizer
let locationInView = localLongPress.locationInView(cardsListed)
let indexPath = charactersListed.indexPathForRowAtPoint(locationInView)
let listed = frc.objectAtIndexPath(indexPath!) as! Characters
let cell: firstCharacterDetails = charactersListed.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath!) as! firstCharacterDetails
if listed == 0 {
} else {
if (sender.state == UIGestureRecognizerState.Ended) {
print("Long press Ended")
} else if (sender.state == UIGestureRecognizerState.Began) {
let bounds: CGRect = UIScreen.mainScreen().bounds
let screenHeight: NSNumber = bounds.size.height
if screenHeight == 480 {
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let characterDetailsController: CharacterDetailsViewController = storyboard.instantiateViewControllerWithIdentifier("characterDetails") as! CharacterDetailsViewController
characterDetailsController.modalPresentationStyle = UIModalPresentationStyle.Popover
let popoverplayersCharacterController = characterDetailsController.popoverPresentationController
popoverCharacterNoteController?.permittedArrowDirections = .Any
popoverCharacterNoteController?.delegate = self
popoverCharacterNoteController?.sourceView = cell.cardDescription
characterDetailsController.characterDetails = listed
self.presentViewController(characterDetailsController, animated: true, completion: nil)
}
this is an example sorry if the coding isn't complete as I just took out this area of code for reference. Now my issue is one that is quite unique and is only likely to happen on accidental gestures by the user but for the safety of the user I would like to remove this problem.
The issue I am facing is that when the user longpress gestures a selected cell if they accidentally drag to another cell with their finger while still holding the cell will actually duplicate itself or drag itself below the cell that the user long press gestured dragged to. I am unsure how to handle preventing this from happening but if anyone has any insight it would be appreciated!
Discovered what I was doing wrong. After looking at the code a second time through I realized calling the dequeueResuableCell was the incorrect function to use. I switch the line of code with:
let cell: firstCharacterDetails = charactersListed.cellForRowAtIndexPath(indexPath!) as! firstCharacterDetails
and problem solved. I wasn't think about much when I copy and pasted some of my code that I reused in a few different controllers.
Related
I have a viewController with a Tableview, multiple TableViewCells and in each TableViewCell, a UICollectionView with multiple UICollectionViewItems. Each collectionView item has a label and image view. I'm trying to get 3d touch to work so that the user and peek and pop by force touching on areas of the tableCell that don't contain the collection view, to preview and pop into one view controller and then be able to do the same thing with one of the images in the collectionView but preview and pop into a different view controller. I have the first scenario working fine, the tableCell remains sharp on the screen when starting to force touch and "peek". I'm stuck on getting this to work in the collection view, no matter what I do only an image view frame remains sharp on the first tableview row regardless of which row i'm actually pressing. Code below:
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
//get the tableviewCell
if let tableCellPath = tableView.indexPathForRow(at: location) {
print("tableCellPath=", tableCellPath)
if let tableCell = tableView.cellForRow(at: tableCellPath) as? VenueMainTableViewCell {
//user tapped on a beer in the collectionview
if let collectionView = tableCell.collectionView {
let collectionPoint = collectionView.convert(location, from: tableView)
if let cvPath = collectionView.indexPathForItem(at: collectionPoint) {
let collectionViewCell = collectionView.cellForItem(at: cvPath) as? VenueMainCollectionViewCell
let cvFrame = collectionViewCell?.itemLabelImageView.frame
let bc = storyboard?.instantiateViewController(withIdentifier: "itemDetail") as! ItemDetailViewController
let ven = UTcheckin.sharedInstance.Venues[collectionView.tag]
let selectedItem = ven.ItemsAtVenue[(collectionViewCell?.tag)!]
bc.item = selectedItem
previewingContext.sourceRect = cvFrame!
return bc
}
}
}
if let tablePath = tableView.indexPathForRow(at: location) {
//user tapping on a venue, this works
previewingContext.sourceRect = tableView.rectForRow(at: tablePath)
let vc = storyboard?.instantiateViewController(withIdentifier: "venueDetail") as! VenueDetailViewController
vc.venue = UTcheckin.sharedInstance.Venues[tablePath.row]
return vc
}
return nil
}
return nil
}
It seems like I need to get the rect of the collection view item image view but how can I access this since it is in the table cell? Thanks in advance for any pointers.
I think the solution for this is the same as for UITableView. You have to register each cell for previewing using registerForPreviewingWithDelegate method. You should register it in
cellForRow method.
This should be very helpful for you. Especially The Solution Paragraph:
How to Peek & Pop A Specific View Inside a UITableViewCell
I implemented an NSFetchedResultsController on a UITableView in a Core Data project in Swift 2.0. Additionally, I have a UISearchController implemented. Everything works perfectly with the exception of the behavior I'm encountering on my custom UITableViewCell buttons.
When UISearchController is active, the customTableViewCell's buttons work as they should. If I click the same button when the fetchedResultsController is displaying its results, the method thinks Index 0 is the sender, regardless of which button I click.
func playMP3File(sender: AnyObject) {
if resultsSearchController.active {
// ** THIS WORKS **
// get a hold of my song
// (self.filteredSounds is an Array)
let soundToPlay = self.filteredSounds[sender.tag]
// grab an attribute
let soundFilename = soundToPlay.soundFilename as String
// feed the attribute to an initializer of another class
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
} else {
// ** THIS ALWAYS GETS THE OBJECT AT INDEX 0 **
let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.tag, inSection: (view.superview?.tag)!)) as! Sound
// OTHER THINGS I'VE TRIED
// let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.indexPath.row, inSection: (view.superview?.tag)!)) as! Sound
// let soundToPlay: Sound = fetchedResultsController.objectAtIndexPath(NSIndexPath(index: sender.indexPath.row)) as! Sound
let soundFilename = soundToPlay.soundFilename as String
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
}
}
Here's an abbreviated version of my cellForRowAtIndexPath to show I'm setting up the cells' buttons:
let customCell: SoundTableViewCell = tableView.dequeueReusableCellWithIdentifier("customCell", forIndexPath: indexPath) as! SoundTableViewCell
if resultsSearchController.active {
let sound = soundArray[indexPath.row]
customCell.playButton.tag = indexPath.row
} else {
let sound = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
customCell.playButton.tag = indexPath.row
}
// add target actions for cells
customCell.playButton.addTarget(self, action: "playMP3file:", forControlEvents: UIControlEvents.TouchUpInside)
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck. Everything that looked promising in the compiler crashed when I clicked the button in the simulator.
Thank you for reading.
Update
Installed Xcode 7.1, rebooted, cleaned caches, nuked derived data, did a cold boot.
Solution
Tags will get the job done in many cases (such as getting the location in an Array) and get lots of votes here, but as I've learned, they don't work all the time. Thank you to Mundi for pointing me towards a more robust solution.
// this gets the correct indexPath when resultsSearchController is not active
let button = sender as! UIButton
let view = button.superview
let cell = view?.superview as! SoundTableViewCell
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
let soundToPlay = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck.
Translating points is indeed the most robust solution. This answer contains the correct code.
The Good
My drag & drop function almost works wonderfully. I longPress a cell and it smoothly allows me to move the pressed cell to a new location between two other cells. The table adjusts and the changes save to core data. Great!
The Bad
My problem is that if I drag the cell below the bottom cell in the table, even if I don't let go (un-press) of the cell... the app crashes. If I do the drag slowly, really it crashes as the cell crosses the y-center of the last cell... so I do think it's a problem related to the snapshot getting a location. Less important, but possibly related, is that if I long press below the last cell with a value in it, it also crashes.
The drag/drop runs off a switch statement that runs one of three sets of code based on the status:
One case when the press begins
One case when the cell is being dragged
One case when when the user lets go of the cell
My code is adapted from this tutorial:
Drag & Drop Tutorial
My code:
func longPressGestureRecognized(gestureRecognizer: UIGestureRecognizer) {
let longPress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longPress.state
var locationInView = longPress.locationInView(tableView)
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
struct My {
static var cellSnapshot : UIView? = nil
}
struct Path {
static var initialIndexPath : NSIndexPath? = nil
}
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
var dragCellName = currentCell.nameLabel!.text
var dragCellDesc = currentCell.descLabel.text
//Steps to take a cell snapshot. Function to be called in switch statement
func snapshotOfCell(inputView: UIView) -> UIView {
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext())
let image = UIGraphicsGetImageFromCurrentImageContext() as UIImage
UIGraphicsEndImageContext()
let cellSnapshot : UIView = UIImageView(image: image)
cellSnapshot.layer.masksToBounds = false
cellSnapshot.layer.cornerRadius = 0.0
cellSnapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0)
cellSnapshot.layer.shadowRadius = 5.0
cellSnapshot.layer.shadowOpacity = 0.4
return cellSnapshot
}
switch state {
case UIGestureRecognizerState.Began:
//Calls above function to take snapshot of held cell, animate pop out
//Run when a long-press gesture begins on a cell
if indexPath != nil && indexPath != nil {
Path.initialIndexPath = indexPath
let cell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
My.cellSnapshot = snapshotOfCell(cell)
var center = cell.center
My.cellSnapshot!.center = center
My.cellSnapshot!.alpha = 0.0
tableView.addSubview(My.cellSnapshot!)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = locationInView.y
My.cellSnapshot!.center = center
My.cellSnapshot!.transform = CGAffineTransformMakeScale(1.05, 1.05)
My.cellSnapshot!.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
cell.hidden = true
}
})
}
case UIGestureRecognizerState.Changed:
if My.cellSnapshot != nil && indexPath != nil {
//Runs when the user "lets go" of the cell
//Sets CG Y-Coordinate of snapshot cell to center of current location in table (snaps into place)
var center = My.cellSnapshot!.center
center.y = locationInView.y
My.cellSnapshot!.center = center
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context: NSManagedObjectContext = appDel.managedObjectContext!
var fetchRequest = NSFetchRequest(entityName: currentListEntity)
let sortDescriptor = NSSortDescriptor(key: "displayOrder", ascending: true )
fetchRequest.sortDescriptors = [ sortDescriptor ]
//If the indexPath is not 0 AND is not the same as it began (didn't move)...
//Update array and table row order
if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {
swap(&taskList_Cntxt[indexPath!.row], &taskList_Cntxt[Path.initialIndexPath!.row])
tableView.moveRowAtIndexPath(Path.initialIndexPath!, toIndexPath: indexPath!)
toolBox.updateDisplayOrder()
context.save(nil)
Path.initialIndexPath = indexPath
}
}
default:
if My.cellSnapshot != nil && indexPath != nil {
//Runs continuously while a long press is recognized (I think)
//Animates cell movement
//Completion block:
//Removes snapshot of cell, cleans everything up
let cell = tableView.cellForRowAtIndexPath(Path.initialIndexPath!) as UITableViewCell!
cell.hidden = false
cell.alpha = 0.0
UIView.animateWithDuration(0.25, animations: { () -> Void in
My.cellSnapshot!.center = cell.center
My.cellSnapshot!.transform = CGAffineTransformIdentity
My.cellSnapshot!.alpha = 0.0
cell.alpha = 1.0
}, completion: { (finished) -> Void in
if finished {
Path.initialIndexPath = nil
My.cellSnapshot!.removeFromSuperview()
My.cellSnapshot = nil
}
})//End of competion block & end of animation
}//End of 'if nil'
}//End of switch
}//End of longPressGestureRecognized
Potential Culprit
My guess is that the issue is related to the cell being unable to get coordinates once it is below the last cell. It isn't really floating, it is constantly setting its location in relation to the other cells. I think the solution will be an if-statement that does something magical when there's no cell to reference for a location. But what!?! Adding a nil check to each case isn't working for some reason.
Clearly Stated Question
How do I avoid crashes and handle an event where my dragged cell is dragged below the last cell?
Screenshot of crash:
The Ugly
It seems that you simply need to do a preemptive check, to ensure your indexPath is not nil:
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
if (indexPath != nil) {
//Move your code to this block
}
Hope that helps!
You don't state where in the code the crash occurs, which makes it harder to determine what is going on. Set a breakpoint on exceptions to determine which line is the culprit. To do that, use the '+' in the bottom-left corner of the breakpoint list in XCode.
The main issue I think is with the indexPath. There are a couple of issues:
You are using the indexPath even though it might be nil, in this line:
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
The indexPath can be invalid, even though it is not nil. Check for its section and row members to be different from NSNotFound.
Finally, I have been using a pre-made, open source, UITableView subclass that does all the moving for you, so you don't have to implement it yourself anymore. It also takes care of autoscrolling, which you have not even considered yet. Use it directly, or use it as inspiration for your code:
https://www.cocoacontrols.com/controls/fmmovetableview
Good morning everyone,
I am a newbie Swift developer and I am facing the following problem implementing an exercise I am dealing with.
I have a collection view with collection cells displaying images I have imported in XCODE; when I tap with the finger on the screen I would like to replace the image currently being display with another one that i have also imported, and animate this replacement.
I am implementing the UITapLongPressureRecognizer method but i am getting confused on which state to implement for the recognizer, just to replace the first image view with the one I want to be shown when I tap the screen to scroll up-down.
As you can see from the code below the two recognizer I think should be more appropriate to be implemented are the "Begin" and "Ended".
My problem is that when the state .Begin starts I don't know how to animate the replacement of one image view with another and so when the state is .Ended I don't know how to replace the second image with the first one and animate the replacement (I want it to be like for example a Modal segue with "Cross Dissolve" transition).
Thank you in advance for your kindness and patience.
class MainViewController: UICollectionViewController {
var fashionArray = [Fashion]()
private var selectedIndexPath: NSIndexPath?
override func viewDidLoad() {
super.viewDidLoad()
selectedIndexPath = NSIndexPath()
//Register the cell
let collectionViewCellNIB = UINib(nibName: "CollectionViewCell", bundle: nil)
self.collectionView!.registerNib(collectionViewCellNIB, forCellWithReuseIdentifier: reuseIdentifier)
//Configure the size of the image according to the device size
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let bounds = UIScreen.mainScreen().bounds
let width = bounds.size.width
let height = bounds.size.height
layout.itemSize = CGSize(width: width, height: height)
let longPressRecogn = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
collectionView!.addGestureRecognizer(longPressRecogn)
}
func handleLongPress(recognizer: UILongPressGestureRecognizer){
var cell: CollectionViewCell!
let location = recognizer.locationInView(collectionView)
let indexPath = collectionView!.indexPathForItemAtPoint(location)
if let indexPath = indexPath {
cell = collectionView!.cellForItemAtIndexPath(indexPath) as! CollectionViewCell
}
switch recognizer.state {
case .Began:
cell.screenTapped = false
case .Ended:
cell.screenTapped = false
default:
println("")
}
}
First of all, I suggest you to use UITapGestureRecognizer instead of long press. Because, as far as I understand, you only tap once instead of pressing for a time.
let tapRecognizer = UITapGestureRecognizer(target: self, action:Selector("tapped:"))
collectionView.addGestureRecognizer(tapRecognizer)
And when the user tapped, you can use UIView animations to change the image. You can check the Example II from the following link to get insight about animations.
http://www.appcoda.com/view-animation-in-swift/
So I have seen a lot of posts about reordering cells that pertain to using "edit mode", but none for the problem I have. (Excuse me if I am wrong).
I am building a ranking app, and looking for a way to use a long gesture recognizer to reorder the cells in my UITableView. Essentially a user will be able to reorder and "Rank" the cells full of strings in a group with their friends.
I would go the standard route of using an "edit" bar button item in the nav bar, but I am using the top right of the nav bar for adding new strings to the tableview already. (The following image depicts what I mean).
So far, I have added `
var lpgr = UILongPressGestureRecognizer(target: self, action: "longPressDetected:")
lpgr.minimumPressDuration = 1.0;
tableView.addGestureRecognizer(lpgr)`
to my viewDidLoad method, and started creating the following function:
func longPressDetected(sender: AnyObject) {
var longPress:UILongPressGestureRecognizer = sender as UILongPressGestureRecognizer
var state:UIGestureRecognizerState = longPress.state
let location:CGPoint = longPress.locationInView(self.tableView) as CGPoint
var indexPath = self.tableView.indexPathForRowAtPoint(location)?
var snapshot:UIView!
var sourceIndexPath:NSIndexPath!
}
All of the resources I have scowered for on the internet end up showing me a HUGE, LONG list of additives to that function in order to get the desired result, but those examples involve core data. It seems to me that there must be a far easier way to simply reorder tableview cells with a long press?
Dave's answer is great.
Here is the swift 4 version of this tutorial:
WayPointCell is your CustomUITableViewCell and wayPoints is the dataSource array for the UITableView
First, put this in your viewDidLoad, like Alfi mentionend:
override func viewDidLoad() {
super.viewDidLoad()
let longpress = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureRecognized(gestureRecognizer:)))
self.tableView.addGestureRecognizer(longpress)
}
Then implement the method:
func longPressGestureRecognized(gestureRecognizer: UIGestureRecognizer) {
let longpress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longpress.state
let locationInView = longpress.location(in: self.tableView)
var indexPath = self.tableView.indexPathForRow(at: locationInView)
switch state {
case .began:
if indexPath != nil {
Path.initialIndexPath = indexPath
let cell = self.tableView.cellForRow(at: indexPath!) as! WayPointCell
My.cellSnapShot = snapshopOfCell(inputView: cell)
var center = cell.center
My.cellSnapShot?.center = center
My.cellSnapShot?.alpha = 0.0
self.tableView.addSubview(My.cellSnapShot!)
UIView.animate(withDuration: 0.25, animations: {
center.y = locationInView.y
My.cellSnapShot?.center = center
My.cellSnapShot?.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
My.cellSnapShot?.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
cell.isHidden = true
}
})
}
case .changed:
var center = My.cellSnapShot?.center
center?.y = locationInView.y
My.cellSnapShot?.center = center!
if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {
self.wayPoints.swapAt((indexPath?.row)!, (Path.initialIndexPath?.row)!)
//swap(&self.wayPoints[(indexPath?.row)!], &self.wayPoints[(Path.initialIndexPath?.row)!])
self.tableView.moveRow(at: Path.initialIndexPath!, to: indexPath!)
Path.initialIndexPath = indexPath
}
default:
let cell = self.tableView.cellForRow(at: Path.initialIndexPath!) as! WayPointCell
cell.isHidden = false
cell.alpha = 0.0
UIView.animate(withDuration: 0.25, animations: {
My.cellSnapShot?.center = cell.center
My.cellSnapShot?.transform = .identity
My.cellSnapShot?.alpha = 0.0
cell.alpha = 1.0
}, completion: { (finished) -> Void in
if finished {
Path.initialIndexPath = nil
My.cellSnapShot?.removeFromSuperview()
My.cellSnapShot = nil
}
})
}
}
func snapshopOfCell(inputView: UIView) -> UIView {
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
inputView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
let cellSnapshot : UIView = UIImageView(image: image)
cellSnapshot.layer.masksToBounds = false
cellSnapshot.layer.cornerRadius = 0.0
cellSnapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
cellSnapshot.layer.shadowRadius = 5.0
cellSnapshot.layer.shadowOpacity = 0.4
return cellSnapshot
}
struct My {
static var cellSnapShot: UIView? = nil
}
struct Path {
static var initialIndexPath: IndexPath? = nil
}
Give this tutorial a shot, you'll likely be up and running within 20 minutes:
Great Swift Drag & Drop tutorial
It's easy. I've only been developing for 3 months and I was able to implement this. I also tried several others and this was the one I could understand.
It's written in Swift and it's practically cut and paste. You add the longPress code to your viewDidLoad and then paste the function into the 'body' of your class. The tutorial will guide you but there's not much more to it.
Quick explanation of the code: This method uses a switch statement to detect whether the longPress just began, changed, or is in default. Different code runs for each case. It takes a snapshot/picture of your long-pressed cell, hides your cell, and moves the snapshot around. When you finished, it unhides your cell and removes the snapshot from the view.
Warning: My one word of caution is that although this drag/drop looks great and works close to perfectly, there does seem to be an issue where it crashes upon dragging the cell below the lowest/bottom cell.
Drag & Drop Crash Problem
Since iOS 11 this can be achieved by implementing the built in UITableView drag and drop delegates.
You will find a detailed description of how to implement them in this answer to a similar question:
https://stackoverflow.com/a/57225766/10060753