How to cut a specific part from my CALayer - swift

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.

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

Enable user interaction on overlay view

I'm creating an darken overlay on top of my UITableView, I would like to highlight one of the row. I'm able to highlight it, but how can I enable user interaction (e.g. tap on the cell)?
func addFullScreenDarkOverlay(){
let viewDarkOverlay = UIView()
viewDarkOverlay.alpha = 0.5
viewDarkOverlay.backgroundColor = .black
viewDarkOverlay.frame = UIApplication.shared.keyWindow!.frame
// add dummy hole (will change it to the frame of the particular cell)
let holeFrame = CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width/2, height: viewDarkOverlay.frame.height/2)
// Do any additional setup after loading the view.
let path = CGMutablePath()
// Dark background
path.addRect(CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width, height: viewDarkOverlay.frame.height))
// White/spotlight path
path.addRect(holeFrame)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
viewDarkOverlay.layer.mask = maskLayer
viewDarkOverlay.clipsToBounds = true
UIApplication.shared.keyWindow!.addSubview(viewDarkOverlay)
}
Basically, you can disable the user interaction of the mask view, that will pass the all the touch events to the view below... however, if you just want the highlighted area to be intractable (i.e. the cell), you can create a custom UIView subclass, override the hitTest method, like so:
import UIKit
class MaskView: UIView {
var hitableArea: CGRect {
return CGRect(x: 0, y: 0, width: bounds.width * 0.5, height: bounds.height * 0.5)
}
override init(frame: CGRect) {
super.init(frame: frame)
installMaskLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
installMaskLayer()
}
private func installMaskLayer() {
backgroundColor = UIColor.black.withAlphaComponent(0.5)
let path = CGMutablePath()
// Dark background
path.addRect(bounds)
// White/spotlight path
path.addRect(hitableArea)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
layer.mask = maskLayer
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if (hitableArea.contains(point)) {
return nil
}
return self
}
}
Just create this custom view and add it to your window, and now you can only touch the cell now.
Remove the user interaction in your overlay view and you will be able to select the rows in the view below it:
viewDarkOverlay.isUserInteractionEnabled = false
If you want the selection only inside the whole you can transfer the touch from the overlay view to your parent view using a delegate like so:
protocol OverlaySelection: class{
func selected(with touch: UITouch?)
}
class OverlayView: UIView{
weak var delegate : OverlaySelection?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
delegate?.selected(with: touches.first)
}
}
then in your parent view:
func addFullScreenDarkOverlay(){
let viewDarkOverlay = OverlayView()
viewDarkOverlay.delegate = self
//other config..
}
//Delegate
func selected(with touch: UITouch?) {
if let location = touch?.location(in: self.view){
//Here you check if this point is inside the whole and if it is you select
//the row, if not just return
let indexpath = tableview.indexPathForRow(at: location)
tableview.selectRow(at: indexpath, animated: true, scrollPosition: .bottom)
}
}

Gradient Layer not showing on UIButton

I am trying to create a custom UIButton that i can use throughout my application. I would like to apply a gradient as its background however the gradient is not showing up using this very simple Code.
It would be very helpful if someone could point out my mistake in the code below.
class GradientBtn: UIButton {
let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = CGSize.zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1.0
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//rounded corners
layer.cornerRadius = frame.size.height / 2
//gradient
gradientLayer.locations = [0.0, 1.0]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.frame = bounds
layer.insertSublayer(gradientLayer, at: 0)
}
}
Replace
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
with
gradientLayer.colors = [Theme.colorlightBlue.cgColor, Theme.colorMidBlue.cgColor]
And it's always good to add
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = self.bounds
}
Sh_Khan is right about the CGColor references, but a few additional refinements:
Rather than adding the gradient as a sublayer, make it the layerClass of the UIButton. This makes animations much better (e.g. if the button changes size on device rotation).
When you do this, remove all attempts to set the frame of the gradientLayer.
In layoutSubviews, you’ll no longer need to update the gradientLayer.frame, so remove that entirely, but you do want to put your corner rounding there.
It’s extremely unlikely to ever matter, but when you set layer.cornerRadius (or any property of the view, its layer, or that layer's sublayers), you should never refer to self.frame. The frame of the view is the CGRect in the superview’s coordinate system. Only refer to the view’s bounds within the subclass; never its frame.
Thus:
class GradientBtn: UIButton {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = .zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//gradient
gradientLayer.locations = [0, 1]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue].map { $0.cgColor }
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}

Preview CAShapeLayer using IBDesignable and IBInspectable

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