Fortunately it’s now very easy today (2020)
These days it’s very easy to do this:
Here’s the whole thing
import UIKit
class GlowBox: UIView {
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = .clear
layer.shadowOpacity = 1
layer.shadowColor = UIColor.red.cgColor
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.shadowRadius = 3
let p = UIBezierPath(
roundedRect: bounds.insetBy(dx: 0, dy: 0),
cornerRadius: 4)
let hole = UIBezierPath(
roundedRect: bounds.insetBy(dx: 2, dy: 2),
cornerRadius: 3)
.reversing()
p.append(hole)
layer.shadowPath = p.cgPath
}
}
A really handy tip:
When you add (that is .append
) two bezier paths like that…
The second one has to be either “normal” or “reversed”.
Notice the line of code near the end:
.reversing()
Like most programmers, I can NEVER REMEMBER if it should be “normal” or “reversed” in different cases!!
The simple solution …
Very simply, try both!
Simply try it with, and without, the .reversing()
It will work one way or the other! 🙂
If you want JUST the shadow to be ONLY outside, with the insides exactly cut out:
override func layoutSubviews() {
super.layoutSubviews()
// take EXTREME CARE of frame vs. bounds
// and + vs - throughout this function:
let _rad: CGRect = 42
colorAndShadow.frame = bounds
colorAndShadow.path =
UIBezierPath(roundedRect: bounds, cornerRadius: _rad).cgPath
let enuff: CGFloat = 200
shadowHole.frame = colorAndShadow.frame.insetBy(dx: -enuff, dy: -enuff)
let _sb = shadowHole.bounds
let p = UIBezierPath(rect: _sb)
let h = UIBezierPath(
roundedRect: _sb.insetBy(dx: enuff, dy: enuff),
cornerRadius: _rad)
p.append(h)
shadowHole.fillRule = .evenOdd
shadowHole.path = p.cgPath
layer.mask = shadowHole
}
shadowHole
is a CAShapeLayer that you must set up using a lazy variable in the usual way.