macOS: CALayer shadow not showing - swift

I need a shadow on my view. I tried using the view’s NSShadow capability, but it is too slow to use in a scroll view. I want to try using the layer’s shadow properties to hopefully improve performance.
In my NSView.updateLayer() method, I set the following properties:
layer.shadowOffset = CGSize(width: 0, height: -3)
layer.shadowRadius = 3
layer.shadowColor = NSColor.black().cgColor
layer.shadowOpacity = 0.3
No shadow is shown. I tried also setting NSView.wantsDefaultClipping and CALayer.masksToBounds to false, but there is still no shadow.
Why is there no shadow when using the CALayer shadow properties?

What you need to add is:
self.view.wantsLayer = true
I tried running the following code; the layer does show as well as the shadow:
let layer = CALayer()
layer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
layer.backgroundColor = NSColor.redColor().CGColor
layer.shadowOffset = CGSize(width: 0, height: -3)
layer.shadowRadius = 3
layer.shadowColor = NSColor.blackColor().CGColor
layer.shadowOpacity = 1.0 //Or: 0.3 as you originally have
self.view.wantsLayer = true
self.view.layer?.addSublayer(layer)
By the way, you have a typo on the following line:
NSColor.black().cgColor
as it should be:
NSColor.blackColor().CGColor

I looked at the disassembly of CALayer.render(in:) and it looked like it was properly accessing the layer shadow properties. So NSView probably overwrites the layer shadow properties with its own on every draw cycle. The bottom line is that you can only add shadows to view-backing layers by using the shadow property on the view.
I did solve my scroll performance problem though. I profiled my app during a scroll and noticed that the creation of the shadow image in CGContextEndTransparencyLayer was causing the CPU to spike.
There are two steps for creating a shadow. First, a path for the shadow has to be computed based on the alpha channel of the pixels above. Second, a gaussian blur is applied in order to soften the edges of the shadow.
Since I know the view on top of the shadow is fully opaque, I know the path will be simply the view’s bounds. I could have skipped the first step by setting the layer’s shadowPath property. But unfortunately, the layer’s shadow properties are overridden by the view which doesn’t have a shadowPath property.
My solution was to create a container view that draws a rectangular shadow image underneath the content view. This shadow image is created once and cached, increasing scroll performance dramatically. And thanks to the power of Auto Layout (specifically, the alignment rect insets), the container view can be used without having to manually adjust for the shadow.
You can view my code on GitHub.

NSView sets the shadow related properties on CALayer at predictable times, like when wantsLayer is set to true and when the view is added to a superview. Set the layer shadow properties after NSView and the shadow will be visible. I set the shadow properties during resizeSubviews.

Related

How do I determine the arcCenter for a UIBezierPath relative to its parent view?

I am using a CAShapeLayer with UIBezierPath to make a circular progress bar. The code requires a center value based on a CGPoint.
let circleLayer = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
I have set the value manually as follows:
let center = CGPoint(x: 70, y: 70)
However those values are relative to the superView and I need to make it relative to its parent UIView.
All other elements in the UIView are constrained using programmatic UI code like this:
sampleText.translatesAutoresizingMaskIntoConstraints = false
sampleText.topAnchor.constraint(equalTo: directionsTitle.bottomAnchor, constant: 0).isActive = true
sampleText.leadingAnchor.constraint(equalTo: timerFooter.leadingAnchor, constant: 8).isActive = true
sampleText.trailingAnchor.constraint(equalTo: timerFooter.trailingAnchor, constant: -8).isActive = true
Fixed values may work fine on one device but will be out of alignment on a larger or smaller device. My question is, how do I determine the values for center of the UIView that will meet the values required of CGPoint?
I have tried this:
let centerX = view.centerXAnchor
let centerY = view.centerYAnchor
let center = CGPoint(x: centerX, y: centerY)
However, that throws an error: No exact matches in call to initializer
The following image reflects where the shape placement is relative to. The blue box indicates the desired UIView area where I want to position the circle.
Advice from wiser minds would be greatly appreciated.
Ah, ok, I think I understand what the problem is that you're facing.
I think your issue is that you're trying to use the CAShapeLayer like it is a UIView. When in reality they don't behave like that at all.
What you should be doing is creating a UIView subclass and putting the CAShapeLayer in there. Now you only need to worry about how the CAShapeLayer fits into its own UIView subclass.
To "position the layer" into some other view you actually don't need to worry about that at all. You just place the new UIView subclass and the layer will go along with it.
e.g.
class MyProgressView: UIView {
let circleLayer: CAShapeLayer
// add the circleLayer
// add properties for customising the circle layer
// Position the circle layer in the centre of the MyProgressView frame
// I'd suggest using `layoutSubviews` or something for this.
// etc...
}
Now... to position your "CircleLayer" you actually don't need to. You just position the MyProgressView where you want it to be and the layer is part of that so will go along for the ride.
From your screenshot it looks like you want the view with the blue rectangle to contain the circleLayer. If that is the case then make the blue rectangle view your custom UIView subclass and ... done. It will now contain the circle view.
RayWenderlich has some good stuff about customising views with CALayer... https://www.raywenderlich.com/10317653-calayer-tutorial-for-ios-getting-started#toc-anchor-002
It sounds like you want the midpoint of the UIView's bounds I think that's yourView.bounds.midX and yourView.bounds.midY
A view's bounds is its own coordinate space. A view's frame is its location in its parent space.
UIView has methods like convertPoint:toCoordinateSpace: and friends for converting points and rects between those spaces.

add two shadows with different colors on UIView?

I am going to make 3D effect to uiview and add shadows like in the following image. there is white shadow on top and left sides and gray shadow on right and bottom side.
I already added white shadow on top and left sides.
my result
self.layer.shadowColor = UIColor.white.cgColor
self.layer.shadowOffset = CGSize(width: -4.0, height: -4.0)
self.layer.shadowRadius = 2.0
self.layer.shadowOpacity = 1.0
self.layer.masksToBounds = false
Is it possible to achieve the following result by uibezierPath?
Image
Try adding a creating another layer with the other shadow color and change the offset. Then add this sublayer underneath
layer.addsubview(yourNewView)

UITextField border isn't clipping

I'm almost sure this question has been asked before, but I searched for almost an hour and didn't find anything
This is my code:
textField.font = UIFont.systemFont(ofSize: 12.0)
textField.layer.borderWidth = 0.9
textField.placeholder = "border is 0.9 clip is false"
textField.layer.borderColor = UIColor.black.cgColor
textField.borderStyle = UITextBorderStyle.roundedRect
textField.returnKeyType = UIReturnKeyType.done
Doesn't matter if set textField.clipsToBounds to true or false, the borders won't be rounded. However if I change the background color, it will fill it as it has a rounded border, see the image below.
if you zoom in, you'll see that the it doesn't fill the corners completely
If I set textField.layer.borderWidth = 0.1 then I would have a nice roundedRect, but they border would then have to be a thin line. See the image below:
again setting clipToBounds = true won't make any difference.
EDIT:
So the answer is to do textField.layer.cornerRadius = 5. But then I wonder what is the UITextBorderStyle.roundedRect doing then?!
Try textField.layer.cornerRadius = 5
cornerRadius - the radius to use when drawing rounded corners for the layer’s background. Animatable.
var cornerRadius: CGFloat { get set }
Setting the radius to a value greater than 0.0 causes the layer to
begin drawing rounded corners on its background. By default, the
corner radius does not apply to the image in the layer’s contents
property; it applies only to the background color and border of the
layer. However, setting the masksToBounds property to true causes the
content to be clipped to the rounded corners.
The default value of this property is 0.0.
Update:
But then I wonder what is the UITextBorderStyle.roundedRect doing then?
Basically the same thing but with default radius. Maybe it was overwritten somewhere. Check out your IB properties, in Attribute Inspector, Border Style property:

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

Mask a View with the alpha from another view in Swift

I've been struggling for a view hours to figure out how to get my view masked by a shape that is in another view. Basically I have a circular countdown timer that I want to be masked out by an animating circle that scales up from the center of the timer when the timer is reset.
I tried setting timerMask.maskView = timerCircleGrahics where timerCircleGraphics is the name of my timer animation view. But this is giving me very strange results when I test the app. It seems to clip the view to the rectangle bounds of my mask view rather than the alpha of the bounds that are drawn within that view. The mask layer is centered and being drawn properly, but I've never attempted this before so am not sure if I am doing it right.
Here is the class for my mask shape:
class timerBackgroundMask: UIView {
override func drawRect(rect: CGRect) {
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 238, 238))
colorGreen.setFill()
ovalPath.fill()
}
}
Then using IB, I assign this mask to a manually placed View in my Storyboard called timerMask. I am realizeing now that by assigning timerBackgroundMask class to timerMask I have programatically added a subview to my manually placed Storyboard view, but I feel like the alpha should come through just the same when set this view to mask out anotherview. Here is the code i use to set the mask
timerCircleGraphics.layer.mask = timerMask.layer
The result I am getting is pretty weird:
The red portion should be a circle that is partially clipped by my timerMask from the center outward. The light green circle that you see is simply the background view of the counter, however it happens to be the exact position and size as my timerBackgroundMask for reference.
Don't bother creating a class and using the Storyboard. Do it straight in code, the simple way:
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 238, 238))
colorGreen.setFill()
ovalPath.fill()
var mask = CAShapeLayer()
mask.path = ovalPath.CGPath
timerCircleGraphics.layer.mask = mask