I’ve been trying to add a drop shadow to a semi transparent UIView
but the drop shadow is showing up underneath the view. Basically anywhere inside the outline of the view, I don't want to see any shadows. The location icon has no styling.
// Basic Shadow
self.myView.layer.shadowColor = UIColor.black.cgColor
self.myView.layer.shadowOpacity = 0.3
self.myView.layer.shadowOffset = CGSize(width: 0, height: 3)
self.myView.layer.shadowRadius = 0
CodePudding user response:
The easiest way to do this is to use a custom UIView
subclass with two CAShapeLayer
s...
For the "shadow" layer path, use a rounded-rect UIBezierPath
that is slightly taller than the view, so it extends below the bottom.
Here's a quick example...
Custom View Class
class CustomView: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.7) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.73, green: 0.84, blue: 0.96, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var offset: CGFloat = 10 { didSet { setNeedsLayout() } }
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height = offset
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
Example Controller Class
class CustomViewTestVC: UIViewController {
let gradView = BasicGradientView()
let customView = CustomView()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemBlue
v.font = .systemFont(ofSize: 34.0, weight: .bold)
v.text = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque." // Tap anywhere to toggle this label's visibility."
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradView, testLabel, customView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
gradView.widthAnchor.constraint(equalToConstant: 312.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor, multiplier: 1.0),
gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabel.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -4.0),
testLabel.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: 0.0),
testLabel.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
testLabel.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
customView.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -90.0),
customView.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: -90.0),
customView.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
customView.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
])
gradView.endPoint = CGPoint(x: 1.0, y: 1.0)
gradView.colors = [
.red, .yellow, .cyan,
]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabel.isHidden.toggle()
}
}
Basic Gradient View
class BasicGradientView: UIView {
public var colors: [UIColor] = [.white, .black] { didSet { setNeedsLayout() } }
public var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.0) { didSet { setNeedsLayout() } }
public var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.0) { didSet { setNeedsLayout() } }
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
}
override func layoutSubviews() {
super.layoutSubviews()
gLayer.colors = colors.compactMap( {$0.cgColor })
gLayer.startPoint = startPoint
gLayer.endPoint = endPoint
}
}
This is the output -- tap anywhere to toggle the UILabel
visibility:
Then add your imageView on top (or as a subview of the custom view):
Edit - to answer comment
We can get a shadow to show only on the outside by:
- replacing the "fake-shadow shape layer" with a
CALayer
- using the bezier path as the layer's
.shadowPath
- creating a bezier path with a "hole" cut in it
- use that path as a
CAShapeLayer
path - and then masking the shadow layer with that
CAShapeLayer
Like this:
Here are updates to the above code as examples. Both classes are very similar, with the same custom properties that can be changed from their defaults. I've also added a UIImageView
as a subview, to produce this output:
as before, tapping anywhere will toggle the UILabel
visibility:
CustomViewA Class
class CustomViewA: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.3
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 8.0) { didSet { setNeedsLayout() } }
// shadowRadius is not used, but this allows us to treat both CustomViewA and CustomViewB the same
public var shadowRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height = shadowOffset.height
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
CustomViewB Class
class CustomViewB: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.7
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 10.0) { didSet { setNeedsLayout() } }
public var shadowRadius: CGFloat = 6 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CALayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
// add a square (1:1) image view, 1/2 the width of self
// centered horizontally and vertically
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// we're going to mask the shadow layer with a "cutout" of the rounded rect
// the shadow is going to spread outside the bounds,
// so the "outer" path needs to be larger
// we'll make it plenty large enough
let bpth = UIBezierPath(rect: bounds.insetBy(dx: -bounds.width, dy: -bounds.height))
bpth.append(pth)
bpth.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.path = bpth.cgPath
shadowLayer.mask = maskLayer
shadowLayer.shadowPath = pth.cgPath
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowColor = shadowColor.cgColor
shadowLayer.shadowRadius = shadowRadius
shadowLayer.shadowOffset = shadowOffset
}
}
Example Controller Class - uses the BasicGradientView
class above
class CustomViewTestVC: UIViewController {
let gradViewA = BasicGradientView()
let gradViewB = BasicGradientView()
let customViewA = CustomViewA()
let customViewB = CustomViewB()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabelA: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemRed
v.font = .systemFont(ofSize: 32.0, weight: .regular)
return v
}()
let testLabelB: UILabel = {
let v = UILabel()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradViewA, gradViewB, testLabelA, testLabelB, customViewA, customViewB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
gradViewA.widthAnchor.constraint(equalToConstant: 300.0),
gradViewA.heightAnchor.constraint(equalTo: gradViewA.widthAnchor, multiplier: 1.0),
gradViewA.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -4.0),
testLabelA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: 0.0),
testLabelA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
testLabelA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
customViewA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -84.0),
customViewA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: -84.0),
customViewA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
customViewA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
gradViewB.topAnchor.constraint(equalTo: gradViewA.bottomAnchor, constant: 8.0),
gradViewB.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: 0.0),
gradViewB.heightAnchor.constraint(equalTo: gradViewB.widthAnchor, multiplier: 1.0),
gradViewB.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelB.widthAnchor.constraint(equalTo: testLabelA.widthAnchor, constant: -0.0),
testLabelB.heightAnchor.constraint(equalTo: testLabelA.heightAnchor, constant: 0.0),
testLabelB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
testLabelB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
customViewB.widthAnchor.constraint(equalTo: customViewA.widthAnchor, constant: 0.0),
customViewB.heightAnchor.constraint(equalTo: customViewA.heightAnchor, constant: 0.0),
customViewB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
customViewB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
])
// let's setup the gradient views the same
gradViewA.colors = [
.init(red: 0.242, green: 0.591, blue: 0.959, alpha: 1.0),
.init(red: 0.113, green: 0.472, blue: 0.866, alpha: 1.0)
]
gradViewA.endPoint = CGPoint(x: 1.0, y: 1.0)
gradViewB.colors = gradViewA.colors
gradViewB.endPoint = gradViewA.endPoint
// let's give the two test labels the same properties
testLabelB.numberOfLines = testLabelA.numberOfLines
testLabelB.textAlignment = testLabelA.textAlignment
testLabelB.textColor = testLabelA.textColor
testLabelB.font = testLabelA.font
let s = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque."
testLabelA.text = "CustomViewA\n" s
testLabelB.text = "CustomViewB\n" s
// set the .image property of both custom views
if let img = UIImage(named: "marker") {
customViewA.image = img
customViewB.image = img
} else {
if let img = UIImage(systemName: "mappin.and.ellipse")?.withTintColor(.white, renderingMode: .alwaysOriginal) {
customViewA.image = img
customViewB.image = img
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabelA.isHidden.toggle()
testLabelB.isHidden.toggle()
}
}