Ali Akbar
Ali Akbar

Reputation: 1

How to fit CAShape Layer to UIImage View?

I am parsing SVG image file and convert it paths into CAShapeLayer, after that I am adding this CAShapeLayer to UIImage View's layer. It's work fine, but the problem is that it does not fit to the UIImage View. That's output. Image 1. That's code of adding layers to UIImageView

        if let svgURL = Bundle.main.url(forResource: "image", withExtension: "svg") {
        let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
        for path in paths {
            items.append(path)
            let layer = createLayer(path: path)
            layer.frame = self.backgroundIV.bounds
            self.backgroundIV.layer.addSublayer(layer)
        }
        
        
    }   // Create Layer from Paths Method


    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
}

I Searched and added these two lines of code, i got this result

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

That's is output. Image 2

I don't know how to calculate scaleX or scaleY (using in CGAffineTransform method) value according to uiImage View bounds

Edit Note: I am apply touch support to CAShapeLayer, That's code.

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
    {
        guard let point = touches.first?.location(in: self.backgroundIV),
              let layers = self.backgroundIV.layer.sublayers
        else { return }
        var hitLayers: [CAShapeLayer] = []
        selectedLayer?.lineWidth = CGFloat(0)
        for subLayer in layers {
            if let thisLayer = subLayer as? CAShapeLayer,
               let pth = thisLayer.path {
                let layerPoint: CGPoint = thisLayer.convert(point, from: self.backgroundIV.layer)
                if pth.contains(layerPoint) {
//                    undoModel.append()
                    hitLayers.append(thisLayer)
                }
            }
        }
        
        selectedLayer = hitLayers.last
        selectedLayer?.strokeColor = UIColor.red.cgColor
        selectedLayer?.lineWidth = CGFloat(3)
        undo.append(.init(stokeColor: nil, selectedLayer: selectedLayer, points: []))
        
        if ((selectedLayer?.frame.contains(point)) != nil) {
            isDragged = true
        }
    }

Upvotes: 0

Views: 331

Answers (2)

DonMag
DonMag

Reputation: 77433

This is basically the same answer I gave you to your previous question about detecting touches on multiple layers.

The difference is, instead of using your code to create/add the layers, this example uses SVGImageView which is part of PocketSVG:

class BoxesViewController: UIViewController {
    
    var backgroundIV: SVGImageView!
    
    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 = SVGImageView.init(contentsOf: svgURL)
        
        backgroundIV.frame = view.bounds
        backgroundIV.contentMode = .scaleAspectFit
        backgroundIV.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
        
        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
            }
        }
        
    }
    
}

Now the SVG file is scaled to fit the view:

enter image description here

Upvotes: 0

ChanOnly123
ChanOnly123

Reputation: 1024

According to your updated question, calculating scale using view bounds

var paths = [SVGBezierPath]()

override func viewDidLoad() {
    super.viewDidLoad()

    if let svgURL = Bundle.main.url(forResource: "Freesample", withExtension: "svg") {
        paths = SVGBezierPath.pathsFromSVG(at: svgURL)
        backgroundIV.layer.borderColor = UIColor.black.withAlphaComponent(0.3).cgColor
        backgroundIV.layer.borderWidth = 1
    }
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    let svgBounds = SVGBoundingRectForPaths(paths)
    var scale = CGFloat.zero
    if backgroundIV.bounds.width < backgroundIV.bounds.height {
        scale = backgroundIV.bounds.width / svgBounds.width
    } else {
        scale = backgroundIV.bounds.height / svgBounds.height
    }
    
    self.backgroundIV.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
    for path in paths {
        path.apply(.init(scaleX: scale, y: scale))
        let layer = createLayer(path: path)
        layer.frame = self.backgroundIV.bounds
        self.backgroundIV.layer.addSublayer(layer)
    }
}

Upvotes: 0

Related Questions