Home > Mobile >  How to hit multiple CAShapeLayer in swift?
How to hit multiple CAShapeLayer in swift?

Time:10-16

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")
    }
}

CodePudding user response:

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.

  • Related