0

I have got mutliple bezier paths which are incorporated into CAShapeLayers and then add all layers to UIImageView. I have implemented hittest to all layers for selection, But it select the last CAShapeLayer. I want to select others layer as touch, but i don't know how?

here is my code for touch.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
    super.touchesBegan(touches, with: event)
            if let touch = touches.first, let touchedLayer = self.layerFor(touch)
            {
                print("hi")
                selectedLayer = touchedLayer
                touchedLayer.strokeColor = UIColor.red.cgColor
                touchedLayer.lineWidth = CGFloat(3)
            }
    
    
}

    private func layerFor(_ touch: UITouch) -> CAShapeLayer?
{
    let touchLocation = touch.location(in: self.backgroundIV)
    let locationInView = self.backgroundIV!.convert(touchLocation, to: nil)
    print("\(locationInView.x)  \(locationInView.y)")
    let hitPresentationLayer = view!.layer.presentation()?.hitTest(locationInView) as? CAShapeLayer
    return hitPresentationLayer?.model()
}

Here is how I create layers from path

    fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    if let any = path.svgAttributes["stroke"] {
        shapeLayer.strokeColor = (any as! CGColor)
    }
    
    if let any = path.svgAttributes["fill"] {
        shapeLayer.fillColor = (any as! CGColor)
    }
    return shapeLayer
}

EDIT: here is the code that add shape layers to parent view

        if let svgURL = Bundle.main.url(forResource: "image", withExtension: "svg") {
        let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
        let scale = CGFloat(0.5)
        for path in paths {
            path.apply(CGAffineTransform(scaleX: scale, y: scale))
            items.append(path)
            let layer = createLayer(path: path)
            layer.frame = self.backgroundIV.bounds
            self.backgroundIV.layer.addSublayer(layer)
        }


    }

and changes in touchBegan methods

 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{

    let point = touches.first?.location(in: self.backgroundIV)
    if let layer = self.backgroundIV.layer.hitTest(point!) as? CAShapeLayer {
        selectedLayer = layer
    selectedLayer.strokeColor = UIColor.red.cgColor
    selectedLayer.lineWidth = CGFloat(3)
        print("Touched")
    }
}
Ali Akbar
  • 7
  • 5
  • Don't use the presentation layer to hit test for touches in your various layers unless you are trying to hit test while you have an animation in flight. Instead you should call hitTest on the base layer of the view that contains your child shape layers. – Duncan C Oct 14 '21 at 13:31
  • You need to show the code that adds your shape layers to another layer so we can see your layer hierarchy. You might also be converting your touch coordinates to the wrong coordinate system. – Duncan C Oct 14 '21 at 13:32
  • @DuncanC Thanks for your reply, according to your first comment i add this lines to touchBegan method let point = touches.first?.location(in: self.backgroundIV) if let layer = self.backgroundIV.layer.hitTest(point!) as? CAShapeLayer { selectedLayer = layer selectedLayer.strokeColor = UIColor.red.cgColor selectedLayer.lineWidth = CGFloat(3) print("Touched") } But result is same. – Ali Akbar Oct 14 '21 at 14:24
  • Edit your question to include the code that adds your shape layers to their parent layer, as well as your most recent changes. It's just about impossible to follow code in comments due to the lack of formatting. – Duncan C Oct 14 '21 at 14:31
  • @DuncanC i think i edited – Ali Akbar Oct 14 '21 at 14:46
  • @AliAkbar - is your goal to find ALL layers where the touch is inside the layer's shape? – DonMag Oct 14 '21 at 14:53
  • So now you need to debug your code. For testing, get rid of the code that tries to cast the result of your hitTest to a CAShapeLayer and log the result of the call to hitTest. Also, I suggest adding a borderWidth and light borderColor to your shape layers so you can see their bounds. (The hitTest method only checks that the tap falls inside a layer's frame rect.) – Duncan C Oct 14 '21 at 17:20

1 Answers1

1

I'll make a couple assumptions here...

  1. you're using PocketSVG (or similar)
  2. you want to detect a tap inside the shape off the layer

Even if you're only looking for the layer (not only inside the path of the layer), I would recommend looping through the sublayers and using .contains(point) rather than trying to use layer.hitTest(point).

Here is a quick example:

import UIKit
import PocketSVG

enum DetectMode {
    case All, TopMost, BottomMost
}
enum DetectType {
    case ShapePath, ShapeBounds
}

class BoxesViewController: UIViewController {
    
    var backgroundIV: UIImageView!
    
    var detectMode: DetectMode = .All
    var detectType: DetectType = .ShapePath
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        guard let svgURL = Bundle.main.url(forResource: "roundedboxes", withExtension: "svg") else {
            fatalError("SVG file not found!!!")
        }
        
        backgroundIV = UIImageView()
        
        let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
        let scale = CGFloat(0.5)
        for path in paths {
            path.apply(CGAffineTransform(scaleX: scale, y: scale))
            //items.append(path)
            let layer = createLayer(path: path)
            self.backgroundIV.layer.addSublayer(layer)
        }

        let modeControl = UISegmentedControl(items: ["All", "Top Most", "Bottom Most"])
        let typeControl = UISegmentedControl(items: ["Shape Path", "Shape Bounding Box"])

        modeControl.translatesAutoresizingMaskIntoConstraints = false
        typeControl.translatesAutoresizingMaskIntoConstraints = false
        backgroundIV.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(modeControl)
        view.addSubview(typeControl)
        view.addSubview(backgroundIV)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([

            modeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            modeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            modeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),

            typeControl.topAnchor.constraint(equalTo: modeControl.bottomAnchor, constant: 40.0),
            typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            backgroundIV.topAnchor.constraint(equalTo: typeControl.bottomAnchor, constant: 40.0),
            backgroundIV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            backgroundIV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            backgroundIV.heightAnchor.constraint(equalTo: backgroundIV.widthAnchor),

        ])
        
        modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
        typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
        
        modeControl.selectedSegmentIndex = 0
        typeControl.selectedSegmentIndex = 0
        
        // so we can see the frame of the image view
        backgroundIV.backgroundColor = .white

    }
    
    @objc func modeChanged(_ sender: UISegmentedControl) -> Void {
        switch sender.selectedSegmentIndex {
        case 0:
            detectMode = .All
        case 1:
            detectMode = .TopMost
        case 2:
            detectMode = .BottomMost
        default:
            ()
        }
    }
    
    @objc func typeChanged(_ sender: UISegmentedControl) -> Void {
        switch sender.selectedSegmentIndex {
        case 0:
            detectType = .ShapePath
        case 1:
            detectType = .ShapeBounds
        default:
            ()
        }
    }
    
    fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        if let any = path.svgAttributes["stroke"] {
            shapeLayer.strokeColor = (any as! CGColor)
        }
        
        if let any = path.svgAttributes["fill"] {
            shapeLayer.fillColor = (any as! CGColor)
        }
        return shapeLayer
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        guard let point = touches.first?.location(in: self.backgroundIV),
              // make sure backgroundIV has sublayers
              let layers = self.backgroundIV.layer.sublayers
        else { return }
        
        var hitLayers: [CAShapeLayer] = []
        
        // loop through all sublayers
        for subLayer in layers {
            // make sure
            //  it is a CAShapeLayer
            //  it has a path
            if let thisLayer = subLayer as? CAShapeLayer,
               let pth = thisLayer.path {
                // clear the lineWidth... we'll reset it after getting the hit layers
                thisLayer.lineWidth = 0
                
                // convert touch point from backgroundIV.layer to thisLayer
                let layerPoint: CGPoint = thisLayer.convert(point, from: self.backgroundIV.layer)
                
                if detectType == .ShapePath {
                // does the path contain the point?
                    if pth.contains(layerPoint) {
                        hitLayers.append(thisLayer)
                    }
                } else if detectType == .ShapeBounds {
                    if pth.boundingBox.contains(layerPoint) {
                        hitLayers.append(thisLayer)
                    }
                }
            }
        }

        if detectMode == .All {
            hitLayers.forEach { layer in
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        } else if detectMode == .TopMost {
            if let layer = hitLayers.last {
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        } else if detectMode == .BottomMost {
            if let layer = hitLayers.first {
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        }

    }
    
}

When you run this, it will look like this (I'm in a navigation controller):

enter image description here

This is the SVG file I used:

roundedboxes.svg

<?xml version="1.0" encoding="UTF-8"?>
<svg width="240px" height="240px" viewBox="0 0 240 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Untitled</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <rect id="RedRectangle" fill-opacity="0.75" fill="#FF0000" x="0" y="0" width="160" height="160" rx="60"></rect>
        <rect id="GreenRectangle" fill-opacity="0.75" fill="#00FF00" x="80" y="0" width="160" height="160" rx="60"></rect>
        <rect id="BlueRectangle" fill-opacity="0.75" fill="#0000FF" x="40" y="80" width="160" height="160" rx="60"></rect>
    </g>
</svg>

By default, we're going to test for tap inside the shape path on each layer, and we'll highlight all layers that pass the test:

enter image description here

Note that tapping where the shapes / layers overlap will select all layers where the tap is inside its path.

If we want only the Top Most layer, we'll get this:

enter image description here

If we want only the Bottom Most layer, we'll get this:

enter image description here

If we switch to detecting the Shape Bounding Box, we get this:

enter image description here

If that is at least close to what you're trying for, play around with this example code and see what you get.


Edit - minor change...

Using SVGImageView instead of UIImageView so we can scale the SVG to fit the view:

class BoxesViewController: UIViewController {
    
    var backgroundIV: SVGImageView!
    
    var detectMode: DetectMode = .All
    var detectType: DetectType = .ShapePath
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        let svgName = "roundedboxes"
        guard let svgURL = Bundle.main.url(forResource: svgName, withExtension: "svg") else {
            fatalError("SVG file not found!!!")
        }
        
        backgroundIV = SVGImageView.init(contentsOf: svgURL)
        
        backgroundIV.contentMode = .scaleAspectFit
        
        let modeControl = UISegmentedControl(items: ["All", "Top Most", "Bottom Most"])
        let typeControl = UISegmentedControl(items: ["Shape Path", "Shape Bounding Box"])
        
        modeControl.translatesAutoresizingMaskIntoConstraints = false
        typeControl.translatesAutoresizingMaskIntoConstraints = false
        backgroundIV.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(modeControl)
        view.addSubview(typeControl)
        view.addSubview(backgroundIV)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            modeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            modeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            modeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            typeControl.topAnchor.constraint(equalTo: modeControl.bottomAnchor, constant: 40.0),
            typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            backgroundIV.topAnchor.constraint(equalTo: typeControl.bottomAnchor, constant: 40.0),
            backgroundIV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            backgroundIV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            backgroundIV.heightAnchor.constraint(equalTo: backgroundIV.widthAnchor),
            
        ])
        
        modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
        typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
        
        modeControl.selectedSegmentIndex = 0
        typeControl.selectedSegmentIndex = 0
        
        // so we can see the frame of the image view
        backgroundIV.backgroundColor = .white
        
    }
    
    @objc func modeChanged(_ sender: UISegmentedControl) -> Void {
        switch sender.selectedSegmentIndex {
        case 0:
            detectMode = .All
        case 1:
            detectMode = .TopMost
        case 2:
            detectMode = .BottomMost
        default:
            ()
        }
    }
    
    @objc func typeChanged(_ sender: UISegmentedControl) -> Void {
        switch sender.selectedSegmentIndex {
        case 0:
            detectType = .ShapePath
        case 1:
            detectType = .ShapeBounds
        default:
            ()
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        guard let point = touches.first?.location(in: self.backgroundIV),
              // make sure backgroundIV has sublayers
              let layers = self.backgroundIV.layer.sublayers
        else { return }
        
        var hitLayers: [CAShapeLayer] = []
        
        // loop through all sublayers
        for subLayer in layers {
            // make sure
            //  it is a CAShapeLayer
            //  it has a path
            if let thisLayer = subLayer as? CAShapeLayer,
               let pth = thisLayer.path {
                // clear the lineWidth... we'll reset it after getting the hit layers
                thisLayer.lineWidth = 0
                
                // convert touch point from backgroundIV.layer to thisLayer
                let layerPoint: CGPoint = thisLayer.convert(point, from: self.backgroundIV.layer)
                
                if detectType == .ShapePath {
                    // does the path contain the point?
                    if pth.contains(layerPoint) {
                        hitLayers.append(thisLayer)
                    }
                } else if detectType == .ShapeBounds {
                    if pth.boundingBox.contains(layerPoint) {
                        hitLayers.append(thisLayer)
                    }
                }
            }
        }
        
        if detectMode == .All {
            hitLayers.forEach { layer in
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        } else if detectMode == .TopMost {
            if let layer = hitLayers.last {
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        } else if detectMode == .BottomMost {
            if let layer = hitLayers.first {
                layer.strokeColor = UIColor.cyan.cgColor
                layer.lineWidth = 3
            }
        }
        
    }
    
}

Result:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86