Reputation: 23263
In my app I have a UIView
that is using CALayer
in order to achieve a shadow:
@implementation MyUIView
- (instancetype) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(!self) return self;
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:.2].CGColor;
self.layer.shadowOffset = CGSizeMake(0, 2);
self.layer.shadowOpacity = 1;
self.layer.shadowRadius = 1;
return self;
}
@end
If I want anything approaching reasonable performance, I have to define the CALayer
's shadowPath
:
@implementation MyUIView
- (void) setFrame:(CGRect)frame {
[super setFrame:frame];
self.layer.shadowPath = CGPathCreateWithRect(self.bounds, NULL);
}
@end
I've noticed two things whenever I animate this UIView
:
If I don't use a shadowPath
, the shadow animates along nicely with rotations and frame size changes. The caveat here being very slow animation and a general lack of performance.
If I do use a shadowPath
whenever the UIView
is animated the animation is smooth and timely, however the shadow's transition itself is much more block-like (and less smooth) than it is without a shadowPath
.
Examples:
shadowPath
: https://gfycat.com/ColdBlissfulIndusriverdolphin (notice how the shadow behaves like a poorly transformed rectangle?)shadowPath
(the slowness of the animation is more apparent on the device, but you get the idea): https://gfycat.com/ActiveRemorsefulBandicootEdit:
It's worth noting that these animations are implicit - I'm not invoking them myself. They are the result of the UIViewController
rotating with the device orientation. The shadow is on a UIView
that changes size during rotation.
Upvotes: 7
Views: 1523
Reputation: 2720
The shadowPath needs to set additional animation when view resizing.
You can use my class directly.
/*
Shadow.swift
Copyright © 2018, 2020-2021 BB9z
https://github.com/BB9z/iOS-Project-Template
The MIT License
https://opensource.org/licenses/MIT
*/
/**
A view drops shadow.
*/
@IBDesignable
class ShadowView: UIView {
@IBInspectable var shadowOffset: CGPoint = CGPoint(x: 0, y: 8) {
didSet { needsUpdateStyle = true }
}
@IBInspectable var shadowBlur: CGFloat = 10 {
didSet { needsUpdateStyle = true }
}
@IBInspectable var shadowSpread: CGFloat = 0 {
didSet { needsUpdateStyle = true }
}
/// Set nil can disable shadow
@IBInspectable var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.3) {
didSet { needsUpdateStyle = true }
}
@IBInspectable var cornerRadius: CGFloat {
get { layer.cornerRadius }
set { layer.cornerRadius = newValue }
}
private var needsUpdateStyle = false {
didSet {
guard needsUpdateStyle, !oldValue else { return }
DispatchQueue.main.async { [self] in
if needsUpdateStyle { updateLayerStyle() }
}
}
}
private func updateLayerStyle() {
needsUpdateStyle = false
if let color = shadowColor {
Shadow(view: self, offset: shadowOffset, blur: shadowBlur, spread: shadowSpread, color: color, cornerRadius: cornerRadius)
} else {
layer.shadowColor = nil
layer.shadowPath = nil
layer.shadowOpacity = 0
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateLayerStyle()
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
lastLayerSize = layer.bounds.size
if shadowColor != nil, layer.shadowOpacity == 0 {
updateLayerStyle()
}
}
private var lastLayerSize = CGSize.zero {
didSet {
if oldValue == lastLayerSize { return }
guard shadowColor != nil else { return }
updateShadowPathWithAnimationFixes(bonuds: layer.bounds)
}
}
// We needs some additional step to achieve smooth result when view resizing
private func updateShadowPathWithAnimationFixes(bonuds: CGRect) {
let rect = bonuds.insetBy(dx: shadowSpread, dy: shadowSpread)
let newShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
if let resizeAnimation = layer.animation(forKey: "bounds.size") {
let key = #keyPath(CALayer.shadowPath)
let shadowAnimation = CABasicAnimation(keyPath: key)
shadowAnimation.duration = resizeAnimation.duration
shadowAnimation.timingFunction = resizeAnimation.timingFunction
shadowAnimation.fromValue = layer.shadowPath
shadowAnimation.toValue = newShadowPath
layer.add(shadowAnimation, forKey: key)
}
layer.shadowPath = newShadowPath
}
}
/**
Make shadow with the same effect as Sketch app.
*/
func Shadow(view: UIView?, offset: CGPoint, blur: CGFloat, spread: CGFloat, color: UIColor, cornerRadius: CGFloat = 0) { // swiftlint:disable:this identifier_name
guard let layer = view?.layer else {
return
}
layer.shadowColor = color.cgColor
layer.shadowOffset = CGSize(width: offset.x, height: offset.y)
layer.shadowRadius = blur
layer.shadowOpacity = 1
layer.cornerRadius = cornerRadius
let rect = layer.bounds.insetBy(dx: spread, dy: spread)
layer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
}
via https://github.com/BB9z/iOS-Project-Template/blob/master/App/General/Effect/Shadow.swift
Upvotes: 0
Reputation: 2165
I tried to reproduce the behavior shown in the two gifs you supplied, but without success (maybe you could edit your question with the animation code, e.g. UIView animateWithDuration:animations:
).
However, somewhere at the back of my mind I remember that once in a while I encountered a similar issue, and it turned out that I had to rasterize the view to make it smooth.
So I cannot guarantee that it solves the problem for you, but give it a try:
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [[UIScreen mainScreen] scale];
Upvotes: 4