Adding the same layer in a subview changes its visual position - ios

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?

#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.

Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

Related

How to add a custom shape to an UIImageView in Swift?

I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}

Why the edges of UIView are not smooth for huge corner radius?

class AttributedView: UIView {
private let gradient = CAGradientLayer()
var cornerRadius: CGFloat = 3 {
didSet {
layer.cornerRadius = cornerRadius
}
}
}
I simply use it for both: button view (corner radius: 20) and background circle (corner radius: 600).
Why button is smooth, and background is not?
With iOS 13.0 you can simple do, in addition to setting corner radius
yourView.layer.cornerCurve = .continuous
You should use bezzier paths and draw circle. After that you will receive nice, smooth edges.
let context = UIGraphicsGetCurrentContext()!
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.white.cgColor] as CFArray, locations: [0, 1])!
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 64, y: 9, width: 111, height: 93))
context.saveGState()
ovalPath.addClip()
context.drawLinearGradient(gradient, start: CGPoint(x: 119.5, y: 9), end: CGPoint(x: 119.5, y: 102), options: [])
context.restoreGState()
UIBezierPath is a simple and efficient class for drawing shapes using Swift, which you can then put into CAShapeLayer, SKShapeNode, or other places. It comes with various shapes built in, so you can write code like this to create a rounded rectangle or a circle:
let rect = CGRect(x: 0, y: 0, width: 256, height: 256)
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let circle = UIBezierPath(ovalIn: rect)
You can also create custom shapes by moving a pen to a starting position then adding lines:
let freeform = UIBezierPath()
freeform.move(to: .zero)
freeform.addLine(to: CGPoint(x: 50, y: 50))
freeform.addLine(to: CGPoint(x: 50, y: 150))
freeform.addLine(to: CGPoint(x: 150, y: 50))
freeform.addLine(to: .zero)
If your end result needs a CGPath, you can get one by accessing the cgPath property of your UIBezierPath.
You probably should clip bounds of this view:
attributedView.clipsToBounds = true

Drawing a 3D cone shape in Swift using a UIBezierPath() and CAShapeLayer()

Background
I’m attempting to draw a 3D cone shape as shown in the image below.
I have a method that I use to draw a simple triangle and fill it with a solid color in Swift using a UIBezierPath() and CAShapeLayer().
Question
In Swift code, how can I draw a 3D cone shape or fill the triangle
shape I’ve drawn with a complex gradient that gives the triangle shape
a 3D effect and effectively a cone appearance?
Code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 100))
path.addLineToPoint(CGPoint(x: 200, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 0))
path.closePath()
let shape = CAShapeLayer()
shape.path = path.CGPath
shape.fillColor = UIColor.grayColor().CGColor
myView.layer.insertSublayer(shape, atIndex: 0)
}
}
Image
I was able to achieve your goal making use of CGGradientLayer with a type of conic. The trick is to use the triangle shape as a mask for the gradient.
Here is an example that can be run in a Swift Playground. Comments in the code.
import UIKit
import PlaygroundSupport
class ConeView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
// Just for display
backgroundColor = .yellow
// Create the triangle to be used as the gradient's mask
// Base the size of the triangle on the view's frame
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: frame.height))
path.addLine(to: CGPoint(x: frame.width, y: frame.height))
path.addLine(to: CGPoint(x: frame.width / 2, y: 0))
path.close()
// Create a shape from the path
let shape = CAShapeLayer()
shape.path = path.cgPath
// Create a conical gradient and mask it with the triangle shape
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: -0.5)
gradientLayer.type = .conic
// Change the white value of the center color to adjust the center highlight brightness as desired
gradientLayer.colors = [UIColor.black.cgColor, UIColor(white: 0.9, alpha: 1.0).cgColor, UIColor.black.cgColor]
// Tweak the 1st and 3rd number as desired.
// The closer to 0.5 the darker the cone edges will be.
// The further from 0.5 the lighter the cone edges will be.
gradientLayer.locations = [ 0.38, 0.5, 0.62 ]
gradientLayer.frame = bounds
gradientLayer.mask = shape
layer.insertSublayer(gradientLayer, at: 0)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Set whatever size you want for the view
PlaygroundPage.current.liveView = ConeView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
Here's the result (the black border is not part of the view, just part of the screen capture):

Playground code: AffineTransform and AnchorPoint don't work as I want

I want to shrink a triangle downward as below:
But the triangle shrink upward as below:
The transform code is below:
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
I tried to use anchorPoint but I can't scceed.
//: Playground - noun: a place where people can play
import UIKit
let view = UIView(frame: CGRectMake(0, 0, 200, 150))
let layer = CAShapeLayer()
let triangle = UIBezierPath()
triangle.moveToPoint (CGPointMake( 50, 150))
triangle.addLineToPoint(CGPointMake(100, 50))
triangle.addLineToPoint(CGPointMake(150, 150))
triangle.closePath()
layer.path = triangle.CGPath
layer.strokeColor = UIColor.blueColor().CGColor
layer.lineWidth = 3.0
view.layer.addSublayer(layer)
view
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
view
Anish gave me a advice but doesn't work well:
layer.setAffineTransform(CGAffineTransformMakeScale(2.0, 0.5))
Transforms operate around the layer's anchorPoint, which by default is at the center of the layer. Thus, you shrink by collapsing inwards, not by collapsing downwards.
If you want to shrink downward, you will need to scale down and change the view's position (or use a translate transform in addition to your scale transform), or change the layer's anchorPoint before performing the transform.
Another serious problem with your code is that your layer has no assigned size. This causes the results of your transform to be very misleading.
I ran this version of your code and got exactly the results you are asking for. I have put a star next to the key lines that I added. (Note that this is Swift 3; you really need to step up to the plate and update here.)
let view = UIView(frame:CGRect(x: 0, y: 0, width: 200, height: 150))
let layer = CAShapeLayer()
layer.frame = view.layer.bounds // *
let triangle = UIBezierPath()
triangle.move(to: CGPoint(x: 50, y: 150))
triangle.addLine(to: CGPoint(x: 100, y: 50))
triangle.addLine(to: CGPoint(x: 150, y: 150))
triangle.close()
layer.path = triangle.cgPath
layer.strokeColor = UIColor.blue.cgColor
layer.lineWidth = 3
view.layer.addSublayer(layer)
view
layer.anchorPoint = CGPoint(x: 0.5, y: 0) // *
layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 0.5))
view
Before:
After:

Draw shadow only from 3 sides of UIView

I've successfully implemented drawing a shadow around my UIView like this:
block1.layer.masksToBounds = NO;
block1.layer.shadowOffset = CGSizeMake(0, 0);
block1.layer.shadowRadius = 1;
block1.layer.shadowOpacity = 0.7;
What now happens is I have a rectangular UIView and i would like to draw the shadow around it three sides, leaving the bottom side of it without the shadow.
I know that I have to specify the block1.layer.shadowPath by creating a new UIBezierPath but I'm not sure how to do it.
Obviously, setting layer.shadowOffset won't do the trick for me.
Thanks in advance!
I know you say setting layer.shadowOffset won't work for you, but you are allowed to put in negative values so setting it layer.shadowOffset = CGSizeMake(0.0, -2.0) would come close to the effect you're looking for but of course I expect you want it to be even on the three sides.
So here we go with layer.shadowPath!
UIView *block1 = [[UIView alloc] initWithFrame:CGRectMake(32.0, 32.0, 128.0, 128.0)];
[block1 setBackgroundColor:[UIColor orangeColor]];
[self.view addSubview:block1];
block1.layer.masksToBounds = NO;
block1.layer.shadowOffset = CGSizeMake(0, 0);
block1.layer.shadowRadius = 1;
block1.layer.shadowOpacity = 0.7;
UIBezierPath *path = [UIBezierPath bezierPath];
// Start at the Top Left Corner
[path moveToPoint:CGPointMake(0.0, 0.0)];
// Move to the Top Right Corner
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame), 0.0)];
// Move to the Bottom Right Corner
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame), CGRectGetHeight(block1.frame))];
// This is the extra point in the middle :) Its the secret sauce.
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame) / 2.0, CGRectGetHeight(block1.frame) / 2.0)];
// Move to the Bottom Left Corner
[path addLineToPoint:CGPointMake(0.0, CGRectGetHeight(block1.frame))];
// Move to the Close the Path
[path closePath];
block1.layer.shadowPath = path.CGPath;
And to give you an idea of whats going on, here is the actual shadow path you just drew :)
Its possible to just shift that extra middle point before or after the other lines to choose which side will be omitted.
A bit of improvement for other answers, thanks to Ashok R for swift code.
Since we were creating a triangular view in the background of the view with shadow on all sides, and a white triangle on the sides shadow is not needed.
It breaks in case of views with width comparatively larger than height.
A workaround will be to shift the path for the line where shadow is not needed a bit towards that side of view, instead of creating the triangular view Path completely.
I have created an extension for that -
extension UIView {
func addshadow(top: Bool,
left: Bool,
bottom: Bool,
right: Bool,
shadowRadius: CGFloat = 2.0) {
self.layer.masksToBounds = false
self.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
self.layer.shadowRadius = shadowRadius
self.layer.shadowOpacity = 1.0
let path = UIBezierPath()
var x: CGFloat = 0
var y: CGFloat = 0
var viewWidth = self.frame.width
var viewHeight = self.frame.height
// here x, y, viewWidth, and viewHeight can be changed in
// order to play around with the shadow paths.
if (!top) {
y+=(shadowRadius+1)
}
if (!bottom) {
viewHeight-=(shadowRadius+1)
}
if (!left) {
x+=(shadowRadius+1)
}
if (!right) {
viewWidth-=(shadowRadius+1)
}
// selecting top most point
path.move(to: CGPoint(x: x, y: y))
// Move to the Bottom Left Corner, this will cover left edges
/*
|☐
*/
path.addLine(to: CGPoint(x: x, y: viewHeight))
// Move to the Bottom Right Corner, this will cover bottom edge
/*
☐
-
*/
path.addLine(to: CGPoint(x: viewWidth, y: viewHeight))
// Move to the Top Right Corner, this will cover right edge
/*
☐|
*/
path.addLine(to: CGPoint(x: viewWidth, y: y))
// Move back to the initial point, this will cover the top edge
/*
_
☐
*/
path.close()
self.layer.shadowPath = path.cgPath
}
and set the boolean true for whichever side you want the shadow to appear
myView.addshadow(top: false, left: true, bottom: true, right: true, shadowRadius: 2.0)
// shadow radius is optional above and is set as default at 2.0
or
myView.addshadow(top: true, left: true, bottom: true, right: true, shadowRadius: 2.0)
or
myView.addshadow(top: false, left: false, bottom: true, right: true, shadowRadius: 2.0)
Updating Ryan Poolos Answer to Swift 3.0
Thanks to Ryan Poolos
class sampleViewController: UIViewController {
var block1: UIView! = nil
override func viewDidLoad() {
super.viewDidLoad()
block1 = UIView(frame: CGRect(x: 32.0, y: 32.0, width: 128.0, height: 128.0))
block1.backgroundColor = UIColor.orange
self.view.addSubview(block1)
block1.layer.masksToBounds = false
block1.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
block1.layer.shadowRadius = 1.0
block1.layer.shadowOpacity = 0.7
let path = UIBezierPath()
// Start at the Top Left Corner
path.move(to: CGPoint(x: 0.0, y: 0.0))
// Move to the Top Right Corner
path.addLine(to: CGPoint(x: block1.frame.size.width, y: 0.0))
// Move to the Bottom Right Corner
path.addLine(to: CGPoint(x: block1.frame.size.width, y: block1.frame.size.height))
// This is the extra point in the middle :) Its the secret sauce.
path.addLine(to: CGPoint(x: block1.frame.size.width/2.0, y: block1.frame.size.height/2.0))
// Move to the Bottom Left Corner
path.addLine(to: CGPoint(x: 0.0, y: block1.frame.size.height))
path.close()
block1.layer.shadowPath = path.cgPath
}
}
Result:
Try this
extension CALayer {
func applySketchShadow(color: UIColor, alpha: CGFloat, x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat)
{
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2.0
if spread == 0 {
shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
}

Resources