Preview CAShapeLayer using IBDesignable and IBInspectable - swift

I try to create a view that includes a shape layer, which can be previewed inside Xcode. I somehow do not manage to get it working completely although most of it works. Specifically I cannot figure out how I can set the shape layer's stroke property since it needs to be set before the layer is added otherwise no strokes are rendered. But if I set it before then the inspectable property does not work anymore. Here is how far I came:
import Foundation
import UIKit
#IBDesignable
class CustomView: UIView
{
#IBInspectable var layerBackgroundColor: UIColor? {
didSet {
if let actualColor = layerBackgroundColor {
layer.backgroundColor = actualColor.cgColor
} else {
layerBackgroundColor = nil
}
}
}
#IBInspectable var strokeColor: UIColor? = UIColor.red {
didSet {
if let actualColor = strokeColor {
self.shapeLayer.strokeColor = actualColor.cgColor
} else {
shapeLayer.strokeColor = nil
}
}
}
private var shapeLayer: CAShapeLayer
override func prepareForInterfaceBuilder()
{
let width = self.bounds.width
let height = self.bounds.height
shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
let path = CGMutablePath()
let middle = width/2
path.move(to:CGPoint(x:middle, y:20))
path.addLine(to:CGPoint(x:middle, y:100))
layerBackgroundColor = UIColor.gray
// If this is not set no stroke will be rendered!
strokeColor = UIColor.red
shapeLayer.path = path
shapeLayer.fillColor = nil
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineWidth = 5
layer.addSublayer(shapeLayer)
}
override init(frame: CGRect)
{
shapeLayer = CAShapeLayer()
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}

You are creating a new shapeLayer inside prepareForInterfaceBuilder, so you need to set the color properties of the shapeLayer inside that method. The inspectable variables are being assigned after initialization but before prepareForInterfaceBuilder, so creating a new CAShapeLayer inside prepareForInterfaceBuilder is trashing your previous assignments. I assume in practice you are going to be doing the custom drawing for your class in another method (since this method is never called outside of IB).
Anyway, the following code should fix your problems with IB, I made four changes, all commented....
import Foundation
import UIKit
#IBDesignable
class CustomView: UIView
{
#IBInspectable var layerBackgroundColor: UIColor? {
didSet {
if let actualColor = layerBackgroundColor {
layer.backgroundColor = actualColor.cgColor
} else {
layerBackgroundColor = nil
}
}
}
#IBInspectable var strokeColor: UIColor? {//1 - Maybe you want to comment out assignment to UIColor.red: = UIColor.red {
didSet {
if let actualColor = strokeColor {
self.shapeLayer.strokeColor = actualColor.cgColor
} else {
shapeLayer.strokeColor = nil
}
}
}
private var shapeLayer: CAShapeLayer
override func prepareForInterfaceBuilder()
{
let width = self.bounds.width
let height = self.bounds.height
shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
let path = CGMutablePath()
let middle = width/2
path.move(to:CGPoint(x:middle, y:20))
path.addLine(to:CGPoint(x:middle, y:100))
//2 - you probably also want to set the shapeLayer's background color to the class variable, not hardcode the class variable
//layerBackgroundColor = UIColor.gray
shapeLayer.backgroundColor = layerBackgroundColor?.cgColor
// If this is not set no stroke will be rendered!
//3 Comment the hardcoding line out
//strokeColor = UIColor.red
//4 - assign the shapeLayer's stroke color
shapeLayer.strokeColor = strokeColor?.cgColor
shapeLayer.path = path
shapeLayer.fillColor = nil
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineWidth = 5
layer.addSublayer(shapeLayer)
}
override init(frame: CGRect)
{
shapeLayer = CAShapeLayer()
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}

Related

Is there a way to set border to round rectangle that is added to CGMutablePath in Swift?

I am trying to create an overlay view, that has a "cutout" part that comes from a frame that is passed to the view, that size and position of that passed frame will change upon creation of the view. And in that "cutout" part I am expecting to see the content that is under that overlay view. Tried to set border to a rounded rectangle that is added to a CGMutablePath, but no luck.
The expected result is something like this:
The code I currently have in my UIView class, without tried solutions as I can't seem to get them to work properly. This current code displays the expected result, but without the red border:
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setFill()
UIRectFill(rect)
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
// frame that will change position on the screen
if let frame = changingFrame {
path.addRoundedRect(in: frame, cornerWidth: 16, cornerHeight: 16)
}
path.addRect(bounds)
shapeLayer.path = path
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.layer.mask = shapeLayer
}
I have tried solutions from here, here, but no luck as CAShapeLayer for border just overlays existing one.
What can I do differently to achieve the expected result? Thanks!
try this ⭐️
If all you want to do is create a rounded rectangle, then you can simply use.
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
let path = UIBezierPath(roundedRect: rectangle, cornerRadius: 20)
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [1.0]
view.layer.addSublayer(border)
One approach is to use two sublayers... a "cutout" layer and a "border" layer.
Use the same path for the cutout and the border shape, setting the line width and stroke color for the "outline".
Here's an example -- including making it #IBDesignable with a few #IBInspectable properties:
#IBDesignable
class BorderedCutoutView: UIView {
#IBInspectable
var bkgColor: UIColor = .systemBlue {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdColor: UIColor = .white {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdWidth: CGFloat = 1 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var radius: CGFloat = 20 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var horizInset: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var vertInset: CGFloat = 60.0 {
didSet {
setNeedsLayout()
}
}
private let cutoutLayer = CAShapeLayer()
private let borderLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
backgroundColor = .clear
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(cutoutLayer)
layer.addSublayer(borderLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
let cp = UIBezierPath(roundedRect: bounds.insetBy(dx: horizInset, dy: vertInset), cornerRadius: radius)
path.append(cp)
path.usesEvenOddFillRule = true
cutoutLayer.path = path.cgPath
cutoutLayer.fillRule = .evenOdd
cutoutLayer.fillColor = bkgColor.cgColor
borderLayer.path = cp.cgPath
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.lineWidth = brdWidth
borderLayer.strokeColor = brdColor.cgColor
}
}
This example uses horizontal and vertical "inset" values to center the cutout in the view.
Result:

Cocoa: Set NSView mask as CAShapeLayer

Hi I am trying to set CAShapeLayer inside to NSView. But I can't set corner radius of the CAShapeLayer outside of draw(_ dirtyRect: NSRect) function.
I have to use CAShapeLayer to set NSBezierPath to draw custom shapes. So, that only I am using CAShapeLayer instead of the NSView's default CALayer.
Here is the code:
extension NSBezierPath
extension NSBezierPath {
var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< elementCount {
let type = element(at: i, associatedPoints: &points)
switch type {
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("Unknown!")
}
}
return path
}
}
CustomView.swift
class CustomView: NSView{
let shapeLayer = CAShapeLayer()
var cornerRadius: CGFloat {
set{
let path = NSBezierPath(roundedRect: bounds, xRadius: newValue, yRadius: newValue)
shapeLayer.path = path.cgPath
path.close()
}
get{
return shapeLayer.cornerRadius
}
}
var backgroundColor: NSColor{
set{
shapeLayer.fillColor = newValue.cgColor
}
get{
return NSColor(cgColor: shapeLayer.backgroundColor ?? .clear)!
}
}
init() {
super.init(frame: .zero)
wantsLayer = true
shapeLayer.masksToBounds = true
layer?.addSublayer(shapeLayer)
// layer?.mask = shapeLayer
// layer?.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
shapeLayer.frame = dirtyRect
let path = NSBezierPath(rect: dirtyRect)
shapeLayer.path = path.cgPath
// shapeLayer.cornerRadius = 22
shapeLayer.borderColor = NSColor.black.cgColor
shapeLayer.borderWidth = 2.0
path.close()
}
}
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: 44),
customView.widthAnchor.constraint(equalToConstant: 144)
])
return customView
}()
override func viewDidLoad() {
super.viewDidLoad()
// customView.layer?.cornerRadius = 22
customView.backgroundColor = .red
customView.cornerRadius = 22
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Current output:
Expected Output:
Code is commented in the attached codes..
Please help me to solve this problem. Thank you in advance...
The following is a simple example. First, draw a simple rectangle with a subclass of NSView. So instantiate it to make an object with a view controller. Then use object's layer to make it a round rect with a stroke color.
// Subclass of `NSView` //
import Cocoa
class MyView: NSView {
var fillColor: NSColor
init(frame: CGRect, fillColor: NSColor){
self.fillColor = fillColor
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.zero)
path.line(to: CGPoint(x: 0.0, y: self.frame.height))
path.line(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.line(to: CGPoint(x: self.frame.width, y: 0.0))
path.line(to: CGPoint.zero)
fillColor.set()
path.fill()
}
}
// 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: 50))
let myView = MyView(frame: rect, fillColor: NSColor.red)
myView.wantsLayer = true
if let myLayer = myView.layer {
myLayer.cornerRadius = 24.0
myLayer.borderWidth = 4.0
//myLayer.borderColor = NSColor.green.cgColor
}
view.addSubview(myView)
}
}

How to cut a specific part from my CALayer

I have a UIView. My UIView has a layer. I added another layer to it. I need to cut a circle in this layer when the user moves his finger to create a drawing effect. I will leave my code, but it is not correct, as it creates a circle once.
class CustomImageView: UIImageView {
private var recognizer: UIPanGestureRecognizer!
private var maskLayer: CALayer!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
recognizer = UIPanGestureRecognizer(target: self, action: #selector(swipeGesture))
self.addGestureRecognizer(recognizer)
maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.black.withAlphaComponent(0.6).cgColor
maskLayer.frame = self.bounds
self.layer.insertSublayer(maskLayer, at: 0)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func crateMask(location: CGPoint, layer: CALayer) {
let maskFrame = CGRect(origin: CGPoint(x: location.x - 25 , y: location.y - 25 ), size: CGSize(width: 50, height: 50))
layer.createMask(maskRect: maskFrame, invert: true)
}
#objc func swipeGesture(sender: UIPanGestureRecognizer) {
let location = sender.location(in: self)
print(location)
crateMask(location: location, layer: self.maskLayer )
}
}
extension CALayer {
func createMask(maskRect: CGRect, invert: Bool = false) {
print(maskRect)
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(self.bounds)
}
let cornerRadius = maskRect.height / 2
path.addRoundedRect(in: maskRect, cornerWidth: cornerRadius, cornerHeight: cornerRadius)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
self.mask = maskLayer
}
}
Here is the screen shot of the UI: https://prnt.sc/ozki6s
Any help will be appreciated! Thanks in advance.

Masking UIView/UIImageView to cutout transparent text

How can I mask an UIView or UIImageView so that a text is cutout from it?
I googled a lot and it seems that many people struggled the same. Most irritating I always tried to invert the alpha of a snapshotted view to get the result.
What I want looks like this:
This is the custom mask label.
import UIKit
final class MaskLabel: UILabel {
// MARK: - IBInspectoable
#IBInspectable var cornerRadius: CGFloat {
get { return self.layer.cornerRadius }
set { self.layer.cornerRadius = newValue }
}
#IBInspectable var borderWidth: CGFloat {
get { return self.layer.cornerRadius }
set { self.layer.borderWidth = newValue }
}
#IBInspectable var borderColor: UIColor {
get { return UIColor(cgColor: self.layer.borderColor ?? UIColor.clear.cgColor) }
set { self.layer.borderColor = newValue.cgColor }
}
#IBInspectable var insetTop: CGFloat {
get { return self.textInsets.top }
set { self.textInsets.top = newValue }
}
#IBInspectable var insetLeft: CGFloat {
get { return self.textInsets.left }
set { self.textInsets.left = newValue }
}
#IBInspectable var insetBottom: CGFloat {
get { return self.textInsets.bottom }
set { self.textInsets.bottom = newValue }
}
#IBInspectable var insetRight: CGFloat {
get { return self.textInsets.right }
set { self.textInsets.right = newValue }
}
// MARK: - Value
// MARK: Public
private var textInsets = UIEdgeInsets.zero
private var originalBackgroundColor: UIColor? = nil
// MARK: - Initializer
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setLabelUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
setLabelUI()
}
override func prepareForInterfaceBuilder() {
setLabelUI()
}
// MARK: - Draw
override func drawText(in rect: CGRect) {
super.drawText(in: UIEdgeInsetsInsetRect(rect, textInsets))
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.setBlendMode(.clear)
originalBackgroundColor?.setFill()
UIRectFill(rect)
super.drawText(in: rect)
context.restoreGState()
}
// MARK: - Function
// MARK: Private
private func setLabelUI() {
// cache (Before masking the label, the background color must be clear. So we have to cache it)
originalBackgroundColor = backgroundColor
backgroundColor = .clear
layer.cornerRadius = cornerRadius
layer.borderWidth = borderWidth
layer.borderColor = borderColor.cgColor
}
}
This is the result.
The main problem I had was my understanding. Instead of taking a colored view and trying to make a transparent hole in it, we can just layer it the other way around.
So we have the colored background in the back, followed by the image in front that has the mask on it to only show the text part. And actually, that's pretty simple if you're using iOS 8+ by using the maskView property of UIView.
So it could look something like this in swift:
let coloredBackground = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
coloredBackground.backgroundColor = UIColor.greenColor()
let imageView = UIImageView(frame: coloredBackground.bounds)
imageView.image = UIImage(named: "myImage")
coloredBackground.addSubview(imageView)
let label = UILabel(frame: coloredBackground.bounds)
label.text = "stackoverflow"
coloredBackground.addSubview(label)
imageView.maskView = label

Set frame of view once

Some of the frames of my views can be set only after layoutSubviews has been called:
class CustomButton: UIButton {
let border = CAShapeLayer()
init() {
super.init(frame: CGRectZero)
border.fillColor = UIColor.whiteColor().CGColor
layer.insertSublayer(border, atIndex: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
border.frame = layer.bounds
border.path = UIBezierPath(rect: bounds).CGPath
}
}
Because I want to animate border in a later state, I only want the code in layoutSubviews to be called once. To do that, I use the following code:
var initial = true
override func layoutSubviews() {
super.layoutSubviews()
if initial {
border.frame = layer.bounds
border.path = UIBezierPath(rect: bounds).CGPath
initial = false
}
}
Question:
I was wondering if there is a better way of doing this. A more elegant and functional way, without having to use an extra variable.
There is a way to do it without an extra variable (albeit not particularly elegant too) which relies on lazy static properties initialization:
class CustomButton: UIButton {
struct InitBorder {
// The static property definition
static let run: Void = {
border.frame = layer.bounds
border.path = UIBezierPath(rect: bounds).CGPath
}()
}
let border = CAShapeLayer()
init() {
super.init(frame: CGRectZero)
border.fillColor = UIColor.whiteColor().CGColor
layer.insertSublayer(border, atIndex: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
// The call which initializes the static property
let _ = InitBorder.run
}
}
let initial = false
override func layoutSubviews() {
super.layoutSubviews()
if initial {
border.frame = layer.bounds
border.path = UIBezierPath(rect: bounds).CGPath
initial = false
}
}