4

Imagine an iOS screen where you can move your finger up/down to do something (imagine say, "scale"),

Or,

as a separate function you can move your finger left/right to do something (imagine say, "change color" or "rotate").

They are

separate

functions, you can only do one at at time.

So, if it "begins as" a horizontal version, it is remains only a horizontal version. Conversely if it "begins as" a vertical version, it remains only a vertical version.

It is a little bit tricky to do this, I present exactly how to do it below...the fundamental pattern is:

if (r.state == .Began || panway == .WasZeros )
    {
    prev = tr
    if (tr.x==0 && tr.y==0)
        {
        panway = .WasZeros
        return
        }
    if (abs(tr.x)>abs(tr.y)) ... set panway
    }

This works very well and here's exactly how to do it in Swift.

In storyboard take a UIPanGestureRecognizer, drag it to the view in question. Connect the delegate to the view controller and set the outlet to this call ultraPan:

enum Panway
    { 
    case Vertical
    case Horizontal
    case WasZeros
    }
var panway:Panway = .Vertical
var prev:CGPoint!

@IBAction func ultraPan(r:UIPanGestureRecognizer!)
    {
    let tr = r.translationInView(r.view)
    
    if (r.state == .Began || panway == .WasZeros )
        {
        prev = tr
        
        if (tr.x==0 && tr.y==0)
            {
            panway = .WasZeros
            return
            }
        
        if (abs(tr.x)>abs(tr.y))
            {
            panway = .Horizontal
            }
        else
            {
            panway = .Vertical
            }
        }
    
    if (panway == .Horizontal)  // your left-right function
        {
        var h = tr.x - prev.x
        let sensitivity:CGFloat = 50.0
        h = h / sensitivity
        // adjust your left-right function, example
        someProperty = someProperty + h
        }
    
    if (panway == .Vertical)    // bigger/smaller
        {
        var v = tr.y - prev.y
        let sensitivity:CGFloat = 2200.0
        v = v / sensitivity
        // adjust your up-down function, example
        someOtherProperty = someOtherProperty + v
        }
    
    prev = tr
    }

That's fine.

But it would surely be better to make a new subclass (or something) of UIPanGestureRecognizer, so that there are two new concepts......

UIHorizontalPanGestureRecognizer

UIVerticalPanGestureRecognizer

Those would be basically one-dimensional panners.

I have absolutely no clue whether you would ... subclass the delegates? or the class? (what class?), or perhaps some sort of extension ... indeed, I basically am completely clueless on this :)

The goal is in one's code, you can have something like this ...

@IBAction func horizontalPanDelta( ..? )
    {
    someProperty = someProperty + delta
    }
@IBAction func verticalPanDelta( ..? )
    {
    otherProperty = otherProperty + delta
    }

How to inherit/extend UIPanGestureRecognizer in this way??

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719

2 Answers2

3

But it would surely be better to make a new subclass (or something) of UIPanGestureRecognizer, so that there are two new concepts......

  • UIHorizontalPanGestureRecognizer

  • UIVerticalPanGestureRecognizer

Those would be basically one-dimensional panners

Correct. That's exactly how to do it, and is the normal approach. Indeed, that is exactly what gesture recognizers are for: each g.r. recognizes only its own gesture, and when it does, it causes the competing gesture recognizers to back off. That is the whole point of gesture recognizers! Otherwise, we'd still be back in the pre-g.r. days of pure touchesBegan and so forth (oh, the horror).

My online book discusses, in fact, the very example you are giving here:

http://www.apeth.com/iOSBook/ch18.html#_subclassing_gesture_recognizers

And here is an actual downloadable example that implements it in Swift:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch05p203gestureRecognizers/ch18p541gestureRecognizers/HorizVertPanGestureRecognizers.swift

Observe the strategy used here. We make UIPanGestureRecognizer subclasses. We override touchesBegan and touchesMoved: the moment recognition starts, we fail as soon as it appears that the next touch is along the wrong axis. (You should watch Apple's video on this topic; as they say, when you subclass a gesture recognizer, you should "fail early, fail often".) We also override translationInView so that only movement directly along the axis is possible. The result (as you can see if you download the project itself) is a view that can be dragged either horizontally or vertically but in no other manner.

Community
  • 1
  • 1
matt
  • 515,959
  • 87
  • 875
  • 1,141
2

@Joe, this is something I scrambled together quickly for the sake of this question. For ease of making it, I simply gave it a callback rather than implementing a target-action system. Feel free to change it.

enum PanDirection {
    case Horizontal
    case Vertical
}

class OneDimensionalPan: NSObject {

    var handler: (CGFloat -> ())?
    var direction: PanDirection

    @IBOutlet weak var panRecognizer: UIPanGestureRecognizer! {
        didSet {
            panRecognizer.addTarget(self, action: #selector(panned))
        }
    }

    @IBOutlet weak var panView: UIView!

    override init() {
        direction = .Horizontal
        super.init()
    }

    @objc private func panned(recognizer: UIPanGestureRecognizer) {
        let translation = panRecognizer.translationInView(panView)
        let delta: CGFloat
        switch direction {
        case .Horizontal:
            delta = translation.x
            break
        case .Vertical:
            delta = translation.y
            break
        }
        handler?(delta)
    }
}

In storyboard, drag a UIPanGestureRecognizer onto your view controller, then drag an Object onto the top bar of the view controller, set its class, and link its IBOutlets. After that you should be good to go. In your view controller code you can set its callback and pan direction.

UPDATE FOR EXPLANATION > I want to clarify why I made the class a subclass of NSObject: the driving idea behind the class is to keep any unnecessary code out of the UIViewController. By subclassing NSObject, I am able to then drag an Object onto the view controller's top bar inside storyboard and set its class to OneDimensionalPan. From there, I am able to connect @IBOutlets to it. I could have made it a base class, but then it would have had to be instantiated programmatically. This is far cleaner. The only code inside the view controller is for accessing the object itself (through an @IBOutlet), setting its direction, and setting its callback.

Adam H.
  • 681
  • 3
  • 8
  • @Joe, for the sake of the question, please give a response as to how it solved or did not solve your problem. – Adam H. May 24 '16 at 14:14
  • Just thought I'd add that you may need to use the delegate method of the gesture recognizer to let them simultaneously recognize. – Adam H. May 25 '16 at 13:55
  • Yea no worries. I'm just wondering how this question received that bounty if you haven't tried it yet. Is that automatic? – Adam H. May 25 '16 at 14:13
  • I like your idea of using an Object here, yes it simplifies code. You know, your actual code in `panned` just sends both directions to either listener; that's different from actually recognizing one or the other (and only one or the other) ... I like the approach though .. – Fattie May 25 '16 at 14:51
  • Yes. I do not know how to circumnavigate that inline logic. – Adam H. May 25 '16 at 14:53