UITableView single-gesture row swipe actions - ios

I'm trying to implement single-gesture swiping actions in my UITableView, where you swipe to reveal action buttons, and continue swiping all the way across to activate the default action (such as "Delete" or "Mark as Read" in Mail).
I'm implementing -tableView:editActionsForRowAtIndexPath: in my delegate. My button appears, and it works when I tap it, but it doesn't get activated by a full swipe.
I've tried it with and without -tableView:commitEditingStyle:forRowAtIndexPath: and -tableView:canEditRowAtIndexPath: in my data source. Only the latter gets called, but neither seems to make a difference.
Is there something more I need to do? Or is this behavior not actually achievable using the standard API?

I would check to see if the swipe has past a certain threshold in your gesture recognizer's target function.
Try this
func swipeRight(sender: UIGestureRecognizer){
let threshold : CGFloat = 100;
var startLocation = CGPoint()
if (sender.state == UIGestureRecognizerState.Began){
startLocation = sender.locationInView(self.view)
}else if (sender.state == UIGestureRecognizerState.Ended){
let stopLocation = sender.locationInView(self.view)
let distanceX = stopLocation.x - startLocation.x
let distanceY = stopLocation.y - startLocation.y
let distance = sqrt(distanceX*distanceX + distanceY*distanceY )
if (distance > threshold ){
//Delete or Edit Row Here
}
}
}

Related

iOS: dragging the copy of a button

I'm not sure whether this has been asked or not, but I failed to find a solution. I'm implementing panning gesture on a button, but the idea is: the button is fixed to a position, and when the user drags it, a copy of the button is created and moving with the gesture; the original one stays at its initial place (so there'll be 2 buttons in the view). When the panning ends, the new button is used for some processing, and after that it should disappear (the original one stays as it is; so this whole process can repeat). Currently what I have is as below:
private func addPanGesture() {
for btn in self.selectors { //selectors is a list of buttons which needs this gesture
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGesturerecognizer(pan)
}
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
var translation = panGesture.translation(in: view)
panGesture.setTranslation(CGPoint(x: 0, y: 0), in: view)
var newButton = UIButton()
if let initButton = panGesture.view as? UIButton {
print ("Button recognized!") // this msg is printed out
newButton.center = CGPoint(x: initButton.center.x + translation.x, y: initButton.center.y + translation.y)
newButton.setImage(UIImage(named: "somename"), for: .normal)
}
if panGesture.state == UIGestureRecognizerState.began {
self.view.addSubview(newButton)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
}
if panGesture.state == UIGestureRecognizerState.changed {
self.view.addSubview(newButton)
}
// printed-out msgs show began, ended, changed states have all been reached
}
But the new button doesn't show up in my view. May I know how to solve this?
You need to create and add the new button as a subview only on .began and remove it on .ended.
Therefore you need to keep a reference to the new button.
You are setting the new button's center but not it's size. You might set its .frame.
You do not need to set a translation to the pan gesture. When you get var translation = panGesture.translation(in: view) you get everything you need.
I have wrote the below code for only one button, but if you are going to allow simultaneous dragging of buttons, you would need to keep a list of moving buttons instead of var movingButton: UIButton?
private func addPanGesture() {
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGestureRecognizer(pan)
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
let initButton = panGesture.view as! UIButton
if panGesture.state == UIGestureRecognizerState.began {
// this is just copying initial button
// this might be overkill
// just make sure you set the frame, title and image of the new button correctly
let initButtonData = NSKeyedArchiver.archivedData(withRootObject: initButton)
let newButton = NSKeyedUnarchiver.unarchiveObject(with: initButtonData) as! UIButton
// we store new button's reference since we will just move it while it is added to view
movingButton = newButton
self.view.addSubview(movingButton!)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
// when we are done just we just remove it from superview
movingButton!.removeFromSuperview()
}
if panGesture.state == UIGestureRecognizerState.changed {
// at any change, all we need to do is update movingButton's frame
var buttonFrame = initButton.frame;
buttonFrame.origin = CGPoint(x: buttonFrame.origin.x + translation.x, y: buttonFrame.origin.y + translation.y)
movingButton!.frame = buttonFrame
}
}
Hard to say without debugging it, but a few things I see:
You create a new button every time through panDetected, and add it to the view each time. You should only create an add the button in the .began state.
You should use init(frame:) to create your button, and initialize it to the size of the image.
It looks like you're attaching the pan gestures to the buttons. Then you get the pan coordinates in the button's coordinate system, which doesn't make sense. You should be converting the pan gesture to the button's superview's coordinate system, and should not be calling setTranslation except when the pan gesture's state is .began.
You should be setting the button's coordinates to the new location of the pan gesture each time you get a 1st.changed` message.

Change UISlider value with swipe gesture swift

I have a UIView with a swipe gesture .
let swipeUpGesture = UISwipeGestureRecognizer(target: self, action: #selector(NextStepCaptureVC.handleSwipeUp(gesture:)))
swipeUpGesture.direction = .up
view.addGestureRecognizer(swipeUpGesture)
func handleSwipeUp(gesture: UISwipeGestureRecognizer) {
print("Swipe Up")
heightSlider.setValue(20, animated: true)
}
When I try to change the value it works but the value jump from 0 to 20. I want the value to change continuously while swiping. How can I do it?
Judging from your code, it looks like you are trying to make 'panning up and down' on the screen translate to the UISlider value changing.
As already mentioned by others, first thing is to change your UISwipeGestureRecognizer to a UIPanGestureRecognizer
let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(gesture:)))
view.addGestureRecognizer(pan)
Then in your pan function, you need to update the slider value based on how much the user has panned.
func pan(gesture: UIPanGestureRecognizer) {
// The amount of movement up/down since last change in slider
let yTranslation = gesture.translation(in: gesture.view).y
// The slide distance needed to equal one value in the slider
let tolerance: CGFloat = 5
if abs(yTranslation) >= tolerance {
let newValue = heightSlider.value + Float(yTranslation / tolerance)
heightSlider.setValue(newValue, animated: true)
// Reset the overall translation within the view
gesture.setTranslation(.zero, in: gesture.view)
}
}
Simply adjust the tolerance variable to make the user swipe more/less in order to adjust the slider value.
You don't want a UISwipeGestureRecognizer, you want a UIPanGestureRecognizer. A swipe is a one-time gesture.
Apple's documentation says "A swipe is a discrete gesture, and thus the associated action message is sent only once per gesture."
You set up a main action from your gesture recognizer to your code (You can use interface builder for that)
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.Began {
} else if recognizer.state == UIGestureRecognizerState.Ended {
} else if recognizer.state == UIGestureRecognizerState.Changed {
}
}
Good luck! =]

Detect taps but also drags for cell

I have a UICollectionView in which I've enabled the dragging of items. But I also need to be able to detect taps on the items.
To detect tapping, I simply use didSelectItemAtIndex
To detect dragging, I've added a UILongPressGestureRecognizer to the collectionView and find the cell from the location of that long press:
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(OrganizeWidgetsViewController.handleLongGesture(gesture:)))
longPressGesture.numberOfTapsRequired = 0
longPressGesture.minimumPressDuration = 0.01
widgetCollectionView.addGestureRecognizer(longPressGesture)
The problem is that I need dragging to occur instantly when the user's finger touches the screen and starts dragging. However, the low minimumPressDuration of my longPressGestrue (which is 0.01) prevents taps from being detected.
My longPressGesture get detected, but the tap usually does not. Is there a better way to detect both tapping and enable dragging of a cell?
I solved this by setting the longPressGesture.minimumPressDuration to 0, and then check how far away the user dragged from the origin of the tap.
If the drag's max distance away from the origin is greater than a certain amount, I recognize it as a drag. Else, it's a tap.
Step 1: Implement the longPressGesture to the collectionView:
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(MyViewController.handleLongGesture(gesture:)))
longPressGesture.numberOfTapsRequired = 0
longPressGesture.minimumPressDuration = 0
myCollectionView.addGestureRecognizer(longPressGesture)
Step 2: Declare two variables on the class to calculate the distance of the drag
var startPoint: CGPoint?
var maxDistance: CGFloat?
Step 3 Write a function that will calculate the total distance away from the origin of the drag (we'll use this method in the next step)
func calculateDistance(from: CGPoint, to: CGPoint) -> CGFloat {
return sqrt(pow((from.x - to.x),2) + pow((from.y - to.y),2))
}
Step 4: Handle the drag
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.began:
startPoint = gesture.location(in: myCollectionView)
maxDistance = 0
guard let selectedIndexPath = myCollectionView.indexPathForItem(at: gesture.location(in: myCollectionView)) else {
break
}
myCollectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
maxDistance = max(maxDistance!, calculateDistance(from: startPoint!, to: gesture.location(in: myCollectionView)))
myCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
if maxDistance! < CGFloat(10) {
if let selectedIndexPath = myCollectionView.indexPathForItem(at: gesture.location(in: myCollectionView)) {
collectionView(myCollectionView, didSelectItemAt: selectedIndexPath)
}
}
myCollectionView.endInteractiveMovement()
default:
myCollectionView.cancelInteractiveMovement()
}
}
Note:
We call didSelectItemAtIndex on our collectionView from within step 4, so make sure whatever functionality you want to occur on a tap goes within there.

Draggable UIButton/Elements in Swift?

this is my first question! I was just wondering, in Swift (specifically Swift 2, although that may go without saying!), how you create a button that the user can drag around. So for example, if it is a UIButton, the user can tap and hold it, and when they move their finger, the UIButton moves with it, and when they release it, it remains in the position that the user left it. Potentially there could be a snapping system but this is unimportant for now.
I've searched StackOverflow and found some quite interesting things, however it's all for Objective-C, and although Swift is pretty similar in some respects, I can't figure out in the slightest as to how to implement this in Swift.
It would be massively appreciated for a project that I am working on!
Thank you very much!
You can implement UIPanGestureRecognizer on your UIButton.
Wherever you create your button (viewDidLoad if using outlets):
let pan = UIPanGestureRecognizer(target: self, action: "panButton:")
button.addGestureRecognizer(pan)
This creates a new pan gesture recognizer and adds it to the button. Now, you'll want to implement the pan's action. First, you need to store the center of the button to be able to reset it when you finish panning. Add this as a view controller property:
var buttonCenter = CGPointZero
Then you implement the pan action. Note that you can use gesture recognizer states to determine when the pan starts and ends:
func panButton(pan: UIPanGestureRecognizer) {
if pan.state == .Began {
buttonCenter = button.center // store old button center
} else if pan.state == .Ended || pan.state == .Failed || pan.state == .Cancelled {
button.center = buttonCenter // restore button center
} else {
let location = pan.locationInView(view) // get pan location
button.center = location // set button to where finger is
}
}
Swift 4 & 5 Version of accepted answer:
var buttonCenter: CGPoint = .zero
viewDidLoad() {
super.viewDidLoad()
let pan = UIPanGestureRecognizer(target: self, action: #selector(YourViewController.panButton(pan:)))
button.addGestureRecognizer(pan)
}
#objc func panButton(pan: UIPanGestureRecognizer) {
if pan.state == .began {
buttonCenter = button.center // store old button center
} else if pan.state == .ended || pan.state == .failed || pan.state == .cancelled {
button.center = buttonCenter // restore button center
} else {
let location = pan.location(in: view) // get pan location
button.center = location // set button to where finger is
}
}
Basically, you want to implement a touch gesture recognizer and set the button's center to the center of your press when you tap/move said button.
Here's how you'll want to do that: https://stackoverflow.com/a/31487087/5700898
Also, really cool idea!

Pan Gesture - Swipe Gesture Conflict

I’m trying to create an application which duplicates the ability of Apple’s Photos app (iPhone) to zoom, pan and scroll through photographic images. (I also want to use the same controls when viewing pdfs and other documents.) I got the tap gesture to show/hide the navigation bar and the swipe gesture to scroll through the images from left to right & vice versa. Then I got the pinch gesture to zoom in and out, but when I added the pan gesture to move around within a zoomed image, the swipe gesture quit working.
I found potential solutions elsewhere on StackOverflow including the use of shouldRecognizeSimultaneouslyWithGestureRecognizer, but so far I have not been able to resolve the conflict. Any suggestions?
Here's the code:
func gestureRecognizer(UIPanGestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer UISwipeGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#IBAction func handlePinch(sender: UIPinchGestureRecognizer) {
sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
sender.scale = 1
}
#IBAction func handlePan(sender: UIPanGestureRecognizer) {
self.view.bringSubviewToFront(sender.view!)
var translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x + translation.x, sender.view!.center.y + translation.y)
sender.setTranslation(CGPointZero, inView: self.view)
}
#IBAction func handleSwipeRight(sender: UISwipeGestureRecognizer) {
if (self.index == 0) {
self.index = ((photos.count) - 1);
}
else
{
self.index--;
}
// requireGestureRecognizerToFail(panGesture)
setImage()
}
You do not want shouldRecognizeSimultaneouslyWithGestureRecognizer: (which allows two gestures to happen simultaneously). That's useful if you want to, for example, simultaneously pinch and pan. But the simultaneous gestures will not help in this scenario where you are panning and swiping at the same time. (If anything, recognizing those simultaneously probably confuses the situation.)
Instead, you might want to establish precedence of swipe and pan gestures (e.g. only pan if swipe fails) with requireGestureRecognizerToFail:.
Or better, retire the swipe gesture entirely and use solely the pan gesture, which, if you're zoomed out will be an interactive gesture to navigate from one image to the next, and if zoomed in, pans the image. Interactive pan gestures generally a more satisfying UX, anyway; e.g., if swiping from one photo to the next, be able to stop mid pan gesture and go back. If you look at the Photos.app, it's actually using a pan gesture to swipe from one image to another, not a swipe gesture.
I discovered a tutorial at http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift that does a great job of introducing UIScrollView as a way of combining zooming, panning and paging in Swift. I recommend it for anyone trying to learn how to make these gestures work well together.
In similar case I've used another approach: extended pan gesture to support swipe:
// in handlePan()
switch recognizer.state {
struct Holder {
static var lastTranslate : CGFloat = 0
static var prevTranslate : CGFloat = 0
static var lastTime : TimeInterval = 0
static var prevTime : TimeInterval = 0
}
case .began:
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
//perform appropriate pan action
case .changed:
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
//perform appropriate pan action
case .ended ,.cancelled:
let seconds = CGFloat(Date.timeIntervalSinceReferenceDate) - CGFloat(Holder.prevTime)
var swipeVelocity : CGFloat = 0
if seconds > 0 {
swipeVelocity = (translation.y - Holder.prevTranslate)/seconds
}
var shouldSwipe : Bool = false
if Swift.abs(swipeVelocity) > velocityThreshold {
shouldSwipe = swipeVelocity < 0
}
if shouldSwipe {
// perform swipe action
} else {
// perform appropriate pan action
}
default:
print("Unsupported")
}
All you need to do is to find appropriate velocityTreshold for your swipe gesture

Resources