iOS: dragging the copy of a button - ios

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.

Related

How to drag a button from a popover across whole app? (swift)

In my app, I am displaying a popover that shows a custom UIView allowing the user to select a color using various sliders. I also want to implement an 'eyedropper' tool that the user can tap and hold then drag around to choose a color from anything visible in the app. Inside my custom UIView I added a UIPanGestureRecognizer to my button that points to the handlePan method:
var eyedropperStartLocation = CGPoint.zero
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// self is a custom UIView that contains my color selection
// sliders and is placed inside a UITableView that's in
// the popover.
let translation = recognizer.translation(in: self)
if let view = recognizer.view {
switch recognizer.state {
case .began:
eyedropperStartLocation = view.center
case .ended, .failed, .cancelled:
view.center = eyedropperStartLocation
return
default: break
}
view.center = CGPoint(x: view.center.x + translation.x,
y: view.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}
}
I can drag the button around and it changes location, however I have two issues:
The eyedropper button isn't always in front of other items, even inside the popover or the custom UIView inside the popover
The eyedropper button disappears when outside the bounds of the popover
How can I get the button to be visible all the time, including outside the popover? I'll want to detect where the user lets go of it within the app so I can determine what color it was on.
I figured out how to do this so I'll answer my own question. Instead of moving around the view/button that's inside the popup, I create a new UIImageView and add it to the application's Window, letting it span the whole application. The original button stays where it is - you could easily change the state on it to make it look different, or hide it if you wanted to.
You could also use Interface Builder to tie to #IBActions, but I just did everything in code. The clickButton method kicks things off but calculating location in the window and putting it on the screen. The handlePan method does the translation and lets you move it around.
All code below is swift 4 and works in XCode 9.4.1 (assuming I didn't introduce any typos):
// Call this from all your init methods so it will always happen
func commonInit() {
let panner = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
theButton.addGestureRecognizer(panner)
theButton.addTarget(self, action: #selector(clickButton), for: .touchDown)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpInside)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpOutside)
}
var startLocation = CGPoint.zero
lazy var floatingView: UIImageView = {
let view = UIImageView(image: UIImage(named: "imagename"))
view.backgroundColor = UIColor.blue
return view
}()
// When the user clicks button - we create the image and put it on the screen
// this makes the action seem faster vs waiting for the panning action to kick in
#objc func clickButton() {
guard let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window else { return }
// We ask the button what it's bounds are within it's own coordinate system and convert that to the
// Window's coordinate system and set the frame on the floating view. This makes the new view overlap the
// button exactly.
floatingView.frame = theButton.convert(theButton.bounds, to: nil)
window.addSubview(floatingView)
// Save the location so we can translate it as part of the pan actions
startLocation = floatingView.center
}
// This is here to handle the case where the user didn't move enough to kick in the panGestureRecognizer and cancel the action
#objc func unclickButton() {
floatingView.removeFromSuperview()
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
switch recognizer.state {
case .ended, .failed, .cancelled:
doSomething()
floatingView.removeFromSuperview()
return
default: break
}
// This section is called for any pan state except .ended, .failed and .cancelled
floatingView.center = CGPoint(x: floatingView.center.x + translation.x,
y: floatingView.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}

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! =]

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!

bringSubviewToFront(_:) function breaks a UIPanGestureRecognizer

I have a BoardView class which subclasses UIView. It has several subviews, which all have their own UIPanGestureRecognizer. I use this recognizer to drag around a subview inside my BoardView instance, as such:
func pieceDragged(recognizer: UIPanGestureRecognizer) {
let pieceView = recognizer.view!
if recognizer.state == .Began || recognizer.state == .Changed {
// move the pieceView by the appropriate amount
let translation = recognizer.translationInView(self)
pieceView.center = CGPoint(x: pieceView.center.x + translation.x, y: pieceView.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, inView: self)
} else if recognizer.state == .Ended {
// do something else
}
}
This works fine.
The problem is that I want the subview that is being dragged around to be on top of all of its siblings, so I made my BoardView instance the delegate of each gesture recognizer and overrode this function:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
let pieceView = gestureRecognizer.view!
bringSubviewToFront(pieceView)
return true
}
This seems to break the entire gesture recognizer: if I attempt to drag aroudn a subview, the pieceDragged(_:) function is called exactly once (where the state of the recognizer is .Began), but no more than that.
If I comment out bringSubviewToFront(pieceView), my code works as before - I can drag around any subview, but it's below some of its siblings.
I'm clueless. What could be going on here?
The problem was that I replaced all my subviews in the layoutSubviews() method (which is automatically called after bringToFront(_:) is called).
Seems like you are going round the houses as well and implementing a method that doesn't really make sense. The delegate method gestureRecognizerShouldBegin(_:) is all about should this recogniser actually start detecting not a place to perform an action.
You already have a suitable place to perform the action you want you could simply update your code to be
func pieceDragged(recognizer: UIPanGestureRecognizer) {
guard let pieceView = recognizer.view else {
return
}
switch recognizer.state {
case .Began:
pieceView.superview?.bringSubviewToFront(pieceView)
case .Changed:
let translation = recognizer.translationInView(self)
recognizer.setTranslation(CGPoint.zero, inView: self)
pieceView.center = CGPoint(x: pieceView.center.x + translation.x, y: pieceView.center.y + translation.y)
default: ()
}
}

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