Change drawing order of multiple CAShapeLayers - swift

I have my program drawing several different CGMutablePaths, each of which belongs to its own CAShapeLayer. It currently looks like this
Notice how the lines of the rectangle overlap the circles. Here is how I want it to look
Essentially, I want the ellipses and their fill to be drawn on top of the lines of the rectangle.
In my code, I have an array of CAShapeLayers, the first of which is the rectangle. In awakeFromNib(), I loop through all the shape layers and update information like the stroke width and fill, then I manually change the fill color of the rectangle to be different. In the draw function, I create all my paths, rectangle first, then ellipses. Finally, I add those paths to their relative shape layer, ellipses after the rectangle.
I have tried swapping the order in each case, but no luck. I honestly can't think of anything else that might be effecting draw order. The strangest part about it is that, in the first image, you'll see that the bottom left ellipse has indeed been drawn above the rectangle, but nowhere in my code is it set apart.
var shapeLayers: [String: CAShapeLayer] = [
"rectangle": CAShapeLayer(),
"ellipse1": CAShapeLayer(),
"ellipse2": CAShapeLayer()...and so on
]
override func awakeFromNib() {
wantsLayer = true
for (_, shapeLayer) in shapeLayers {
shapeLayer.lineWidth = borderWidth
shapeLayer.strokeColor = ColorScheme.blue.cgColor
shapeLayer.fillColor = ColorScheme.white.cgColor
shapeLayer.path = nil
shapeLayer.lineCap = kCALineCapSquare
layer?.addSublayer(shapeLayer)
}
shapeLayers["rectangle"]?.fillColor = NSColor.clear.cgColor
}
override func draw(_ dirtyRect: NSRect) {
let rectangle = CGMutablePath().addRect(selectionBounds)
let ellipse1 = CGMutablePath().addEllipse(in: CGRect(origin: somePoint, size: someSize))
let ellipse2 = CGMutablePath().addEllipse(in: CGRect(origin: somePoint, size: someSize))
...and so on
shapeLayers["rectangle"]?.path = rectangle
shapeLayers["ellipse1"]?.path = ellipse1
shapeLayers["ellipse2"]?.path = ellipse2...and so on
}
I'm clearly missing something basic in my understanding of shapeLayers and paths. Your help is greatly appreciated - TIA

You can use the zPosition to force the layers to draw in a specific order. Just assign them arbitrary values as long as the numbers ascend from back layer to front layer.

Related

Swift Combine and reverse UIBezierPaths

The problem I am facing is reversing subpaths. Take this example:
let circlePaths: [UIBezierPath] = ...
let rectanglePath: UIBezierPath = ... // a rectangle
let totalPath: UIBezierPath = .init()
for path in circlePaths {
totalPath.append(path)
}
rectanglePath.append(totalPath)
It should look like this:
Now ideally I want to cut out all the circles using
bezierPath.append(totalPath.reversing())
However the effect is not as expected. I expect the two circles to make up a path and this one is reversed, however in reality both circle paths are reversed, which causes the intersection to be part of the path (reversing() twice has no effect). I'd like to combine the circle paths into one with the intersection not being present but as part of the path. I want the smaller circle to "extend" the larger circle as a path.
Any idea how I would do it?
Edit 1: Here is an image how the resulting path should look like.
If you need to actually create a single path as the combination / union of your multiple paths, you may want to look at one of the libraries that are out there.
However, if you only need that visual output, this might be a usable approach.
Create 3 paths -- outer rect, large circle, small circle.
Stroke and Fill the outer rect path
Stroke both circle paths
Fill both circle paths
An example UIView class:
class FakeUnionUIBezierPaths : UIView {
override func draw(_ rect: CGRect) {
// yellow line width
let lWidth: CGFloat = 8
// paths are stroked on the center, so
// use one-half the line width to adjust ediges
let hWidth: CGFloat = lWidth * 0.5
// border / outer rect, inset by one-half line width
let dRect: CGRect = rect.insetBy(dx: hWidth, dy: hWidth)
// first circle
// full-height
// aligned to left edge
var c1Rect: CGRect = dRect
c1Rect.size.width = c1Rect.height
// second circle
// half-height
// aligned to right edge
var c2Rect: CGRect = dRect
c2Rect.size.height *= 0.5
c2Rect.size.width = c2Rect.height
c2Rect.origin.x = dRect.width - c2Rect.width + hWidth
c2Rect.origin.y = dRect.height * 0.25
let pRect: UIBezierPath = UIBezierPath(rect: dRect)
let p1: UIBezierPath = UIBezierPath(ovalIn: c1Rect)
let p2: UIBezierPath = UIBezierPath(ovalIn: c2Rect)
UIColor.yellow.setStroke()
UIColor.blue.setFill()
// same line-width for all three paths
[pRect, p1, p2].forEach { p in
p.lineWidth = lWidth
}
// stroke and fill the border / outer rect
pRect.stroke()
pRect.fill()
// stroke both circle paths
p1.stroke()
p2.stroke()
// fill both circle paths
p1.fill()
p2.fill()
}
}
Output:

swift CAShapeLayer strokeColor by gradient color

hi i have a circle shapeLayer need color by GradientColor
i tried ues CAGradientLayer and mask it like this
let gradientChargeLayer = CAGradientLayer()
gradientChargeLayer.colors = CGColor.mColor
gradientChargeLayer.frame = vShape.bounds
gradientChargeLayer.locations = [0,1]
gradientChargeLayer.startPoint = CGPoint(x: 0,y: 0.5)
gradientChargeLayer.endPoint = CGPoint(x: 1,y: 0.5)
gradientChargeLayer.mask = shapeLayer
vShape.layer.addSublayer(gradientChargeLayer)
but what i want is gradient color from stroke start to end
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor <<< here
thanks
you could’ve different approaches I believe
One, you could make an outer and inner circle and then add the required path/layer to the outer circle (this circle bigger than inner) both circles without visible stroke
Or I could recommend you UIGraphigsImageRenderer to work with
https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer
This last have a context that could be used to do more advanced rendering context.cgContext.setStrokePattern here you can add a CGPattern and colors to trying accomplish it
PD. Sorry if my english is confuse
Shape layers only draw in a single color, as I guess you are aware.
By making your shape layer a mask onto a gradient layer you're able to have the shape layer reveal the color of the underlying gradient layer.
If you are always drawing a circle and want the color of the circle's stroke to vary from beginning to end you could set up your gradient layer using a type of conic, and specifying the colors in the gradient layer to begin and end where you want them. However, that would only work for shape layers that contain ovals, where the size and center of the shape and the size and the center of the gradient match.
As #Guillodacosta said in their answer, you may have to give up on using a shape layer and draw your graphic into a graphics context using Core Graphics. (You could do that once and capture the results into a UIImageView to avoid the slowdown of Core Graphics rendering.)

iOS Fill half of UIBezierPath with other color without CAGradientLayer

I have a question about drawing half of a UIBezierPath. How do I fill left part (left from thumb) with green color and right part (right from thumb) with white color without using CAGradientLayer?
Code I used to create Bezier Path - https://gist.github.com/robertmryan/67484c74297cede3926a3aed2fceedb9
Screenshot of what I want to achieve:
One approach is to add a mask layer to your curved-path shape layer.
When the "thumb" position changes, change the width of the mask to reveal only the "left-side" of the shape layer.
Create a shape layer to use as the mask:
let maskLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.black.cgColor
return layer
}()
In viewDidLoad() set that layer as the mask for the curved-shape layer:
pathLayer.mask = maskLayer
Whenever the "thumb" position is set, update the width of the mask:
func updateMask(at point: CGPoint) -> Void {
var f = view.bounds
f.size.width = point.x
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.frame = f
CATransaction.commit()
}
I posted a modified version of your gist at: https://gist.github.com/DonMag/a2154e70a3c67193a7b19bee41c8fe95
It really has only a few changes... look for comments beginning with // DonMag -
Here is the result (with an imageView behind it to show the transparency):
Edit
After comments, the goal is to have the "right-side" of the track path be white instead of transparent.
Using the same approach, we can add a white shape layer on top of the original shape layer, and mask it to show only the right-hand-side.
Here is an updated gist - https://gist.github.com/DonMag/397dfbe4779e817531ef7a663365b2e7 - showing this result:

uibezierpath with multiple line width and a background image (swift)

I'm trying to draw several shapes (rectangle, triangle, circle, ...) in swift and for that, I use uibezierpath but I am not able to draw exactly what I want.
I need to draw for example a rectangle, but the borders of this rectangle need to have different line width.
To do that, I create different path then use the "appendpath" to merge them in one path. So that works, BUT I also need to have a background image in this rectangle.
For that, I create a layer and set it an image. The issue is that, no background image are displayed when I use "appendpath", certainly because it doesn't recognize my drawing as a rectangle.
I hope it is clear enough, but is there a way to draw a shape with a background image, and have different border width ?
Thanks for your help !!
There are two solutions I'd suggest you try:
1) Masking
Create a normal CALayer and set the image as its contents. Then create a CAShapeLayer with the path you like and use it as the first layer's mask.
E.g.:
let imageLayer = CALayer()
imageLayer.contents = UIImage(named: "yourImage")?.CGImage // Your image here
imageLayer.frame = ... // Define a frame
let maskPath = UIBezierPath(...) // Create your path here
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
imageLayer.mask = maskLayer
Don't forget to set the right frames and paths, and you should be able to achieve the effect you wanted.
2) Fill color
Create a CAShapeLayer with the path you like, then use your image as its fillColor.
E.g.:
let path = UIBezierPath(...) // Create your path here
let layer = CAShapeLayer()
layer.path = path.CGPath
let image = UIImage(named: "yourImage") // Your image here
layer.fillColor = UIColor(patternImage: image!).CGColor
You may find this approach easier at first, but controlling the way the image fills your shape is not trivial at all.
I hope this will help.
If you'd like more details, please provide an image or a sketch of what you're trying to achieve and / or the code you've written so far. Thanks!

Borders not covering background

I've got a UILabel is using a border the same color as a background which it is half obscuring, to create a nice visual effect. However the problem is that there is still a tiny, yet noticeable, sliver of the label's background color on the OUTSIDE of the border.
The border is not covering the whole label!
Changing the border width doesn't change anything either, sadly.
Here's a picture of what's going on, enlarged so you can see it:
And my code follows:
iconLbl.frame = CGRectMake(theWidth/2-20, bottomView.frame.minY-20, 40, 40)
iconLbl.font = UIFont.fontAwesomeOfSize(23)
iconLbl.text = String.fontAwesomeIconWithName(.Info)
iconLbl.layer.masksToBounds = true
iconLbl.layer.cornerRadius = iconLbl.frame.size.width/2
iconLbl.layer.borderWidth = 5
iconLbl.layer.borderColor = topBackgroundColor.CGColor
iconLbl.backgroundColor = UIColor.cyanColor()
iconLbl.textColor = UIColor.whiteColor()
Is there something I'm missing?
Or am I going to have to figure out another to achieve this effect?
Thanks!
EDIT:
List of things I've tried so far!
Changing layer.borderWidth
Fussing around with clipsToBounds/MasksToBounds
Playing around the the layer.frame
Playing around with an integral frame
EDIT 2:
No fix was found! I used a workaround by extending this method on to my UIViewController
func makeFakeBorder(inputView:UIView,width:CGFloat,color:UIColor) -> UIView {
let fakeBorder = UIView()
fakeBorder.frame = CGRectMake(inputView.frame.origin.x-width, inputView.frame.origin.y-width, inputView.frame.size.width+width*2, inputView.frame.size.height+width*2)
fakeBorder.backgroundColor = color
fakeBorder.clipsToBounds = true
fakeBorder.layer.cornerRadius = fakeBorder.frame.size.width/2
fakeBorder.addSubview(inputView)
inputView.center = CGPointMake(fakeBorder.frame.size.width/2, fakeBorder.frame.size.height/2)
return fakeBorder
}
I believe this is the way a border is drawn to a layer in iOS. In the document it says:
When this value is greater than 0.0, the layer draws a border using the current borderColor value. The border is drawn inset from the receiver’s bounds by the value specified in this property. It is composited above the receiver’s contents and sublayers and includes the effects of the cornerRadius property.
One way to fix this is to apply a mask to a view's layer, but I found out that even if so we still can see a teeny tiny line around the view when doing snapshot tests. So to fix it more, I put this code to layoutSubviews
class MyView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let maskInset: CGFloat = 1
// Extends the layer's frame.
layer.frame = layer.frame.inset(dx: -maskInset, dy: -maskInset)
// Increase the border width
layer.borderWidth = layer.borderWidth + maskInset
layer.cornerRadius = bounds.height / 2
layer.maskToBounds = true
// Create a circle shape layer with true bounds.
let mask = CAShapeLayer()
mask.path = UIBezierPath(ovalIn: bounds.inset(dx: maskInset, dy: maskInset)).cgPath
layer.mask = mask
}
}
CALayer's mask