How to skip an area (cutout) when drawing with core graphics - ios

I want to draw some items but leave a true alpha transparency cutout for a circular area. What I want to achieve:
Yellow is example background to show bleed through.
The cutout width is actually wider than the arc stroke, so they don't fully intersect. I need true cutout because I a saving to an image with transparency.
I thought maybe I could use setBlendMode() but I believe that would only work if I wanted my cutout to be exactly the same width as the arc. But there is the gist of how I was trying to go about it:
A Swift workbook follows. Any tips on achieving this are greatly appreciated.
import Foundation
import UIKit
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIView
{
override func draw(_ rect: CGRect)
{
// cutout
let cutoutColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
cutoutColor.setFill()
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
cutoutPath.fill()
// let context = UIGraphicsGetCurrentContext()!
// context.setBlendMode(.sourceOut)
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view
(Edit: I should have stated this initially: this is to ultimately create a UIImage for WatchKit)

With help from How to clear circle in CGContext in iOS
import Foundation
import UIKit
import PlaygroundSupport
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIImageView
{
override init(frame: CGRect) {
super.init(frame: frame)
UIGraphicsBeginImageContextWithOptions(CGSize(width: dimen, height: dimen), false, 1)
let context = UIGraphicsGetCurrentContext()!
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
// Cutout circle
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.addEllipse(in: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
context.drawPath(using: .fill)
context.setBlendMode(.normal)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view

You can do this by using another CAShapeLayer as a mask.
The portion(s) of the mask layer that are alpha = 1.0 will be fully transparent.
So...
If we make the Arc Layer a sublayer of the Ring Layer, we can then apply the Cutout Layer as a mask, resulting in:
Here is source for a Playground page:
class MyDonutView : UIView
{
let ringLayer = CAShapeLayer()
let arcLayer = CAShapeLayer()
let cutoutLayer = CAShapeLayer()
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add arcLayer as a sublayer of ringLayer
ringLayer.addSublayer(arcLayer)
// add ringLayer as a sublayer of self.layer
layer.addSublayer(ringLayer)
// ring layer stroke is black at 0.3 alpha, fill is clear
ringLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3).cgColor
ringLayer.fillColor = UIColor.clear.cgColor
ringLayer.lineWidth = strokeWidth
// arc layer stroke is black at 0.6 alpha, fill is clear
arcLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.6).cgColor
arcLayer.lineWidth = strokeWidth
arcLayer.fillColor = UIColor.clear.cgColor
// cutout layer stroke is black (although we're using Zero line width
// fill is black
cutoutLayer.strokeColor = UIColor.red.cgColor
cutoutLayer.lineWidth = 0
cutoutLayer.fillColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// define the "padding" around the ring
let ringOffset = cutoutWidth / 2.0
// define the diameter of the ring
let circleWidth = bounds.size.width - cutoutWidth;
// ring path
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
// arc path
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
// set ring layer path
ringLayer.path = ringPath.cgPath
// set arc layer path
arcLayer.path = arcPath.cgPath
// create a rect path the full size of bounds of self
let fullPath = UIBezierPath(rect: bounds)
// create a cutout path (the small circle to cut-out of the ring/arc)
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: bounds.size.width-cutoutWidth, y: bounds.size.width/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
// append the cutout path to the full rect path
fullPath.append(cutoutPath)
// even-odd winding rule
cutoutLayer.fillRule = CAShapeLayerFillRule.evenOdd
// set cutout layer path
cutoutLayer.path = fullPath.cgPath
// use cutout layer to mask ring layer
ringLayer.mask = cutoutLayer
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// instantiate a MyDonutView
let myDonutView = MyDonutView()
// we can set the stroke and cutout widths here
myDonutView.strokeWidth = 20.0
myDonutView.cutoutWidth = 30.0
// we're using auto-layout
myDonutView.translatesAutoresizingMaskIntoConstraints = false
// background color yellow to see the frame
//myDonutView.backgroundColor = .yellow
// otherwise, it should be clear
myDonutView.backgroundColor = .clear
// add as subview
view.addSubview(myDonutView)
// constrain centerX and centerY
// width = 200, height = width
NSLayoutConstraint.activate([
myDonutView.widthAnchor.constraint(equalToConstant: 200.0),
myDonutView.heightAnchor.constraint(equalTo: myDonutView.widthAnchor),
myDonutView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myDonutView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
let vc = TestViewController()
PlaygroundPage.current.liveView = vc

Related

I want to use only top border line and right and left top corners . I did it but corners colors does not appear. Can anybody help me?

extension UIView {
func roundCorners(view :UIView, corners: UIRectCorner, radius: CGFloat){
let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
view.layer.mask = mask
path.close()
let color = UIColor.white
color.setStroke()
path.stroke()
}
enum ViewSide: String {
case Left = "Left", Right = "Right", Top = "Top", Bottom = "Bottom"
}
func addBorder(toSide side: ViewSide, withColor color: CGColor, andThickness thickness: CGFloat) {
let border = CALayer()
border.borderColor = color
border.name = side.rawValue
switch side {
case .Left: border.frame = CGRect(x: 0, y: 0, width: thickness, height: frame.height)
case .Right: border.frame = CGRect(x: frame.width - thickness, y: 0, width: thickness, height: frame.height)
case .Top: border.frame = CGRect(x: 0, y: 0, width: frame.width, height: thickness)
case .Bottom: border.frame = CGRect(x: 0, y: frame.height - thickness, width: frame.width, height: thickness)
}
border.borderWidth = thickness
layer.addSublayer(border)
}
func removeBorder(toSide side: ViewSide) {
guard let sublayers = self.layer.sublayers else { return }
var layerForRemove: CALayer?
for layer in sublayers {
if layer.name == side.rawValue {
layerForRemove = layer
}
}
if let layer = layerForRemove {
layer.removeFromSuperlayer()
}
}
}
class TabbarView: UIView {
var viewColor = UIView()
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
addBorder(toSide: .Bottom, withColor:CGColor.init(gray: 100/255, alpha: 100/255), andThickness: 1)
addBorder(toSide: .Top, withColor: CGColor.init(gray: 100/255, alpha: 100/255), andThickness: 1)
addBorder(toSide: .Left, withColor: CGColor.init(gray: 100/255, alpha: 100/255), andThickness: 1)
addBorder(toSide: .Right, withColor: CGColor.init(gray: 100/255, alpha: 100/255), andThickness: 1)
self.roundCorners(view: self, corners: [.topLeft, .topRight], radius: 20)
removeBorder(toSide: .Bottom)
removeBorder(toSide: .Left)
removeBorder(toSide: .Right)
}

How to add shadows to gradient border layer . refer the below image

How to add a shadow effect to this gradient border.
Here is the sample extension to create a border layer with a specified width. When I tried to add a shadow layer whole UI gets affected.
self.gradientBorder(width: 3, colors: UIColor.defaultGradient, andRoundCornersWithRadius: min(bounds.size.height, bounds.size.width))
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0) {
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
width: bounds.size.width + width, height: bounds.size.height + width)
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
Edit
Minor changes from initial code:
background layer doesn't interfere with added subviews
handles resizing correctly (when called in viewDidLayoutSubviews)
You can do this by adding a shadow properties to the view's layer, and adding another layer as a "background" layer.
After Edit... Here is your UIView extension - slightly modified (see the comments):
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
private static let kLayerNameBackgroundLayer = "BackgroundLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0,
bgColor: UIColor = .white,
shadowColor: UIColor = .black,
shadowRadius: CGFloat = 5.0,
shadowOpacity: Float = 0.75,
shadowOffset: CGSize = CGSize(width: 0.0, height: 0.0)
) {
let existingBackground = backgroundLayer()
let bgLayer = existingBackground ?? CALayer()
bgLayer.name = UIView.kLayerNameBackgroundLayer
// set its color
bgLayer.backgroundColor = bgColor.cgColor
// insert at 0 to not cover other layers
if existingBackground == nil {
layer.insertSublayer(bgLayer, at: 0)
}
// use same cornerRadius as border
bgLayer.cornerRadius = cornerRadius
// inset its frame by 1/2 the border width
bgLayer.frame = bounds.insetBy(dx: width * 0.5, dy: width * 0.5)
// set shadow properties
layer.shadowColor = shadowColor.cgColor
layer.shadowRadius = shadowRadius
layer.shadowOpacity = shadowOpacity
layer.shadowOffset = shadowOffset
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.name = UIView.kLayerNameGradientBorder
// don't do this
// border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
// width: bounds.size.width + width, height: bounds.size.height + width)
// use this instead
border.frame = bounds
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func backgroundLayer() -> CALayer? {
let aLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameBackgroundLayer }
if aLayers?.count ?? 0 > 1 {
fatalError()
}
return aLayers?.first
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
After Edit... and here's an example in-use:
class GradBorderViewController: UIViewController {
var topGradView: UIView = UIView()
// make bottom grad view a button
var botGradView: UIButton = UIButton()
var topBkgView: UIView = UIView()
var botBkgView: UIView = UIView()
let embededLabel: UILabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
embededLabel.textColor = .red
embededLabel.textAlignment = .center
embededLabel.text = "Label as subview"
botGradView.setTitle("Button", for: [])
botGradView.setTitleColor(.red, for: [])
botGradView.setTitleColor(.lightGray, for: .highlighted)
topGradView.backgroundColor = .clear
botGradView.backgroundColor = .clear
topBkgView.backgroundColor = .yellow
botBkgView.backgroundColor = UIColor(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)
[topBkgView, topGradView, botBkgView, botGradView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
embededLabel.translatesAutoresizingMaskIntoConstraints = false
// embed label in topGradView
topGradView.addSubview(embededLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// yellow background view on top half, dark-red background view on bottom half
topBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
topBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
botBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
botBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
topBkgView.topAnchor.constraint(equalTo: g.topAnchor),
botBkgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
topBkgView.heightAnchor.constraint(equalTo: g.heightAnchor, multiplier: 0.5),
botBkgView.topAnchor.constraint(equalTo: topBkgView.bottomAnchor),
// each grad border view 75% of width, 80-pt constant height
topGradView.widthAnchor.constraint(equalTo: topBkgView.widthAnchor, multiplier: 0.75),
topGradView.heightAnchor.constraint(equalToConstant: 80.0),
botGradView.widthAnchor.constraint(equalTo: topGradView.widthAnchor),
botGradView.heightAnchor.constraint(equalTo: topGradView.heightAnchor),
// center each grad border view in a background view
topGradView.centerXAnchor.constraint(equalTo: topBkgView.centerXAnchor),
topGradView.centerYAnchor.constraint(equalTo: topBkgView.centerYAnchor),
botGradView.centerXAnchor.constraint(equalTo: botBkgView.centerXAnchor),
botGradView.centerYAnchor.constraint(equalTo: botBkgView.centerYAnchor),
// center the embedded label in the topGradView
embededLabel.centerXAnchor.constraint(equalTo: topGradView.centerXAnchor),
embededLabel.centerYAnchor.constraint(equalTo: topGradView.centerYAnchor),
])
botGradView.addTarget(self, action: #selector(self.testTap(_:)), for: .touchUpInside)
}
#objc func testTap(_ sender: Any?) -> Void {
print("Tapped!")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let a1: [CGFloat] = [173, 97, 222].map({$0 / 255.0})
let a2: [CGFloat] = [0, 198, 182].map({$0 / 255.0})
let c1 = UIColor(red: a1[0], green: a1[1], blue: a1[2], alpha: 1.0)
let c2 = UIColor(red: a2[0], green: a2[1], blue: a2[2], alpha: 1.0)
topGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5
)
botGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5,
shadowColor: .white,
shadowRadius: 12,
shadowOpacity: 0.95,
shadowOffset: CGSize(width: 0.0, height: 0.0)
)
}
}
After Edit... Results:

Adding a mask to CAGradientLayer makes UIBezierPath disappear

I want to add an inner border to a view with a gradient. The following code works and gives me this result
import UIKit
class InnerGradientBorderView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
addGradientInnerBorder(width: 8, color: FlatWhite())
}
func addGradientInnerBorder(width: CGFloat, color: UIColor) {
// Setup
let topLeftO = CGPoint(x: 0, y: 0)
let topLeftI = CGPoint(x: width, y: width)
let topRightO = CGPoint(x: frame.width, y: 0)
let topRightI = CGPoint(x: frame.width - width, y: width)
let bottomLeftO = CGPoint(x: 0, y: frame.height)
let bottomLeftI = CGPoint(x: width, y: frame.height - width)
let bottomRightO = CGPoint(x: frame.width, y: frame.height)
let bottomRightI = CGPoint(x: frame.width - width, y: frame.height - width)
// Top
let topPoints = [topLeftO, topLeftI, topRightI, topRightO, topLeftO]
let topGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: topPoints), color: color, gradientPoints: topGradientPoints)
// Left
let leftPoints = [topLeftO, topLeftI, bottomLeftI, bottomLeftO, topLeftO]
let leftGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: leftPoints), color: color, gradientPoints: leftGradientPoints)
// Right
let rightPoints = [topRightO, topRightI, bottomRightI, bottomRightO, topRightO]
let rightGradientPoints = [CGPoint(x: 1, y: 0), CGPoint(x: 0, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: rightPoints), color: color, gradientPoints: rightGradientPoints)
// Bottom
let bottomPoints = [bottomLeftO, bottomLeftI, bottomRightI, bottomRightO, bottomLeftO]
let bottomGradientPoints = [CGPoint(x: 0, y: 1), CGPoint(x: 0, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: bottomPoints), color: color, gradientPoints: bottomGradientPoints)
}
func addClosedPathForPoints(points: [CGPoint]) -> UIBezierPath? {
guard points.count == 5 else { return nil }
let path = UIBezierPath()
path.move(to: points[0])
path.addLine(to: points[1])
path.addLine(to: points[2])
path.addLine(to: points[3])
path.addLine(to: points[4])
path.close()
return path
}
func addGradientToBeizerPath(path: UIBezierPath?, color: UIColor, gradientPoints: [CGPoint]) {
guard let path = path, gradientPoints.count == 2 else { return }
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [color.cgColor, UIColor.clear.cgColor]
gradient.startPoint = gradientPoints[0]
gradient.endPoint = gradientPoints[1]
// let shapeMask = CAShapeLayer()
// shapeMask.path = path.cgPath
// gradient.mask = shapeMask
self.layer.insertSublayer(gradient, at: 0)
}
}
You will notice that the edges do not look that great.To fix that, I am giving the edges an angle. When I apply a mask to this gradient with this angle, the right and bottom paths disappear like this:
All I am doing here is using some closed bezierPaths and applying a gradient to them. If the gradient has a mask (the commented code is uncommented), two of the paths disappear. I have a feeling that I am not understanding something so hopefully someone here can tell me how to use CAShapeLayer properly.
This comment to CALayer mask property
explains it perfectly:
The mask layer lives in the masked layer's coordinate system just as if it were a sublayer.
In your case, the origin of the right and bottom gradient layer is not
at (0, 0) of the enclosing view, but at (frame.width - width, 0)
and (frame.height - width, 0) respectively.
On the other hand, the coordinates of the points in
oshapeMask.path are relative to (0, 0) of the enclosing view.
A possible simple fix is to transform the coordinate system of
the shape layer so that it uses the same coordinates as the points
of the given path:
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.bounds = path.bounds // <<--- ADDED HERE!
gradient.colors = [color.cgColor, UIColor.clear.cgColor]
gradient.startPoint = gradientPoints[0]
gradient.endPoint = gradientPoints[1]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
self.layer.addSublayer(gradient)

Create CAGradient Layer with transparent hole in it?

Currently I do it like this:
final class BorderedButton: BottomNavigationButton {
private let gradient = CAGradientLayer()
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
let radius = bounds.size.height / 2
let outside = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height), cornerRadius: radius)
let inside = UIBezierPath(roundedRect: CGRect(x: 3.0, y: 3.0, width: bounds.width - 6, height: bounds.height - 6), cornerRadius: radius - 3)
outside.append(inside)
outside.usesEvenOddFillRule = true
shapeLayer.path = outside.cgPath
}
init(color: UIColor?) {
super.init(frame: .zero)
let isGradient = color == nil
shapeLayer.fillRule = kCAFillRuleEvenOdd
shapeLayer.fillColor = UIColor.red.cgColor
layer.insertSublayer(shapeLayer, at: 0)
//gradient part
gradient.colors = isGradient ? [Constants.gradientStart, Constants.gradientEnd] : [color!.cgColor, color!.cgColor]
gradient.startPoint = CGPoint(x: 0.2, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
}
}
How can I apply that gradient to my code?
Don't add the CAShapeLayer to the view's layer, rather set it as the mask of the CAGradientLayer. Also don't forget to set the bounds of the gradient layer.
I had to make some modifications to get it to run in a playground, but this works for me:
let gradientStart = UIColor.orange
let gradientEnd = UIColor.blue
final class BorderedButton: UIButton {
private let gradient = CAGradientLayer()
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
let radius = bounds.size.height / 2
let outside = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height), cornerRadius: radius)
let inside = UIBezierPath(roundedRect: CGRect(x: 3.0, y: 3.0, width: bounds.width - 6, height: bounds.height - 6), cornerRadius: radius - 3)
outside.append(inside)
outside.usesEvenOddFillRule = true
shapeLayer.path = outside.cgPath
gradient.frame = CGRect(x: 0, y: 0, width: bounds.size.width, height: bounds.size.height)
}
init(color: UIColor?, frame: CGRect = .zero) {
super.init(frame: frame)
let isGradient = color == nil
shapeLayer.fillRule = kCAFillRuleEvenOdd
//gradient part
gradient.colors = isGradient ? [gradientStart.cgColor, gradientEnd.cgColor] : [color!.cgColor, color!.cgColor]
gradient.startPoint = CGPoint(x: 0.2, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.mask = shapeLayer
layer.addSublayer(gradient)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How do i call functions of a class - Swift

Im still trying to learn this portion of things. I've looked around and read a few questions about it but truthfully I dont understand any of it.
I've got a circle class that creates and draws a circle
class CircleView: UIView {
var circleLayer: CAShapeLayer!
var isAnimating = false
override init(frame: CGRect) {
let fColor = UIColor.clear.cgColor
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleLayer.lineWidth = 9.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setCircleClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func setCircleCounterClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func formatCirle(circlePath: UIBezierPath) -> CAShapeLayer{
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.clear.cgColor
circleShape.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleShape.lineWidth = 9.0;
circleShape.strokeEnd = 0.0
return circleShape
}
func animate(duration: TimeInterval){
self.isAnimating = true
self.animateCircleFull(duration: 1)
}
func endAnimate(){
self.isAnimating = false
}
func animateCircleFull(duration: TimeInterval) {
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
self.setCircleCounterClockwise()
self.animateCircleEmpty(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
func animateCircleEmpty(duration: TimeInterval){
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1
animation.toValue = 0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 0
CATransaction.setCompletionBlock {
self.setCircleClockwise()
self.animateCircleFull(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
Which is being called from my viewController with the below function. It all works fine but what i cant work out is how do i go about calling the endAnimation function on the same circle?
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
}
For calling endAnimation function on the same circle you have to declare property in your ViewController class.
import UIKit
class ViewController: UIViewController {
var circleView = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
self.addCircleView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
self.circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
//now you are able to call endAnimation function
self.circleView.endAnimate()
}
}

Resources