UIKit: How to mix several CALayers into one view layer? - ios

I would like to mix several sublayers into only one layer.
If I simply assign my modifications to the view layer, it works fine:
testView.layer.cornerRadius = 15.0
testView.layer.shadowColor = UIColor.yellow.withAlphaComponent(1.0).cgColor
testView.layer.shadowOpacity = 1.0
testView.layer.shadowRadius = 24.0
testView.layer.shadowOffset = .zero
testView.backgroundColor = .white
Then, I tried this:
testView.layer.cornerRadius = 15.0
// sl1
let layer1 = CALayer()
layer1.shadowColor = UIColor.yellow.withAlphaComponent(1.0).cgColor
layer1.shadowOpacity = 1.0
layer1.shadowRadius = 24.0
layer1.shadowOffset = .zero
// sl2
let layer2 = CALayer()
layer2.shadowColor = UIColor.red.withAlphaComponent(1.0).cgColor
layer2.shadowOpacity = 1.0
layer2.shadowRadius = 24.0
layer2.shadowOffset = .zero
// sublayers
let layer = CALayer()
testView.layer.sublayers = [layer1, layer2]
testView.backgroundColor = .white
But now here is the result I get
There is now no shadow surrounding the white view. Why is this happening?
Thank you for your help

The chief problem with your code is that none of your layers have any size. Thus for example you see no shadow, because the layer itself is of zero size in the top left corner so there is nothing there to cast any shadow.
It is your job to give a layer a frame immediately after creating it! Typically this will be the same as the bounds of the superlayer. Keep in mind, however, that unlike views, when a view or superlayer is resized, sublayers are not. Thus the sublayer can cease to "fit" its superlayer properly if you don't take measures to correct that.

Related

Choppy CATextLayer animation: fontSize + position concurrently

I need to animate a CATextLayer's bounds.size.height, position, and fontSize. When I add them to a CAAnimationGroup, the text jitters during the animation, just like this:
https://youtu.be/HfC1ZX-pbyM
The jittering of the text's tracking values (spacing between characters) seems to occur while animating fontSize with bounds.size.height AND/OR position. I've isolated fontSize, and it performs well on its own.
How can I prevent the text from jittering in CATextLayer if I animate bounds and font size at the same time?
EDIT
I've moved on from animating bounds. Now, I only care about fontSize + position. Here are two videos showing the difference.
fontSize only (smooth): https://youtu.be/FDPPGF_FzLI
fontSize + position (jittery): https://youtu.be/3rFTsp7wBzk
Here is the code for that.
let startFontSize: CGFloat = 16
let endFontSize: CGFloat = 30
let startPosition: CGPoint = CGPoint(x: 40, y: 100)
let endPosition: CGPoint = CGPoint(x: 20, y: 175)
// Initialize the layer
textLayer = CATextLayer()
textLayer.string = "Hello how are you?"
textLayer.font = UIFont.systemFont(ofSize: startFontSize, weight: UIFont.Weight.semibold)
textLayer.fontSize = startFontSize
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.contentsScale = UIScreen.main.scale
textLayer.isWrapped = true
textLayer.backgroundColor = UIColor.lightGray.cgColor
textLayer.anchorPoint = CGPoint(x: 0, y: 0)
textLayer.position = startPosition
textLayer.bounds.size = CGSize(width: 450, height: 50)
view.layer.addSublayer(textLayer)
// Animate
let damping: CGFloat = 20
let mass: CGFloat = 1.2
var animations = [CASpringAnimation]()
let fontSizeAnim = CASpringAnimation(keyPath: "fontSize")
fontSizeAnim.fromValue = startFontSize
fontSizeAnim.toValue = endFontSize
fontSizeAnim.damping = damping
fontSizeAnim.mass = mass
fontSizeAnim.duration = fontSizeAnim.settlingDuration
animations.append(fontSizeAnim)
let positionAnim = CASpringAnimation(keyPath: "position.y")
positionAnim.fromValue = textLayer.position.y
positionAnim.toValue = endPosition.y
positionAnim.damping = damping
positionAnim.mass = mass
positionAnim.duration = positionAnim.settlingDuration
animations.append(positionAnim)
let animGroup = CAAnimationGroup()
animGroup.animations = animations
animGroup.duration = fontSizeAnim.settlingDuration
animGroup.isRemovedOnCompletion = true
animGroup.autoreverses = true
textLayer.add(animGroup, forKey: nil)
My device is running iOS 11.0.
EDIT 2
I've broken down each animation (fontSize only, and fontSize + position) frame-by-frame. In each video, I'm progressing 1 frame at a time.
In the fontSize only video (https://youtu.be/DZw2pMjDcl8), each frame yields an increase in fontSize, so there's no choppiness.
In the fontSize + position video (https://youtu.be/_idWte92F38), position is updated in every frame, but not fontSize. There is only an increase in fontSize in 60% of frames, meaning that fontSize isn't animating in sync with position, causing the perceived chopping.
So maybe the right question is: why does fontSize animate in each frame when it's the only animation added to a layer, but not when added as part of CAAnimationGroup in conjunction with the position animation?
Apple DTS believes this issue is a bug. A report has been filed.
In the meantime, I'll be using CADisplayLink to synchronize the redrawing of CATextLayer.fontSize to the refresh rate of the device, which will redraw the layer with the appropriate fontSize in each frame.
Edit
After tinkering with CADisplayLink for a day or so, drawing to the correct fontSize proved difficult, especially when paired with a custom timing function. So, I'm giving up on CATextLayer altogether and going back to UILabel.
In WWDC '17's Advanced Animations with UIKit, Apple recommends "view morphing" to animate between two label states — that is, the translation, scaling, and opacity blending of two views. UIViewPropertyAnimator provides a lot of flexibility for this, like blending multiple timing functions and scrubbing. View morphing is also useful for transitioning between 2 text values without having to fade out the text representation, changing text, and fading back in.
I do hope Apple can beef up CATextLayer support for non-interactive animations, as I'd prefer using one view to animate the same text representation.

Corners of button not staying rounded when using constraints constraints

I have two buttons in a stack view. I have used an extension of UIButton to round the outside corners. This works on the 7Plus which I designed for in storyboard but as soon as I run on a smaller device size in the simulator it stops working and I can only round corners on the left side of either button and not the right. Any ideas?
On a 7Plus
On a 7
These are the extensions I'm using
extension CGSize{
init(_ width:CGFloat,_ height:CGFloat) {
self.init(width:width,height:height)
}
}
extension UIButton{
func roundOneSide(topCorner: UIRectCorner, bottomCorner: UIRectCorner){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [topCorner , bottomCorner],
cornerRadii:CGSize(6.0, 6.0))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
I have also tried the following code to no avail. It can only successfully round corners on the left when constraints come into play.
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 6, height: 6)).cgPath
facebookBtn.layer.mask = maskLayer
facebookBtn.layer.masksToBounds = true
Use you use segment Control as jerky said or
Try this below code:
// For login button
UIBezierPath *cornersPathLeft = [UIBezierPath bezierPathWithRoundedRect:buttonLogin.bounds byRoundingCorners:(UIRectCornerBottomLeft|
UIRectCornerTopLeft) cornerRadii:CGSizeMake(5, 5)];
//Create a new layer to use as a mask
CAShapeLayer *maskLayerLeft = [CAShapeLayer layer];
// Set the path of the layer
maskLayerLeft.path = cornersPathLeft.CGPath;
buttonLogin.layer.mask = maskLayerLeft;
// For FB button
UIBezierPath *cornersPathRight = [UIBezierPath bezierPathWithRoundedRect:buttonFB.bounds byRoundingCorners:(UIRectCornerTopRight|
UIRectCornerBottomRight) cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *maskLayerRight = [CAShapeLayer layer];
maskLayerRight.path = cornersPathRight.CGPath;
buttonFB.layer.mask = maskLayerRight;
NOTE:
do maskToBounds = true it allows to give effect on the layers.
-
Difference Between MaskToBounds and ClipsToBounds
MaskToBounds
Any sublayers of the layer that extend outside its boundaries will be clipped to those boundaries. Think of the layer, in that case, as a window onto its sublayers; anything outside the edges of the window will not be visible. When masksToBounds = NO, no clipping occurs.
When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.
ClipsToBounds
The use case for clipsToBounds is more for subviews which are partially outside the main view.
For example, I have a (circular) subview on the edge of its parent (rectangular) UIView. If you set clipsToBounds to YES, only half the circle/subview will be shown. If set to NO, the whole circle will show up. Just encountered this so wanted to share
Conclusion
MaskToBounds are applied for the sublayer of any view. Like here OP added layer over button but it does not give effects. I mean layer is not bounds properly.
ClipToBounds are applied on the subVies of any view. Assume you have you have a view( says, viewBG ) and and now you added another view (says, upperView), now you dont wanted to see view upper to look outside the viewBG. ViewUpper always bounded inside its superview. so in this case you have to true the clipstobounds.
Practical experience
Try this below code in swift
let viewBG : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
viewBG.backgroundColor = UIColor.lightGray
self.view.addSubview(viewBG)
let viewUpper : UIView = UIView(frame: CGRect(x: -50, y: -50, width: 100, height: 100))
viewUpper.backgroundColor = UIColor.blue
viewBG.addSubview(viewUpper)
Output
1. When i done viewBG.clipsToBounds = false
When i done viewBG.clipsToBounds = true
You need to enable clipToBound property of button in storyboard;
You could try a different, very simple method of curving the corners on a UIButton or Label.
Click on button
Click on identity inspector
Then add an attribute in the identity section
Key path is layer.cornerRadius
Set as Number
Finally give a value, the higher the value, the more rounded the corners are.
Well that was an easy fix. I just needed to round the corners in viewWillLayoutSubviews.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
loginBtn.roundOneSide(topCorner: .topLeft, bottomCorner: .bottomLeft)
facebookBtn.roundOneSide(topCorner: .topRight, bottomCorner: .bottomRight)
}

Draw Shadow Only *Around* UIView Which Contains Transparency

Question I'm struggling on. I've searched SO for a fair good time by now, but couldn't find an answer.
I have a UIView which contains alpha value of 0.5, means - it transparent.
If I'm applying the usual code for UIView shadowing -
class func applyShadow(view : UIView)
{
view.layer.shadowColor = UIColor.blackColor().colorWithAlphaComponent(0.15).CGColor
view.layer.shadowOpacity = 1
view.layer.shadowOffset = CGSizeMake(0, 0.5)
view.layer.shadowRadius = 1.3
view.layer.masksToBounds = false
view.layer.shouldRasterize = true
view.layer.rasterizationScale = UIScreen.mainScreen().scale
}
The "fill" of the my UIView get shadowed as well.
How can I draw the shadow only on the "border" path of my UIView, excluding the UIView fill?
For 2022 this is now possible.
Essentially you do this:
shadowHole.fillRule = .evenOdd
shadowHole.path = p.cgPath
full details ...
https://stackoverflow.com/a/59092828/294884

Resize a CALayer with animation in Swift

I have added a CALayer to an image doing the following :
var menulayer = CALayer()
menulayer.frame = CGRectMake(0.0, 0.0, menuimage.frame.size.width, menuimage.frame.size.height);
menulayer.backgroundColor = bartint.CGColor
menulayer.opacity = 1
menuimage.layer.addSublayer(menulayer)
I want to animate the layer so that it reveals the image from left to right. I have tried this:
let width = CGFloat(0)
CATransaction.begin()
CATransaction.setAnimationDuration(2.0)
self.menulayer.bounds = CGRectMake(0, 0, width , 50)
CATransaction.commit()
But the animation starts at the center of the image, how can I make it start from the left edge of the image ?
So animate the origin of the layer to shift it to the right until your image view is fully revealed. You will need to set menuImage.layer.masksToBounds = true so the cover layer isn't visible as it scrolls out of the frame of the image view.

How to create a CGPath which exactly match a CALayer bounds when the layer rotate or scale?

We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results

Resources