iDeveloper
iDeveloper

Reputation: 51

CAShapeLayer stroke issue

The only reason that I am using CAShapeLayer instead of CALayer is for that's animation property.

Red border for view.layer is the reference. If set set lineWidth for shapeLayer then this layer gone out of the red frame.

But I want it fit to the red box. (fit to the NSView)

Code:

CustomView.swift:

 class CustomView: NSView{
 
    let shapeLayer = CAShapeLayer()
    
    init(){
        super.init(frame: .zero)
        
        wantsLayer = true
 
        layer?.borderWidth = 1.0
        layer?.borderColor = NSColor.red.cgColor
        layer?.masksToBounds = false
 
        layer!.addSublayer(shapeLayer)
 
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
 
    override func draw(_ rect: NSRect) {
        super.draw(rect)
    
        let path = CGMutablePath()

        path.move(to: CGPoint.zero)
        path.addLine(to: CGPoint(x: rect.width/2, y:rect.height))
        path.addLine(to: CGPoint(x: rect.width, y: 0))
        path.closeSubpath()
        
        shapeLayer.path = path
        
        shapeLayer.lineWidth = 30
        shapeLayer.strokeColor = NSColor.lightGray.cgColor
        shapeLayer.fillColor = .white
 
     }
}

ViewController.swift

     class ViewController: NSViewController {
 
    private lazy var customView: CustomView = {
        
        let customView = CustomView()
        
        view.addSubview(customView)
        
        customView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
        
            customView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            customView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            customView.heightAnchor.constraint(equalToConstant: 144),
            customView.widthAnchor.constraint(equalToConstant: 144)
        ])
        
        return customView
 
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        customView.shapeLayer.fillColor = NSColor.systemGreen.cgColor
    }
   
}

Screenshot:

enter image description here

Update:

As per the answer & comment on this question. I did updated override func draw(_ rect: NSRect) by below code

override func draw(_ rect: NSRect) {
        super.draw(rect)
                
        let path = CGMutablePath()
        
        let lineWidth: CGFloat = 30
       
        path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
        path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth/2))
        path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
        
        path.closeSubpath()
        
        shapeLayer.path = path
        shapeLayer.lineWidth = lineWidth
     }

CustomView.init is,

init(){
        super.init(frame: .zero)
        
        wantsLayer = true
 
        layer?.borderWidth = 1.0
        layer?.borderColor = NSColor.red.cgColor
        layer?.masksToBounds = false
 
        layer!.addSublayer(shapeLayer)
 
        shapeLayer.strokeColor = NSColor.lightGray.cgColor
        shapeLayer.fillColor = .white
 
    }

Output:

enter image description here

But still I can't get correct drawing.

Update:

modified 1st line in the paths... So the whole path will be,

let path = CGMutablePath()
    
    let lineWidth: CGFloat = 30
   
    path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
    path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth))
    path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
    
    path.closeSubpath()

Output: enter image description here

Now, I've got confuse to modify 2nd line. I don't have any idea that How to solve it. Please hint/help me to solve this problem. Thanks in advance...

Upvotes: 0

Views: 1092

Answers (3)

El Tomato
El Tomato

Reputation: 6707

I don't really develop iOS apps much. But see the following.

// View controller //
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 200, height: 200))
        let triangleView = TriangleView(frame: rect, backColor: UIColor.green, strokeColor: UIColor.blue, lineWidth: 10)
        view.addSubview(triangleView)
    }
}

// Subclass of UIView //
import UIKit

class TriangleView: UIView {
    var backColor: UIColor
    var strokeColor: UIColor
    var lineWidth: CGFloat
    
    init(frame: CGRect, backColor: UIColor, strokeColor: UIColor, lineWidth: CGFloat) {
        self.backColor = backColor
        self.strokeColor = strokeColor
        self.lineWidth = lineWidth
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        backColor.set()
        //UIRectFill(rect)
        
        let shapeLayer = CAShapeLayer()
        let path = UIBezierPath()
        path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
        path.addLine(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
        path.addLine(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
        path.addLine(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
        path.close()
        shapeLayer.path = path.cgPath
        shapeLayer.lineWidth = lineWidth
        shapeLayer.fillColor = backColor.cgColor
        shapeLayer.strokeColor = strokeColor.cgColor
        layer.insertSublayer(shapeLayer, at: 0)
        layer.masksToBounds = false
    }
}

The following is Cocoa version.

// View controller //
import Cocoa

class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let rect = CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 200, height: 200))
        let myView = MyView(frame: rect, fillColor: NSColor.green, strokeColor: NSColor.red, lineWidth: 10.0)
        myView.wantsLayer = true
        view.addSubview(myView)
    }
}

// Subclass of NSView //
import Cocoa

class MyView: NSView {
    override var isFlipped: Bool { return true }
    
    var fillColor: NSColor
    var strokeColor: NSColor
    var lineWidth: CGFloat
    init(frame: CGRect, fillColor: NSColor, strokeColor: NSColor, lineWidth: CGFloat){
        self.fillColor = fillColor
        self.strokeColor = strokeColor
        self.lineWidth = lineWidth
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: NSRect) {
        super.draw(rect)
        
        let path = NSBezierPath()
        path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
        path.line(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
        path.line(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
        path.line(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
        path.close()
        fillColor.setFill()
        path.fill()
        path.lineWidth = lineWidth
        strokeColor.set()
        path.stroke()
    }
}

Upvotes: 1

Rob
Rob

Reputation: 437412

Another variation on the “double line width with mask” implementation:

class TriangleView: NSView {
    let lineWidth: CGFloat = 20

    private lazy var shapeLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = NSColor.clear.cgColor
        shapeLayer.strokeColor = NSColor.red.cgColor
        shapeLayer.lineWidth = lineWidth * 2
        return shapeLayer
    }()

    private let maskLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = NSColor.white.cgColor
        shapeLayer.strokeColor = NSColor.clear.cgColor
        shapeLayer.lineWidth = 0
        return shapeLayer
    }()

    override init(frame frameRect: NSRect = .zero) {
        super.init(frame: frameRect)
        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    func configure() {
        wantsLayer = true
        layer?.borderColor = NSColor.blue.cgColor
        layer?.borderWidth = 1
        layer?.addSublayer(shapeLayer)
    }

    override func layout() {
        super.layout()

        let path = CGMutablePath()
        path.move(to: NSPoint(x: bounds.midX, y: bounds.maxY))
        path.addLine(to: NSPoint(x: bounds.maxX, y: bounds.minY))
        path.addLine(to: NSPoint(x: bounds.minX, y: bounds.minY))
        path.closeSubpath()

        shapeLayer.path = path
        maskLayer.path = path
        shapeLayer.mask = maskLayer
    }
}

The concept is the same as the accepted answer (re double lineWidth and mask).

As you can see, when using CAShapeLayer, we do not implement draw, but rather let the shape layer take care of the rendering. But we do want to respond to frame changes, so we set (and reset) the path in layout.

Anyway, that yields:

enter image description here

Upvotes: 1

Leo Dabus
Leo Dabus

Reputation: 236275

edit/update:

It would be easier to multiply the line width by 2 and add a mask to your shape:

class Triangle: NSView {
    let shapeLayer = CAShapeLayer()
    var lineWidth: CGFloat
    var strokeColor: NSColor = .clear
    var fillColor: NSColor = .clear
    init(size: CGSize, lineWidth: CGFloat = 10, strokeColor: NSColor = .white,  fillColor: NSColor = .black) {
        self.lineWidth = lineWidth*2
        self.strokeColor = strokeColor
        self.fillColor = fillColor
        super.init(frame: .init(origin: .zero, size: size))
        wantsLayer = true
        let path = NSBezierPath()
        path.move(to: .zero)
        path.line(to: .init(x: size.width/2 , y: size.height))
        path.line(to: .init(x: size.width, y: .zero))
        path.close()
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        shapeLayer.mask = mask
        shapeLayer.path = path.cgPath
        shapeLayer.lineWidth = self.lineWidth
        shapeLayer.strokeColor = strokeColor.cgColor
        shapeLayer.fillColor = fillColor.cgColor
//      layer?.borderWidth = 1
//      layer?.borderColor = NSColor.red.cgColor
        layer?.addSublayer(shapeLayer)
    }
    convenience init(width: CGFloat, height: CGFloat, lineWidth: CGFloat = 10, strokeColor: NSColor = .white, fillColor: NSColor = .black) {
        self.init(size: .init(width: width, height: height), lineWidth: lineWidth, strokeColor: strokeColor, fillColor: fillColor)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension NSBezierPath {
    var cgPath: CGPath {
        let path = CGMutablePath()
        var points: [CGPoint] = .init(repeating: .zero, count: 3)
        for i in 0..<elementCount {
            switch element(at: i, associatedPoints: &points) {
            case .moveTo: path.move(to: points[0])
            case .lineTo: path.addLine(to: points[0])
            case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
            case .closePath: path.closeSubpath()
            @unknown default: fatalError()
            }
        }
        return path
    }

}

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let triangle = Triangle(width: 200, height: 200, lineWidth: 1)
        view.addSubview(triangle)
    }
}

enter image description here

Upvotes: 1

Related Questions