How to add shadow to view with mask - swift

I need to add shadow for view with shape aka trapezoid.
here is my code, and it doesn't work.
[![enter image description here][1]][1]let trapezoidPath = UIBezierPath()
trapezoidPath.move(to: CGPoint(x: 0, y: 0))
trapezoidPath.addLine(to: CGPoint(x: Constants.padding, y: bounds.maxY - 10))
trapezoidPath.addLine(to: CGPoint(x: titleLabel.bounds.width + Constants.padding, y: bounds.maxY - 10))
trapezoidPath.addLine(to: CGPoint(x: titleLabel.bounds.width + 2 * Constants.padding, y: 0))
let trapezoidLayer = CAShapeLayer()
trapezoidLayer.path = trapezoidPath.cgPath
whiteView.layer.shadowPath = UIBezierPath(roundedRect: trapezoidPath.bounds, cornerRadius: whiteView.layer.cornerRadius).cgPath
whiteView.layer.shadowRadius = 15
whiteView.layer.shadowOffset = .zero
whiteView.layer.shadowOpacity = 0.4
whiteView.layer.masksToBounds = false
whiteView.layer.shadowColor = UIColor.red.cgColor
whiteView.layer.mask = trapezoidLayer

Nothing outside your mask, including the shadow, will be drawn.
To fix this you need to create two views. The outer view should be transparent and have the shadow properties set. The inner view gets added as a subview to the outer view and has the layer mask configured.
Basically the inner view draws your shape and content and the outer view around it is only responsible for drawing the shadow.

Related

UIView With Pointed Edges

I am trying to make a UIView with pointed edges like this. Did some searching around and found some questions with slanting just one edge like this one but can't find an answer with intersecting (points) edges like the one in the picture that dynamically sizes based on the UIView height.
I used Rob's answer to create something like this:
#IBDesignable
class PointedView: UIView
{
#IBInspectable
/// Percentage of the slant based on the width
var slopeFactor: CGFloat = 15
{
didSet
{
updatePath()
}
}
private let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 0
// with masks, the color of the shape layer doesn’t matter;
// it only uses the alpha channel; the color of the view is
// dictate by its background color
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
override func layoutSubviews()
{
super.layoutSubviews()
updatePath()
}
private func updatePath()
{
let path = UIBezierPath()
// Start from x = 0 but the mid point of y of the view
path.move(to: CGPoint(x: 0, y: bounds.midY))
// Calculate the slant based on the slopeFactor
let slantEndX = bounds.maxX * (slopeFactor / 100)
// Create the top slanting line
path.addLine(to: CGPoint(x: slantEndX, y: bounds.minY))
// Straight line from end of slant to the end of the view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
// Straight line to come down to the bottom, perpendicular to view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
// Go back to the slant end position but from the bottom
path.addLine(to: CGPoint(x: slantEndX, y: bounds.maxY))
// Close path back to where you started
path.close()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
The end result should give you a view close to what you which can be modified on the storyboard
And can also be created using code, here is a frame example since the storyboard showed its compatibility with autolayout
private func createPointedView()
{
let pointedView = PointedView(frame: CGRect(x: 0,
y: 0,
width: 200,
height: 60))
pointedView.backgroundColor = .red
pointedView.slopeFactor = 50
pointedView.center = view.center
view.addSubview(pointedView)
}

Swift - After adding a custom Curve extension, it does not render programmatically created views, only views created with Interface Builder

I have an extension to curve the bottom edge of views since this styling is used on multiple screens in the app I am trying to create.
However, I have noticed I can only make it work with views that I have added trough interface builder. If I try to apply it on view created programmatically they do not render.
I have created a simple example to illustrate the problem. The main storyboard contains two viewControllers with a single colored view in the middle: one created with Interface Builder while the other programmatically.
In StoryboardVC, the view with the curve is rendered correctly without any problem. The setBottomCurve() method is used to create the curve.
If you compare this to setting the entry point to ProgrammaticVC, running the app you can see a plain white screen. Comment this line out to see the view appear again.
This is the extension used:
extension UIView {
func setBottomCurve(curve: CGFloat = 40.0){
self.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - curve
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I expect ProgrammaticVC to look identical to StoryboardVC (except for the difference in color)
The example project can be found here:
https://github.com/belamatedotdotipa/CurveTest2
I suggest to create a subclass instead of using an extension, this is a specific behaviour.
In this case you cannot see the result expected, when you are adding the view programmatically, because in the viewDidLoad you don't have the frame of your view, in this example you can use the draw function:
class BottomCurveView: UIView {
#IBInspectable var curve: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
override func draw(_ rect: CGRect) {
setBottomCurve()
}
private func setBottomCurve() {
let rect = bounds
let y: CGFloat = rect.size.height - curve
let curveTo: CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}

Create UIView with rounded specified borders

I want to create a UIView that has rounded borders which I have specified. For example, I want to create a UIView that has 3 borders: left, top and right, and the topright and topleft borders should be rounded.
This let's me create resizable border views (without rounded corners):
open class ResizableViewBorder: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
open override func draw(_ rect: CGRect) {
let edges = ... get some UIRectEdges here
let lineWidth = 1
if edges.contains(.top) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: 0 + lineWidth / 2),
CGPoint(x: self.bounds.width, y: 0 + lineWidth / 2)
])
}
if edges.contains(.bottom) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: self.bounds.height - lineWidth / 2),
CGPoint(x: self.bounds.width, y: self.bounds.height - lineWidth / 2)
])
}
if (edges.contains(.left) || edges.contains(.all) || edges.contains(.right)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: 0 + lineWidth / 2, y: 0),
CGPoint(x: 0 + lineWidth / 2, y: self.bounds.height)
])
}
if (edges.contains(.right) || edges.contains(.all) || edges.contains(.left)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: self.bounds.width - lineWidth / 2, y: 0),
CGPoint(x: self.bounds.width - lineWidth / 2, y: self.bounds.height)
])
}
}
private func addBezierPath(paths: [CGPoint]) {
let lineWidth = 1
let borderColor = UIColor.black
let path = UIBezierPath()
path.lineWidth = lineWidth
borderColor.setStroke()
UIColor.blue.setFill()
var didAddedFirstLine = false
for singlePath in paths {
if !didAddedFirstLine {
didAddedFirstLine = true
path.move(to: singlePath)
} else {
path.addLine(to: singlePath)
}
}
path.stroke()
}
}
However, I can not find a nice robust way to add a corner radius to a specified corner. I have a hacky way to do it with a curve:
let path = UIBezierPath()
path.lineWidth = 2
path.move(to: CGPoint(x: fakeCornerRadius, y: 0))
path.addLine(to: CGPoint(x: frame.width - fakeCornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: frame.width, y: fakeCornerRadius), controlPoint: CGPoint(x: frame.width, y: 0))
path.addLine(to: CGPoint(x: frame.width, y: frame.height - fakeCornerRadius))
path.stroke()
Which gives me this:
Why is that line of the quad curve so fat? I prefer using UIBezierPaths over CALayers because CALayers have a huge performance impact...
What's wrong with your curve-drawing code is not that the curve is fat but that the straight lines are thin. They are thin because they are smack dab on the edge of the view. So your line width is 2 points, but one of those points is outside the view. And points are not pixels, so what pixels are there left to fill in? Only the ones inside the view. So the straight lines have an apparent visible line width of 1, and only the curve has a visible line width 2.
Another problem is that you probably are looking at this app running in the simulator on your computer. But there is a mismatch between the pixels of the simulator and the pixels of your computer monitor. That causes numerous drawing artifacts. The way to examine the drawing accurately down to the pixel level is to use the simulator application's screen shot image facility and look at the resulting image file, full-size, in Preview or similar. Or run on a device and take the screen shot image there.
To demonstrate this, I modified your code to operate in an inset version of the original rect (which, by the way, should be your view's bounds, not its frame):
let fakeCornerRadius : CGFloat = 20
let rect2 = self.bounds.insetBy(dx: 2, dy: 2)
let path = UIBezierPath()
path.lineWidth = 2
path.move(
to: CGPoint(x: rect2.minX + fakeCornerRadius, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX - fakeCornerRadius, y: rect2.minY))
path.addQuadCurve(
to: CGPoint(x: rect2.maxX, y: rect2.minY + fakeCornerRadius),
controlPoint: CGPoint(x: rect2.maxX, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX, y: rect2.maxY - fakeCornerRadius))
path.stroke()
Taking a screen shot from within the Simulator application, I got this:
As you can see, this lacks the artifacts of your screen shot.

How to cut edges of a uiview in swift

I attached the View image. I want to achieve the small cut in bottom between the buy button and above flight information view.
I think the easiest way would be to create 2 circles as plain UIView instances and set their center as the left and right edges of the parent view respectively.
Since you set clipsToBounds to true, they will be clipped and only half of them will be visible on the screen.
public class TestView: UIView {
private let leftCircle = UIView(frame: .zero)
private let rightCircle = UIView(frame: .zero)
public var circleY: CGFloat = 0
public var circleRadius: CGFloat = 0
public override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(leftCircle)
addSubview(rightCircle)
}
public override func layoutSubviews() {
super.layoutSubviews()
leftCircle.frame = CGRect(x: -circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
leftCircle.layer.masksToBounds = true
leftCircle.layer.cornerRadius = circleRadius
rightCircle.frame = CGRect(x: bounds.width - circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
rightCircle.layer.masksToBounds = true
rightCircle.layer.cornerRadius = circleRadius
}
}
I've created a sample project demonstrating that. Here is how it looks in my simulator (iPhone SE 11.2):
I had to do this with shadows. I tried creating a layer and subtracting another layer from it using evenOdd fillRule, however that didn't work since I need a specific path for shadows and evenOdd applies to the filling in the path instead.
In the end I just created the path manually
func setShadowPath() {
let path = UIBezierPath()
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: cutoutView.frame.minX, y: bounds.minY))
path.addArc(withCenter: CGPoint(x: cutoutView.frame.midX, y: bounds.minY),
radius: cutoutView.bounds.width/2, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
layer.shadowPath = path.cgPath
}
I created a "cutoutView" in my xib so I could trace around it easily.
That makes the shadow the correct shape, and then in order to create the cut itself I just created a layer using that same path
func setupBackground() {
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = layer.shadowPath
backgroundLayer.fillColor = UIColor.white.cgColor
layer.insertSublayer(backgroundLayer, at: 0)
}

CATransform3DRotate applied to layer doesnt work

Could you please advice why CATransform3DRotate doesnt work for layer in my case, its blue layer on the image.
I have custom view, where I draw nature view, I want to animate changing the moon phase that will be done inside the white circle layer as its mask. I suppose that it is good idea to apply here 3DRotation, but for some reason it doesn't work even without animation, could be please advice what I am doing wrong?
func drawMoonPhase(inRect rect:CGRect, inContext context: CGContext) {
let moonShape = UIBezierPath(ovalIn: rect)
moonShape.lineWidth = 4.0
UIColor.white.setStroke()
moonShape.stroke()
moonShape.close()
let moonLayer = CAShapeLayer()
moonLayer.path = moonShape.cgPath
moonLayer.opacity = 0
self.layer.addSublayer(moonLayer)
let circlePath = UIBezierPath(ovalIn: rect)
UIColor.blue.setFill()
circlePath.fill()
circlePath.close()
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.opacity = 0
var transform = CATransform3DIdentity
transform.m34 = -1 / 500.0
transform = CATransform3DRotate(transform, (CGFloat(Double.pi * 0.3)), 0, 1, 0)
circleShape.transform = transform
moonLayer.mask = circleShape
}
Thanks in advance
EDIT:
Maybe I want clear , the effect i want is the below:
The transform occurs around the layer's anchor point that by default is in the center. Therefore what it is happening there is that the shape rotates around itself causing no visible result. :)
what you should do in this layout of layer is to use cos and sin math functions in order to determine the x and y position of your moon.
Let me know if you need more insights I will be happy to help.
also, please note that you don't need 2 shapeLayers in order to have the blue moon with the white stroke. CAShapeLayer has properties for both fill and stroke so you can simplify your code.
Based on the new info here is my new answer:
I was not able to get a nice effect by using the transform, so I decided to write the mask manually. this is the result:
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.black.cgColor
return mask
}
The function above draws a shapelier that can be used as mask for your moonLayer. This layer will be drawnin relation to a progress parameter that you will pass in the function where 1 is full moon and 0 is new moon.
You can put everything together to have the desired effect, and you can extract the path creation code to make a nice animation if you want.
This should answer your question I hope.
To quick test I wrote this playground:
import UIKit
let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
let view = UIView(frame: rect)
view.backgroundColor = .black
let layer = view.layer
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.white.cgColor
return mask
}
let moonLayer = CAShapeLayer()
moonLayer.path = UIBezierPath(ovalIn: rect).cgPath
moonLayer.fillColor = UIColor.clear.cgColor
moonLayer.strokeColor = UIColor.white.cgColor
moonLayer.lineWidth = 2
moonLayer.shadowColor = UIColor.white.cgColor
moonLayer.shadowOpacity = 1
moonLayer.shadowRadius = 10
moonLayer.shadowPath = moonLayer.path
moonLayer.shadowOffset = .zero
layer.addSublayer(moonLayer)
let moonPhase = moonMask(in: rect, forProgress: 0.3)
moonPhase.shadowColor = UIColor.white.cgColor
moonPhase.shadowOpacity = 1
moonPhase.shadowRadius = 10
moonPhase.shadowPath = moonLayer.path
moonPhase.shadowOffset = .zero
layer.addSublayer(moonPhase)
view