Search code examples
xcode10ios12swift4.2

How Do I Use a PanGestureRecognizer to Reposition a Sublayer?


I would like to reposition sub layers by clicking and dragging. I am having difficulties and have put together a simple demo to illustrate the problems. See the code below and/or this runnable project, which includes a playground, on GitHub.

The demo's UI (see image below) consists of three elements: a main view (black), a subview (red), and a subview sublayer (gray). Both of the views have a tap gesture recognizer which reports to the console. Additionally, the subview has a pan gesture recognizer that allows the subview and/or its sublayer to be repositioned, as well as reporting to the console.

The demo project's UI

First Problem - The sublayer lags behind the gesture.

  1. On my Device and in the Playground: If I tap and drag the red subview then it tracks the finger/cursor fairly well; the gray sublayer moves along with it. If I tap and drag the gray sublayer then it lags behind: I have to move slowly or pause and wait for it to catch up.
  2. On the simulator: Neither the red subview or the gray sublayer track the cursor. They remain stationary until I pause, at which time they quickly move to their new position.

Second Problem - I cannot grab the gray sublayer close to its edge.

If I tap the gray sublayer close to its edge then the console message correctly reports that the gray sublayer was tapped. However, if I try to grab the same spot and move it then red subview moves instead.

Thank you for taking the time to consider this. Any advise will be appreciated.

import UIKit

public class ViewController: UIViewController {

    override public func loadView() {
        let mainView = UIView(frame: CGRect.zero)
        mainView.backgroundColor = .black
        mainView.layer.name = "Main View's Layer"
        mainView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
        self.view = mainView
    }

    override public func viewDidLayoutSubviews() {
        let subview = UIView(frame: view.bounds.insetBy(dx: 50.0, dy: 100.0))
        subview.backgroundColor = .red
        subview.layer.name = "Sub View's Layer"
        subview.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
        subview.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:))))

        let sublayer = CALayer()
        sublayer.bounds = subview.layer.bounds.insetBy(dx: 50, dy: 100)
        sublayer.name = "Sub View's Sublayer"
        sublayer.position = CGPoint(x: subview.layer.bounds.midX, y: subview.layer.bounds.midY)
        sublayer.backgroundColor = UIColor.gray.cgColor
        subview.layer.addSublayer(sublayer)

        view.addSubview(subview)
    }

    // **********************************************************************************************************************

    private struct TouchedLayer : CustomStringConvertible {

        var layer: CALayer
        let startingPosition: CGPoint

        init(recognizer: UIGestureRecognizer) {
            let view = recognizer.view!
            let touchLocation = recognizer.location(in: view)
            let hitTestLocation = view.layer.superlayer!.convert(touchLocation, from: view.layer)

            layer = view.layer.hitTest(hitTestLocation)!
            startingPosition = layer.position
        }

        mutating func updateLayerPosition(translation: CGPoint) {
            layer.position = CGPoint(x: startingPosition.x + translation.x, y: startingPosition.y + translation.y)
        }

        var description: String {
            return "\(layer.name ?? "<Unknown>"): position = \(layer.position)"
        }
    }

    @objc func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
        guard sender.state == .ended else { return }

        let tappedLayer = TouchedLayer(recognizer: sender)
        print("\nTapped", tappedLayer)
    }

    private var panningLayer: TouchedLayer!
    @objc func panGestureRecognized(_ sender: UIPanGestureRecognizer) {
        switch sender.state {
        case .began:
            panningLayer = TouchedLayer(recognizer: sender)
            print("\nPanning Begun - \(panningLayer!)")
        case .changed:
            panningLayer.updateLayerPosition(translation: sender.translation(in: sender.view))
            //print("\tPanning - \(panningLayer!)")
        default:
            print("Panning Ended - \(panningLayer!)")
        }
    }
}

Solution

  • I would like to reposition sub layers by clicking and dragging

    I would suggest that you cease wanting to do that. A layer has no touchability, whereas a view does; indeed, it has been wisely said that a view is a layer plus touchability. If you would use subviews instead of sublayers, it would become possible to touch the subview directly, have a gesture recognizer on it, tap and pan in the normal way, and so on. You are just making life unnecessarily difficult on yourself, with (as far as I know) no perceptible gain, by using layers instead of views.