Reputation: 51
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:
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:
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()
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
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
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:
Upvotes: 1
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)
}
}
Upvotes: 1