I have a CollectionView, the cell in CollectionView has size equal to the screen (CollectionView has paging enable mode).
I want to press long on the screen, then the CollectionView will scroll to the next cell.
For example:
I need 1 second to make the CollectionView scroll to the next cell,
and I press for 2,5 seconds.
The begining time: I am starting long press on the screen and the collection view is now on the first cell.
After the first second: It will scroll to the second cell.
After the second second: It will scroll to the third cell.
The last half second: It still stand on the third cell (because half second is not enough time to make the collection view scroll to the next cell).
I have added the UILongPressGestureRecognizer to the cell and I have tried like this:
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Ended {
let p = longGesture.locationInView(self.collectionView)
let indexPath = self.collectionView.indexPathForItemAtPoint(p)
if let indexPath = indexPath {
let row = indexPath.row + 1
let section = indexPath.section
if row < self.photoData.count {
self.collectionView.selectItemAtIndexPath(NSIndexPath(forRow: row, inSection: section), animated: true, scrollPosition: .Right)
}
print(indexPath.row)
} else {
print("Could not find index path")
}
}
}
But I always have to END the long gesture to make the collection view scroll.
What you seem to want is something that kicks off a timer that fires every 1 second while the finger is down. I'd probably make a function:
func scrollCell() {
if (longPressActive) {
//scroll code
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
scrollCell() // function calls itself after a second
})
}
}
That you could control in your handleLongPress code:
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Began {
longPressActive = true
scrollCell()
} else if longGesture.state == .Ended || longGesture.state == .Canceled {
longPressActive = false
}
}
So, when the long press gesture first fires, it sets a bool (longPressActive), and then calls the scroll function. When the scroll function completes, it calls itself again. If the gesture ever finalizes, it will clear the longPressActive bool, so if the timer fires, the bool will be false and it won't scroll.
More ideally I'd probably not use a long press gesture recognizer and just track the touches myself, as I could reference the touch and check its state instead of use a boolean. Also, there is probably a fun bug involved with the dispatch when it goes into the background.
Here is the way I have tried:
First, I add these properties to my Controller:
var counter = 0
var timer = NSTimer()
var currentIndexPath: NSIndexPath?
Then I count up the counter whenever longGesture.state == .Began
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Began {
let point = longGesture.locationInView(self.collectionView)
currentIndexPath = self.collectionView.indexPathForItemAtPoint(point)
self.counter = 0
self.timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ODIProfileAlbumMode4TableViewCell.incrementCounter), userInfo: nil, repeats: true)
} else if longGesture.state == .Ended {
self.timer.invalidate()
}
}
func incrementCounter() {
self.counter += 1
print(self.counter)
if let indexPath = currentIndexPath {
let section = indexPath.section
if self.counter < self.photoData.count {
self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: self.counter, inSection: section), atScrollPosition: .Right, animated: true)
} else {
self.counter = 0
self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: 0, inSection: section), atScrollPosition: .Right, animated: true)
}
} else {
print("Could not find index path")
}
}
It works perfectly now. :)
Related
I am coding a quiz app. As you can see in the storyboard, there are 2 tableviews. In the first one you select the type of the exam and in the second one you see the question. The way I designed the app is, there are 2 different kind of cells. Question and Multiple Choice.
The first cell of the 2nd tableView is the question cell and the remaining 4 are multipleChoice cells.(You will see 6 cells in the storyboard.In the last one I am trying to show ads.But that's irrelevant to this question.So think of it as 5 cells in total)
If the selected answer is right, I set the selected cell's contentView background color to green. If it is wrong, I set the selected cell's contentView background color to red and make the correct answer blink in green by using an extension.Here is the extension...
extension UIView{
func blink() {
self.alpha = 0.2
self.backgroundColor = .green
UIView.animate(withDuration: 0.15, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse], animations: {self.alpha = 1.0}, completion: nil)
}
func stopBlink() {
self.backgroundColor = .clear
layer.removeAllAnimations()
alpha = 1
}
}
And here is the code that achieves this
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.row)
if indexPath.row == dogruCevapInt && isCompleted == false && sonrakiVCyeAktarilacakIsTimeUp == false {
// The answer is right
print("Doğru Cevap")
self.tableView.allowsSelection = false
dogrular = dogrular + 1
updateNavigationItemTitle()
if let dogruCell = self.tableView.cellForRow(at: indexPath) as? SecenekTableViewCell {
let originalBGColor = dogruCell.contentView.backgroundColor
dogruCell.contentView.backgroundColor = .green
playSound(soundState: SettingsSingleton.shared.isSoundOn!,soundName: "correct", fileExtension: "mp3")
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { (newTimer) in
dogruCell.contentView.backgroundColor = originalBGColor
self.nextQuestion()
})
}
}else if indexPath.row != 0 && isCompleted == false && sonrakiVCyeAktarilacakIsTimeUp == false {
// The answer is wrong
print("Yanlış Cevap. Doğrusu \(dogruCevap)")
self.tableView.allowsSelection = false
yanlislar = yanlislar + 1
updateNavigationItemTitle()
if let yanlisCell = self.tableView.cellForRow(at: indexPath) as? SecenekTableViewCell {
let originalBGColor = yanlisCell.contentView.backgroundColor
yanlisCell.contentView.backgroundColor = .red
playSound(soundState: SettingsSingleton.shared.isSoundOn!,soundName: "wrong", fileExtension: "wav")
self.tableView.scrollToRow(at: IndexPath(row: self.dogruCevapInt, section: indexPath.section), at: .bottom, animated: true)
if let dogruCell = self.tableView.cellForRow(at: IndexPath(row: dogruCevapInt, section: indexPath.section)) {
dogruCell.contentView.backgroundColor = .green
dogruCell.blink()
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { (newTimer) in
dogruCell.stopBlink()
yanlisCell.contentView.backgroundColor = originalBGColor
self.nextQuestion()
dogruCell.contentView.backgroundColor = .clear
})
}else {
//The right answer is currently not visible
//We cannot make it blink let's at least go to the next question
self.tableView.allowsSelection = false
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { (newTimer) in
self.nextQuestion()
})
}
}
}else if isCompleted == true {
print("Is Completed true")
}else {
//If the user selects the question
self.tableView.deselectRow(at: indexPath, animated: false)
}
}
and now the problem...
Everything works perfect if all the multiple choice cells are currently visible(or at least some part of it is visible)on the screen. But if the cell that holds the right answer is not visible, I cannot make it blink no matter what I try. How do I do this?
I recorded a video and converted it into animated gif. Please see the 5th question to see what I mean.
Screen Recording of the problem
I have currently created an array of images [1 - 8]. I have also created a collectionview that pulls images out of the imageArray and puts images in the imageview which is in the cell. Please note that the cell has one imageView in it and it takes up the whole screen, horizontal scrolling and paging are both enabled.
Right now with the current code I have, is very odd what is currently happening so I will tell you what is currently going on. So what currently is happening (I'm going by image 1 which index 0) if the image is on 2 (index 1) and then you swipe next to 3 (index 2), it skips image 3 (index 2) and 4 (index 3) and sits on image 5 (index 4), so when I mean skips, I mean it slides past the image you just swiped to and one more.
(Oddly it deletes image 1 and 2) once on 5. I believe this is due to the it is updating the index or setting it to 2 over again because it just deleted 0. I know this might be hard to get but just think of scrollview with paging and when you swipe to the third image, it skips the one your on and one more and slides you to image 5 where it stays place.
So far thanks to some of you, I have came up with this code below, but I am hoping someone will be able to solve this awful mess.
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
}
if currentImage > 1 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let visibleCells = myCollectionView.visibleCells
if visibleCells.first != nil {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
let indexPathOfLastItem = (indexPath.item) - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: (indexPath.item) - 1)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
}
}
}
}
}
These codes will delete a specific item using the item index. This is working great!
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var visibleRect = CGRect()
visibleRect.origin = myCollectionView.contentOffset
visibleRect.size = myCollectionView.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
let visibleIndexPath: IndexPath = myCollectionView.indexPathForItem(at: visiblePoint)!
print(visibleIndexPath)
fruitArray.remove(at: visibleIndexPath.row )
myCollectionView.deleteItems(at: [visibleIndexPath])
myCollectionView.scrollToItem(at: IndexPath.init(row: visibleIndexPath.row-1, section: 0), at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
}
Use scrollViewDidEndDragging to detect when user stops scrolling and delete the cell,..
Objective C
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if (decelerate == NO) {
[self deleteCell];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self deleteCell];
}
- (void)deleteCell {
NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))]; // if you want middle cell
NSIndexPath *firstVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:0]; // if you want first visible cell
NSIndexPath *lastVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:[self.tableView indexPathsForVisibleRows].count]; // last cell in visible cells
myCollectionView.beginUpdates() //if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems(at: [lastVisibleIndexPath])
myCollectionView.endUpdates()
}
Swift 3.0
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if decelerate == false {
deleteCell()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
deleteCell()
}
func deleteCell() {
var pathForCenterCell: IndexPath? = tableView.indexPathForRow(at: CGPoint(x: CGFloat(tableView.bounds.midX), y: CGFloat(tableView.bounds.midY)))
// if you want middle cell
var firstVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[0] as? IndexPath)
// if you want first visible cell
var lastVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[tableView.indexPathsForVisibleRows?.count] as? IndexPath)
// last cell in visible cells
myCollectionView.beginUpdates()()
//if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems
}
Happy coding
I'm currently using automatic scroll to bottom in UITableView, but there is a bug to solve:
If I press in any location of my UITableView before the scroll is executed that stop to work. (I cannot scroll to bottom before the view is displayed because I retrieve async data)
So the first thing I thought is tho disable the userInteraction like this:
override func viewDidLoad() {
super.viewDidLoad()
self.view.userInteractionEnabled = false
...
}
And to unlock it here:
func tableViewScrollToBottom(animated: Bool) {
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.tableView.numberOfSections
let numberOfRows = self.tableView.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: animated)
}
self.view.userInteractionEnabled = true
})
}
But (it's really strange) is not working, yes the view is disabled (I've tried even to lock the UITableView) and I can not scroll the UITableView but it seems that it can retrieve anyway the touch.
I'm really close to figuring this out. So in the iOS mail app when you click on the two arrow keys it takes you to the previous/next mail. Its on the top right
I've managed to pass the indexPath value to my second viewcontroller and print in in the console. I can also increase and decrease from it.
if segue.identifier == "DetailVC" {
let detailVC = segue.destination as! DetailVC
let indexPath = self.collectionViewIBO.indexPathsForSelectedItems?.last!
detailVC.index = indexPath
}
EDIT
This is where I'm pulling the data from. It reads the values from my model. I cannot assign an indexPath to it however. I can only do that from the previous view controller
var monster: Monsters!
I've attempted to implement the "previous" functionality using this code. My view styling are in the displayDataForIndexPath() function and the function is called from my view will appear
#IBAction func monsPreviousIBO(_ sender: Any) {
self.index = IndexPath(row: self.index.row - 1, section: self.index.section)
displayDataForIndexPath()
}
But all it does is decrease the IndexPath. For some reason the data doesn't actually reload with my function. I'm missing some important puzzle piece here to achieving the same functionality.
EDIT The code in my displayDataForIndex is as follows
func displayDataForIndexPath() {
if index.row == 0 {
self.monsPreviousIBO.removeFromSuperview()
}
var monsterName = (String(format: "%03d", monster.speciesId!))
self.navigationItem.title = monster.name!
let gif = UIImage(gifName: monsterName)
self.gifIBO.setGifImage(gif, manager: gifManager)
gifIBO.contentMode = .center
guard monster.legendary! != true else {
// Value requirements not met, do something
monsterStatusLegend()
return
}
guard monster.subLegend! != true else {
// Value requirements not met, do something
monsterStatusSub()
return
}
guard monster.isMega! != true else {
// Value requirements not met, do something
monsterStatusMega()
return
}
}
you display all data depending on monster but you never change the monster depending which indexPath you used.
add some code to populate the monster from indexPath
monster = getMonster(index.row)
or in your case
#IBAction func monsPreviousIBO(_ sender: Any) {
self.index = IndexPath(row: self.index.row - 1, section: self.index.section)
monster = mons[self.index.row]
displayDataForIndexPath()
}
or better in the displayDataForIndexPath() add this line:
func displayDataForIndexPath() {
if index.row == 0 {
self.monsPreviousIBO.removeFromSuperview()
}
monster = mons[self.index.row]
//....
NOTE some suggestions:
i would change the line for the button then it is enabled if the indexpath gets >0:
self.monsPreviousIBO.isEnabled = (index.row != 0)
just save the row as monsterIndex = indexPath.row and then deal only with the index and not indexPath.
you don't need to save the current monster as monster if you use the monster only in displayDataForIndexPath - then you can get the current monster just there and have it a local variable in this function:
var monster = mons[self.index.row]
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