I have a UIView in which I use a bezierPath to form it's shape. I want to create a duplicate view, but of a different color. The same shape is created, but the color doesn't change with the following code. Any idea why?
let whiteWave = coloredWave
let drawingLayer = CAShapeLayer()
drawingLayer.path = coloredWave.aPath.cgPath
drawingLayer.strokeColor = UIColor.white.cgColor
drawingLayer.lineWidth = audioClip.widthOfClip
drawingLayer.fillColor = UIColor.white.cgColor
whiteWave.layer.addSublayer(drawingLayer)
Edit: Okay so after some digging I found that the drawRect finishes after I set the whiteWave. I tried using a CATransactionblock and setting the new wave in the completion block. But that doesn't work either.
How can I wait for drawRect in the UIView to finish?
To the best of my knowledge it's because you are using the same names for both shapes. Try doing let whiteWave2 for the second shape.
Related
I'm attempting to create a CAShapeLayer animation that draws an outline around the frame of a UILabel. Here's the code:
func newQuestionOutline() -> CAShapeLayer {
let outlineShape = CAShapeLayer()
outlineShape.isHidden = false
let circularPath = UIBezierPath(roundedRect: questionLabel.frame, cornerRadius: 5)
outlineShape.path = circularPath.cgPath
outlineShape.fillColor = UIColor.clear.cgColor
outlineShape.strokeColor = UIColor.yellow.cgColor
outlineShape.lineWidth = 5
outlineShape.strokeEnd = 0
view.layer.addSublayer(outlineShape)
return outlineShape
}
func newQuestionAnimation() {
let outlineAnimation = CABasicAnimation(keyPath: "strokeEnd")
outlineAnimation.toValue = 1
outlineAnimation.duration = 5
newQuestionOutline().add(outlineAnimation, forKey: "key")
}
The animation performs as expected when running on the simulator for an iPhone 11 which is the device size that I used in the storyboard. However when running the project on a different device with different screen dimensions (like iPhone 8 plus) the shape is drawn out of place and not around the UILabel as it should be. I used autolayout to horizontally and vertically center the UILabel to the center of the view so the UILabel is centered no matter what device.
Any suggestions? Thanks in advance!
Cheers!
A shape layer is not a view, so it is not subject to auto layout. And any time you say something like roundedRect: questionLabel.frame you are making yourself dependent on what questionLabel.frame is at that moment, which is a huge mistake because that is exactly what is not determined until auto layout determines what the frame will be (and can change later if auto layout changes its mind due to changing conditions, such as rotation etc.)
There are two kinds of solution:
Host the shape layer in a view. Now you have something that is subject to autolayout. You will still need to redraw the shape layer whenever the view changes its frame, but you can detect that and perform the redraw.
Implement your view controller's viewDidLayoutSubviews to detect that auto layout has just done its work. Respond by (for example) removing the shape layer and making a new one based on the current conditions.
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.
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!
Hi i have a question about shapes and animation of the shapes.
I would like to understand what is the best practice to create these kind of shapes (shapes that are presented on images around buttons).
Previous investigation shows that usage of the UIBezierPathis required but I am interest in how to create this kind of path and constantly animate it? Or probably should i use SpriteKit for that?
I think Spritekit is not necessary. You make this kind of animation just by acting on the CALayer masks.
If you writing a series of masks based each time on a different ellipse ( and build an animations based on it), you can re-produce this effect in your button.
Some useful code examples:
var shape = CAShapeLayer()
shape.fillColor = UIColor(white: 0.90, alpha: 1).CGColor
var image = UIImage(named: "some_image")
shape.contents = image?.CGImage
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(160, 160, 240, 320))
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