Swift - UIPanGestureRecognizer selecting all layers when only want the top layer selected - swift

I have function which creates a drag line to connect 2 buttons to each other. This works fine but if some buttons overlap each other, it will select both if I drag over where they overlap. I only want to connect the top button.
I think the issue is with the sender.location selecting layers on top and below. Is there a way to tell the sender.location to only select the top view? Thanks for any input and direction
func addPanReconiser(view: UIView){
let pan = UIPanGestureRecognizer(target: self, action: #selector(DesignViewController.panGestureCalled(_:)))
view.addGestureRecognizer(pan)
}
#objc func panGestureCalled(_ sender: UIPanGestureRecognizer) {
let currentPanPoint = sender.location(in: self.view)
switch sender.state {
case .began:
panGestureStartPoint = currentPanPoint
self.view.layer.addSublayer(lineShape)
case .changed:
let linePath = UIBezierPath()
linePath.move(to: panGestureStartPoint)
linePath.addLine(to: currentPanPoint)
lineShape.path = linePath.cgPath
lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point) {
button.layer.borderWidth = 4
button.layer.borderColor = UIColor.blue.cgColor
} else {
button.layer.borderWidth = 0
button.layer.borderColor = UIColor.clear.cgColor
}
}
case .ended:
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
//DO my Action here
lineShape.path = nil
lineShape.removeFromSuperlayer()
}
}
default: break
}
}
}
Note: some of the lines of codes are from custom extensions. I kept them in as they were self explanatory.
Thanks for the help

There is a way to walk around. It seems like you simply want your gesture end up at one button above all the others, thus by adding a var outside the loop and each time a button picked, comparing with the var of its level at z.
case .ended:
var pickedButton: UIButton?
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
if pickedButton == nil {
pickedButton = button
} else {
if let parent = button.superView, parent.subviews.firstIndex(of: button) > parent.subviews.firstIndex(of: pickedButton!) {
pickedButton = button
}
}
}
}
//DO my Action with pickedButton here
lineShape.path = nil
lineShape.removeFromSuperlayer()

A UIView has a property called subViews where elements with higher indexes are in front of the ones with lower indexes. For instance, subView at index 1 is in front of subView with index 0.
That being said, to get the button that's on top, you should sort your buttonArray the same way subViews property of UIView is organized. Assuming that your buttons are all siblings of the same UIView (this might not be necessarily the case, but you can tweak them so you get them sorted correctly):
var buttonArray = view.subviews.compactMap { $0 as? UIButton }
Thus, keeping your buttonArray sorted that way, the button you want is the one that contains let point = sender.location(in: button) with higher index in the array.

Related

How can I reset the position of my marker view when the cancel button is pressed?

I have a markerView which you can grab and move around. I added a panGesture to it.
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleDraggedView(_:)))
fanMenu.addGestureRecognizer(panGesture)
To drag the view around and update the position of it:
#objc func handleDraggedView(_ sender: UIPanGestureRecognizer){
if let marker = documentationMarkers.first(where: {$0.documentationId == documentationPressed.id}) {
if(fanMenu.isOpen && marker.documentationType == "DEFAULT") {
self.viewForTilingView.bringSubview(toFront: marker)
let translation = sender.translation(in: self.viewForTilingView)
marker.center = CGPoint(x: marker.center.x + translation.x, y: marker.center.y + translation.y)
let point = viewForTilingView.convert(marker.center, to: scrollView)
fanMenu.center = CGPoint(x: point.x,
y: point.y)
sender.setTranslation(CGPoint.zero, in: self.viewForTilingView.superview)
print("Startpoint-X: \(point.x)", "Startpoint-Y: \(point.y)")
if(sender.state == .ended) {
print("Endpoint-X: \(point.x)", "Endpoint-Y: \(point.y)")
if let documentation = documentations.first(where: {$0.id == marker.documentationId}) {
let levelZoom : CGFloat = pow(2.0, CGFloat(viewForTilingView.tileProvider!.levelsOfDetail - 1))
let positionX = Int(marker.center.x * levelZoom)
let positionY = Int(marker.center.y * levelZoom)
_ = updatePosition(documentation: documentation, positionX: positionX, positionY: positionY)
documentationAdded(doc: documentation, update: true)
}
}
}
}
}
my start and endpoints are correct. When I long press my marker it prints the correct startpoints and when I release it somewhere else it gives me the correct endpoints.
I added some buttons to my marker view. Two of them are a cancel and confirm Button.
In a switch case statement it checks which button is selected. If the conform button is selected and you moved the marker around it updates the position ( = do nothing cause it updates it anyways) but when the cancel button is selected it should not update the postion and reset it to the old one.
func contextMenuButtonClicked(buttonId: String) {
let doc = documentationPressed
switch buttonId {
case "main":
break
case "cancelButton":
//update to the old position
My question is how can I get the old position of my marker view and place it to the start position when the user presses the cancel button.
You can store the marker position by switching between pan states.
For e.g.
//declare a variable inside your class- var olPostion = CGPoint()
if sender.state == .began {
oldPosition = marker.center
}
and on cancelButtonPress, reset the button position to old position
marker.center = oldPosition
NOTE: If you want to reset the button position the position it was
when the view controller loaded, assign the value od oldPosition in
viewDidLoad()

Swift 3 Draggable UIButton

Apologies as I'm still learning the basics of Swift.
I'm trying to move a button when I drag it which sounds simple. I can't figure out how to pass along the sender information to the drag function so I can associate it with the button that is being dragged.
I create multiple one word buttons which are only text and attach a pan gesture recognizer to each of them:
let pan = UIPanGestureRecognizer(target: self, action: #selector(panButton(_:)))
let word = UIButton(type: .system)
word.addGestureRecognizer(pan)
I've created this function to trigger when the button is moved:
func panButton(sender: UIPanGestureRecognizer){
if sender.state == .began {
//wordButtonCenter = button.center // store old button center
} else if sender.state == .ended || sender.state == .failed || sender.state == .cancelled {
//button.center = wordButtonCenter // restore button center
} else {
let location = sender.location(in: view) // get pan location
//button.center = location // set button to where finger is
}
}
I'm getting the following error:
Use of unresolved identifier 'panButton'
First of all, your action needs to be a selector in Swift 3. So that would look something like this:
let pan = UIPanGestureRecognizer(target: self, action: #selector(panButton(_:))
Also, you can't pass the value of a button through a selector, so you would need to change your func to be:
func panButton(sender: UIPanGestureRecognizer){
...
}
If you're wondering how you are supposed to find the button if you can't pass it as a parameter, then you might consider using tags.
As #benjamin points out in Swift 3 you needs to be a selector. I've updated my code to the following in order to extract the button tag:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGestureHandler(panGesture:)))
panGesture.minimumNumberOfTouches = 1
let word = UIButton(type: .system)
With the following selector:
#objc func panGestureHandler(panGesture recognizer: UIPanGestureRecognizer) {
let buttonTag = (recognizer.view?.tag)!
if let button = view.viewWithTag(buttonTag) as? UIButton {
if recognizer.state == .began {
wordButtonCenter = button.center // store old button center
} else if recognizer.state == .ended || recognizer.state == .failed || recognizer.state == .cancelled {
button.center = wordButtonCenter // restore button center
} else {
let location = recognizer.location(in: view) // get pan location
button.center = location // set button to where finger is
}
}
}

Add SCNNode after rotating rootNode

I'm trying to add a node (a sphere) to a body model but it doesn't work properly after I rotate the model through a pan gesture.
Here's how I'm adding the node (using a long tap gesture):
func addSphere(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .Began:
let location = sender.locationInView(bodyView)
let hitResults = bodyView.hitTest(location, options: nil)
if hitResults.count > 0 {
let result = hitResults.first!
let secondSphereGeometry = SCNSphere(radius: 0.015)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
let vpWithZ = SCNVector3(x: Float(result.worldCoordinates.x), y: Float(result.worldCoordinates.y), z: Float( result.worldCoordinates.z))
secondSphereNode.position = vpWithZ
bodyView.scene!.rootNode.addChildNode(secondSphereNode)
}
break
default:
break
}
}
Here is how I rotate the view:
func rotateGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newZAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newZAngle += currentZAngle
bodyView.scene!.rootNode.transform = SCNMatrix4MakeRotation(newZAngle, 0, 0, 1)
if sender.state == .Ended {
currentZAngle = newZAngle
}
}
And to load the 3D model I just do:
bodyView.scene = SCNScene(named: "male_body.dae") // bodyView is a SCNView in the storyboard
I found something related to the worldTransform property and also the function convertPosition:toNode: but couldn't find an example that works well.
The problem is that, if I rotate the model, the sphere are not positioned properly. They're always positioned as if the model was in its initial state.
If I turn the body and add long tap his arm (on the side), the sphere is added somewhere floating in front of the body, as you can see above.
I don't know how to fix this. Appreciate if someone can help me. Thanks!

Detect if user is moving finger left or right (Swift)

This is not Sprite Kit.
If I have a variable like the one below
var value = 0
How am I able to increase the value if the user drags right and decrease if they drag left?
Thanks!
Like Caleb commented, Ray's tutorial is great, but if you want the actual swift example, please check the next example:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
private var value: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.blackColor()
let recognizer = UIPanGestureRecognizer(target: self, action: Selector("handleDragging:"))
let inputView = UIView(frame: CGRectMake(0, 0, 100, 100))
inputView.backgroundColor = UIColor.whiteColor()
inputView.userInteractionEnabled = true
inputView.addGestureRecognizer(recognizer)
self.view.addSubview(inputView)
}
func handleDragging(recognizer: UIPanGestureRecognizer) {
if (recognizer.state == .Changed) {
let point = recognizer.velocityInView(recognizer.view?.superview)
if (point.x > 0) {
self.value++;
} else {
self.value--;
}
println(self.value)
}
}
}
You can use the velocityInView method of UIPanGestureRecognizer to determine which direction you're going. It returns a CGPoint, so you can pull out the x and y values as you wish. Positive is right/down, negative is left/up.

Swift - Programming UISwipeGestureRecognizer to separately control 2 objects

Brand new to Swift but am picking it up little by little. I have 2 objects that I want to control at the same time with UISwipeGestureRecognizer. I have it working for one object but need to be able to swipe on the left side of the screen and the right side to control the 2 objects separately. I'm guessing I can implement a statement that if swipe is less than this position control this object else control this one just not sure how to implement. This is what I'm using to control the one object.
self.swipeRightGesture.addTarget(self, action: Selector("handleRightSwipe:"))
self.swipeRightGesture.direction = .Right
self.swipeRightGesture.numberOfTouchesRequired = 1
self.view?.addGestureRecognizer(self.swipeRightGesture)
func handleRightSwipe(sender: UIGestureRecognizer) {
if !self.isMoving && self.isMovingUp == true{
self.leftobjectmoveright()
self.isMoving = true
self.isMoving = false
self.isMovingUp = false
}
}
func leftobjectmoveright() {
self.leftobject.physicsBody?.velocity = CGVectorMake(75,0)
}
And how do you intend the user to reflect which of the three gestures (up, left, and right) is for which object? Are you starting the gesture on top of the object? If that's the case, there are a couple of approaches.
The most logical approach, in my opinion, is to create a separate set of gestures for each object, and then in the gesture recognizer, you can look at gesture.view to identify which one resulted in the gesture recognizer's selector to be called. For example:
let leftObjectDownSwipe = UISwipeGestureRecognizer(target: self, action: Selector("handleDownSwipe:"))
leftObjectDownSwipe.direction = .Down
leftObject?.addGestureRecognizer(leftObjectDownSwipe)
let rightObjectDownSwipe = UISwipeGestureRecognizer(target: self, action: Selector("handleDownSwipe:"))
rightObjectDownSwipe.direction = .Down
rightObject?.addGestureRecognizer(rightObjectDownSwipe)
With a handleDownSwipe like so:
func handleDownSwipe(gesture: UISwipeGestureRecognizer) {
if gesture.view == leftObject {
println("swiped left one")
} else if gesture.view == rightObject {
println("swiped right one")
}
}
Or, if the handling of the gestures for the two different objects were sufficiently different, you might just give them completely separate gesture handlers. It's just a question of how much common code there is in these two gesture handlers.
Alternatively, you could put the gesture recognizer on the superview that contains these two view objects:
let downSwipe = UISwipeGestureRecognizer(target: self, action: Selector("handleDownSwipe:"))
downSwipe.direction = .Down
view.addGestureRecognizer(downSwipe)
And then you could have the gesture recognizer look at the width of the view and compare that to the locationInView:
func handleDownSwipe(gesture: UISwipeGestureRecognizer) {
let location = gesture.locationInView(gesture.view)
if location.x < (gesture.view!.frame.size.width / 2.0) {
println("swiped left side")
} else {
println("swiped right side")
}
}