Swift how to mask shape layer to blur layer - swift

I was making a progress circle, and I want its track path to have a blur effect, is there any way to achieve that?
This is what the original track looks like(the track path is transparent, I want it to be blurred)
And this is my attempt
let outPutViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let circleRadius: CGFloat = 60
let circleViewCenter = CGPoint(x: outPutViewFrame.width / 2 , y: outPutViewFrame.height / 2)
let circleView = UIView()
let progressWidth: CGFloat = 8
circleView.frame.size = CGSize(width: (circleRadius + progressWidth) * 2, height: (circleRadius + progressWidth) * 2)
circleView.center = circleViewCenter
let circleTrackPath = UIBezierPath(arcCenter: CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2), radius: circleRadius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
let blur = UIBlurEffect(style: .light)
let blurEffect = UIVisualEffectView(effect: blur)
blurEffect.frame = circleView.bounds
blurEffect.mask(withPath: circleTrackPath, inverse: false)
extension UIView {
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
maskLayer.path = path.cgPath
maskLayer.lineWidth = 5
maskLayer.lineCap = CAShapeLayerLineCap.round
self.layer.mask = maskLayer
}
}

Set maskLayer.fillRule to evenOdd, even when not inversed.
if inverse {
path.append(UIBezierPath(rect: self.bounds))
}
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
create the circleTrackPath by using a big circle and a smaller circle.
let circleCenter = CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2)
let circleTrackPath = UIBezierPath(ovalIn:
CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius, dy: circleRadius))
// smaller circle
circleTrackPath.append(CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius * 0.8, dy: circleRadius * 0.8))
Set circleTrackPath.usesEvenOddFillRule to true:
circleTrackPath.usesEvenOddFillRule = true
Now you have a blurred full circle. The non-blurred arc part can be implemented as another sublayer.
Here is a MCVE that you can paste into a playground:
let container = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
// change this to a view of your choice
let image = UIImageView(image: UIImage(named: "my_image"))
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .light))
container.addSubview(image)
blur.frame = image.frame
container.addSubview(blur)
let outer = image.bounds.insetBy(dx: 30, dy: 30)
let path = UIBezierPath(ovalIn: outer)
path.usesEvenOddFillRule = true
path.append(UIBezierPath(ovalIn: outer.insetBy(dx: 10, dy: 10)))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskLayer.fillRule = .evenOdd
blur.layer.mask = maskLayer
container // <--- playground quick look this
Using my profile pic as the background, this produces:

Related

CAShapeLayer not rendering as desired

I have a view that's got some CAShapeLayers on it. I'm lazily instantiating it as follows:
lazy var myViewWithLayersOnIt: UIView = {
let view = UIView(frame: CGRect(origin: CGPoint(x: 14,
y: 2),
size: CGSize(width: 10,
height: 10)))
view.translatesAutoresizingMaskIntoConstraints = false
let outerCircleLayer = CAShapeLayer()
let outerPath = UIBezierPath(ovalIn: view.frame).cgPath
outerCircleLayer.path = outerPath
outerCircleLayer.position = view.center
outerCircleLayer.fillColor = UIColor.white.cgColor
view.layer.addSublayer(outerCircleLayer)
let innerFrame = CGRect(origin: CGPoint(x: 1.5,
y: 1.5),
size: CGSize(width: 8.5,
height: 8.5))
let innerCircleLayer = CAShapeLayer()
let innerPath = UIBezierPath(ovalIn: innerFrame).cgPath
innerCircleLayer.path = innerPath
innerCircleLayer.position = view.center
innerCircleLayer.fillColor = UIColor.red.cgColor
view.layer.addSublayer(innerCircleLayer)
return view
}()
While everything that's supposed to render on the screen is rendering, the CAShapeLayers aren't going in the center of the view. I thought it might be due to lazily instantiating, so I ditched lazy and I see the exact same issue. What did I forget to do?
This is how it's rendering:
Thank you for reading. I welcome your input.
You should use view.bounds instead of view.frame for outerPath. No need to set the position of outerCircleLayer, nor innerCircleLayer.
lazy var myViewWithLayersOnIt: UIView = {
let view = UIView(frame: CGRect(origin: CGPoint(x: 14,
y: 2),
size: CGSize(width: 10,
height: 10)))
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.black
let outerCircleLayer = CAShapeLayer()
let outerPath = UIBezierPath(ovalIn: view.bounds).cgPath /// bounds!
outerCircleLayer.path = outerPath
// outerCircleLayer.position = view.center
outerCircleLayer.fillColor = UIColor.white.cgColor
view.layer.addSublayer(outerCircleLayer)
let innerFrame = CGRect(origin: CGPoint(x: 1.5,
y: 1.5),
size: CGSize(width: 8.5,
height: 8.5))
let innerCircleLayer = CAShapeLayer()
let innerPath = UIBezierPath(ovalIn: innerFrame).cgPath
innerCircleLayer.path = innerPath
// innerCircleLayer.position = view.center
innerCircleLayer.fillColor = UIColor.red.cgColor
view.layer.addSublayer(innerCircleLayer)
return view
}()
Result:
The red circle is off center because
let innerFrame = CGRect(origin: CGPoint(x: 1.5,
y: 1.5),
size: CGSize(width: 8.5,
height: 8.5))
should be
let innerFrame = CGRect(origin: CGPoint(x: 0.75,
y: 0.75),
size: CGSize(width: 8.5,
height: 8.5))
8.5 + 0.75 + 0.75 = 10, not 8.5 + 1.5 + 1.5 = 11.5

How to remove arc border in shadowed UIView with rounded / arc path?

I have ticket View like this
.
My method is I have 2 view, 1 is the ticket itself, and other is for shadow. I have to do this because if I mask the view, it got clipped and the shadow will not appear in the ticket view.
here is the code for create the ticket view:
let shapeLayer = CAShapeLayer()
shapeLayer.frame = someView.bounds
shapeLayer.path = UIBezierPath(roundedRect: someView.bounds,
byRoundingCorners: [UIRectCorner.bottomLeft,UIRectCorner.bottomRight] ,
cornerRadii: CGSize(width: 5.0, height: 5.0)).cgPath
let rect = CGRect(x:0, y:0, width:200, height:100)
let cornerRadius:CGFloat = 5
let subPathSideSize:CGFloat = 25
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
let leftSubPath = UIBezierPath(arcCenter: CGPoint(x: rect.width / 2, y: 0),
radius: subPathSideSize / 2, startAngle: .pi, endAngle: .pi * 0, clockwise: false)
leftSubPath.close()
let rightSubPath = UIBezierPath(arcCenter: CGPoint(x: rect.width / 2, y: rect.height),
radius: subPathSideSize / 2, startAngle: .pi, endAngle: .pi * 0, clockwise: true)
rightSubPath.close()
path.append(leftSubPath)
path.append(rightSubPath.reversing())
let mask = CAShapeLayer()
mask.frame = shapeLayer.bounds
mask.path = path.cgPath
someView.layer.mask = mask
Notes: SomeView is the TicketView.
And here is the code for adding shadow:
let shadowMask = CAShapeLayer()
shadowMask.frame = shadowView.bounds
shadowMask.path = path.cgPath
shadowMask.shadowOpacity = 0.2
shadowMask.shadowRadius = 4
shadowMask.masksToBounds = false
shadowMask.shadowOffset = CGSize(width: 0, height: 2)
shadowView.backgroundColor = UIColor.clear
shadowView.layer.addSublayer(shadowMask)
The shadow makes arc/rounded corner have border like this one (marked with circle red).
Here is my Playground gist
Do you know how to remove the border in the rounded corner and arc path?
Thank you.
u need to add clipToBounds in this block that you have written.
let mask = CAShapeLayer()
mask.frame = shapeLayer.bounds
mask.path = path.cgPath
someView.clipsToBounds = true
someView.layer.mask = mask
So I think there is different calculation for corner in mask and path. So I used fillColor of the shadowLayer to match the color of the CouponView. And now, the borders are gone.
shadowLayer.fillColor = someView.backgroundColor.cgColor

How to add rounded corners to a UIView with gradient applied?

I've written custom subclass of UIView that draws a gradient inside of it:
import UIKit
#IBDesignable
class PlayerCellView: UIView {
var startColor: UIColor = UIColor(red:0.20, green:0.75, blue:1.00, alpha:1.00)
var endColor: UIColor = UIColor(red:0.07, green:0.42, blue:1.00, alpha:1.00)
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let colors = [startColor.cgColor, endColor.cgColor]
let locations : [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: locations)
context?.drawLinearGradient(gradient!, start: CGPoint.zero, end: CGPoint(x: 0, y: self.bounds.height), options: .drawsAfterEndLocation)
}
}
How would I now apply rounded edges to this view?
use this extension method:
extension UIView {
func addGradientLayer(with colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint, locations: [NSNumber] = [0.0, 1.0], frame: CGRect = CGRect.zero) {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = colors
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.locations = locations
gradientLayer.frame = frame
gradientLayer.cornerRadius = self.layer.cornerRadius
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
You can add a rounded corner UIView with gradient using the following method:
func makeCircularGradient(){
let circularView = UIView()
self.view.addSubview(circularView)
circularView.translatesAutoresizingMaskIntoConstraints = false
circularView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
circularView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
circularView.widthAnchor.constraint(equalToConstant: 200).isActive = true
circularView.heightAnchor.constraint(equalToConstant: 200).isActive = true
self.view.layoutIfNeeded()
let gradient = CAGradientLayer()
gradient.frame = circularView.bounds
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
circularView.layer.addSublayer(gradient)
let circularPath = CGMutablePath()
circularPath.addArc(center: CGPoint.init(x: circularView.bounds.width / 2, y: circularView.bounds.height / 2), radius: circularView.bounds.width / 2, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true, transform: .identity)
let maskLayer = CAShapeLayer()
maskLayer.path = circularPath
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.fillColor = UIColor.red.cgColor
circularView.layer.mask = maskLayer
}
Make a UIView and set the constraints as you see fit
Make a CAGradientLayer with colours of your choice
Make a circular mask layer and apply to the UIView.
The result will be something like below:
Making a curved corner radius View with gradient is a little bit more difficult than the circular one. And can be done like :
func makeCurvedCornerGradient(){
let circularView = UIView()
self.view.addSubview(circularView)
circularView.translatesAutoresizingMaskIntoConstraints = false
circularView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
circularView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
circularView.widthAnchor.constraint(equalToConstant: 200).isActive = true
circularView.heightAnchor.constraint(equalToConstant: 200).isActive = true
self.view.layoutIfNeeded()
let gradient = CAGradientLayer()
gradient.frame = circularView.bounds
gradient.colors = [UIColor.blue.cgColor,
UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
circularView.layer.addSublayer(gradient)
let circularPath = CGMutablePath()
circularPath.move(to: CGPoint.init(x: 20, y: 0))
circularPath.addLine(to: CGPoint.init(x: circularView.bounds.width - 20, y: 0))
circularPath.addQuadCurve(to: CGPoint.init(x: circularView.bounds.width, y: 20), control: CGPoint.init(x: circularView.bounds.width, y: 0))
circularPath.addLine(to: CGPoint.init(x: circularView.bounds.width, y: circularView.bounds.height - 20))
circularPath.addQuadCurve(to: CGPoint.init(x: circularView.bounds.width - 20, y: circularView.bounds.height), control: CGPoint.init(x: circularView.bounds.width, y: circularView.bounds.height))
circularPath.addLine(to: CGPoint.init(x: 20, y: circularView.bounds.height))
circularPath.addQuadCurve(to: CGPoint.init(x: 0, y: circularView.bounds.height - 20), control: CGPoint.init(x: 0, y: circularView.bounds.height))
circularPath.addLine(to: CGPoint.init(x: 0, y: 20))
circularPath.addQuadCurve(to: CGPoint.init(x: 20, y: 0), control: CGPoint.init(x: 0, y: 0))
let maskLayer = CAShapeLayer()
maskLayer.path = circularPath
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.fillColor = UIColor.red.cgColor
circularView.layer.mask = maskLayer
}
Change the values according to your need.
Output:
You can possibly use the Following Code:
func setGradientBorderWidthColor(view: UIView)
{
let gradientColor = CAGradientLayer()
gradientColor.frame = CGRect(origin: CGPoint.zero, size: view.frame.size)
gradientColor.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
let pathWithRadius = UIBezierPath(roundedRect:view.bounds, byRoundingCorners:[.topRight, .topLeft, .bottomLeft , .bottomRight], cornerRadii: CGSize(width: 20.0, height: 20.0))
let shape = CAShapeLayer()
shape.lineWidth = 3
shape.path = pathWithRadius.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradientColor.mask = shape
view.layer.mask = shape
view.layer.addSublayer(gradientColor)
}
You can use this as:
let myView = UIView()
myView.backgroundColor = UIColor.gray
myView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
setGradientBorderWidthColor(view: myView)
self.view.addSubview(myView)
Output is like this:
Hope this Helps!

How to apply an inverse text mask in Swift?

I am trying programmatically create a layer with transparent text. Everything I try doesn't seem to work. My end goal is to create an inner shadow on text.
Instead of a circle as in the code below I want text.
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .white
// MASK
let blackSquare = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
blackSquare.backgroundColor = .black
view.addSubview(blackSquare)
let maskLayer = CAShapeLayer()
// Create a path with the rectangle in it.
let path = CGMutablePath()
path.addArc(center: CGPoint(x: 100, y: 100), radius: 50, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(x: 0, y: 0, width: blackSquare.frame.width, height: blackSquare.frame.height))
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
blackSquare.layer.mask = maskLayer
maskLayer.masksToBounds = false
maskLayer.shadowRadius = 4
maskLayer.shadowOpacity = 0.5
maskLayer.shadowOffset = CGSize(width: 5, height: 5)
view.addSubview(blackSquare)
I'm also able to use text as a mask but I'm unable to invert the mask. Any help is appreciated.
EDIT:
I figured it out based on Rob's answer as suggested by Josh. Here's my playground code.
import Foundation
import UIKit
import PlaygroundSupport
// view
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .black
// button
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
button.setTitle("120", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNextCondensed-UltraLight", size: 200)
view.addSubview(button)
addInnerShadow(button: button)
func addInnerShadow(button: UIButton) {
// text
let text = button.titleLabel!.text!
// get context
UIGraphicsBeginImageContextWithOptions(button.bounds.size, true, 0)
let context = UIGraphicsGetCurrentContext()
context?.scaleBy(x: 1, y: -1)
context?.translateBy(x: 0, y: -button.bounds.size.height)
let font = button.titleLabel!.font!
// draw the text
let attributes = [
NSAttributedStringKey.font: font,
NSAttributedStringKey.foregroundColor: UIColor.white
]
let size = text.size(withAttributes: attributes)
let point = CGPoint(x: (button.bounds.size.width - size.width) / 2.0, y: (button.bounds.size.height - size.height) / 2.0)
text.draw(at: point, withAttributes: attributes)
// capture the image and end context
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// create image mask
let cgimage = image?.cgImage!
let bytesPerRow = cgimage?.bytesPerRow
let dataProvider = cgimage?.dataProvider!
let bitsPerPixel = cgimage?.bitsPerPixel
let width = cgimage?.width
let height = cgimage?.height
let bitsPerComponent = cgimage?.bitsPerComponent
let mask = CGImage(maskWidth: width!, height: height!, bitsPerComponent: bitsPerComponent!, bitsPerPixel: bitsPerPixel!, bytesPerRow: bytesPerRow!, provider: dataProvider!, decode: nil, shouldInterpolate: false)
// create background
UIGraphicsBeginImageContextWithOptions(button.bounds.size, false, 0)
UIGraphicsGetCurrentContext()!.clip(to: button.bounds, mask: mask!)
view.backgroundColor!.setFill()
UIBezierPath(rect: button.bounds).fill()
let background = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let backgroundView = UIImageView(image: background)
// add shadows
backgroundView.layer.shadowOffset = CGSize(width: 2, height: 2)
backgroundView.layer.shadowRadius = 2
backgroundView.layer.shadowOpacity = 0.75
button.addSubview(backgroundView)
}
PlaygroundPage.current.liveView = view
Whilst not exactly the same, please refer to the answer provided here by Rob who answered a similar question:
How do I style a button to have transparent text?
This should get you started at the very least...
Stumbled on a possible solution that I've updated to proper syntax:
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.view.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.view.layer.mask = maskLayer
}
You'll obviously need to pick parts out to see if it works for you.

iOS Camera Customization: How to implement Grid Lines?

I am able to use the flash feature, access the photo gallery, and use the rear/front camera.
I would like to implement grid lines that display when the user is taking a photo.
Any ideas?
Create a UIImageView to use for the cameraOverlayView.
Assuming you've got a UIImagePickerController named yourImagePickerController and also that you've got an image file named overlay.png as your 'grid lines'. When making your grid line image file, be sure to use a transparent background - not opaque white.
UIImageView *overlayImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"overlay.png"]];
CGRect overlayRect = CGRectMake(0, 0, overlayImage.image.size.width, overlayImage.image.size.height);
[overlayImage setFrame:overlayRect];
[yourImagePickerController setCameraOverlayView:overlayImage];
It's pretty easy with UIBezierPath. This is how you can achieve.
Save this code in a file called GridView.swift.
import Foundation
import UIKit
class GridView: UIView {
override func draw(_ rect: CGRect) {
let firstColumnPath = UIBezierPath()
firstColumnPath.move(to: CGPoint(x: bounds.width / 3, y: 0))
firstColumnPath.addLine(to: CGPoint(x: bounds.width / 3, y: bounds.height))
let firstColumnLayer = gridLayer()
firstColumnLayer.path = firstColumnPath.cgPath
layer.addSublayer(firstColumnLayer)
let secondColumnPath = UIBezierPath()
secondColumnPath.move(to: CGPoint(x: (2 * bounds.width) / 3, y: 0))
secondColumnPath.addLine(to: CGPoint(x: (2 * bounds.width) / 3, y: bounds.height))
let secondColumnLayer = gridLayer()
secondColumnLayer.path = secondColumnPath.cgPath
layer.addSublayer(secondColumnLayer)
let firstRowPath = UIBezierPath()
firstRowPath.move(to: CGPoint(x: 0, y: bounds.height / 3))
firstRowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height / 3))
let firstRowLayer = gridLayer()
firstRowLayer.path = firstRowPath.cgPath
layer.addSublayer(firstRowLayer)
let secondRowPath = UIBezierPath()
secondRowPath.move(to: CGPoint(x: 0, y: ( 2 * bounds.height) / 3))
secondRowPath.addLine(to: CGPoint(x: bounds.width, y: ( 2 * bounds.height) / 3))
let secondRowLayer = gridLayer()
secondRowLayer.path = secondRowPath.cgPath
layer.addSublayer(secondRowLayer)
}
private func gridLayer() -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor(white: 1.0, alpha: 0.3).cgColor
shapeLayer.frame = bounds
shapeLayer.fillColor = nil
return shapeLayer
}
}
Usage:
func addGridView(cameraView: UIView) {
let horizontalMargin = cameraView.bounds.size.width / 4
let verticalMargin = cameraView.bounds.size.height / 4
let gridView = GridView()
gridView.translatesAutoresizingMaskIntoConstraints = false
cameraView.addSubview(gridView)
gridView.backgroundColor = UIColor.clear
gridView.leftAnchor.constraint(equalTo: cameraView.leftAnchor, constant: horizontalMargin).isActive = true
gridView.rightAnchor.constraint(equalTo: cameraView.rightAnchor, constant: -1 * horizontalMargin).isActive = true
gridView.topAnchor.constraint(equalTo: cameraView.topAnchor, constant: verticalMargin).isActive = true
gridView.bottomAnchor.constraint(equalTo: cameraView.bottomAnchor, constant: -1 * verticalMargin).isActive = true
}
As far as the documentation goes it doesn't state whether the grid lines provided by Apple is actually a shared method but as it's not mentioned I'd say not, but you can implement your own with the cameraOverlayView.