If you inspect the keyboard view using Debug View Hierarchy, you can see that the "key buttons" do not have a shadow ... they have a single-point line on the bottom.
Various ways to do that, but one of the easiest is to add a white CAShapeLayer, give both the view's layer and the shape layer the same size and corner radius, and then shift the shape layer up by one point.
Here's a quick, simple view subclass - marked as @IBDesignable so you can see it in Storyboard / Interface Builder:
@IBDesignable
class BottomShadowView: UIView {
let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// add the "white" layer
layer.addSublayer(topLayer)
// give both layers the same corner radius
layer.cornerRadius = 8
topLayer.cornerRadius = 8
// set top layer to white
topLayer.backgroundColor = UIColor.white.cgColor
// if background color is not set, or is clear
// set it to dark gray
if self.backgroundColor == nil || self.backgroundColor == .clear {
self.backgroundColor = .darkGray
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// if background color is not set, or is clear
// set it to dark gray
if self.backgroundColor == nil || self.backgroundColor == .clear {
self.backgroundColor = .darkGray
}
}
override func layoutSubviews() {
super.layoutSubviews()
// make top layer the same size as bounds
// offset up 1-point
topLayer.frame = bounds.offsetBy(dx: 0, dy: -1.0)
}
}
It looks like this (on a systemYellow background):

and zoomed-in for clarity:

We can also get a slightly "lighter weight" custom view by overriding draw() instead of adding a sublayer:
@IBDesignable
class DrawBottomShadowView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// background color needs to be .clear
self.backgroundColor = .clear
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
var r: CGRect!
var pth: UIBezierPath!
// if rounded rect for "bottom shadow line"
// goes all the way to the top, we'll get
// anti-alias artifacts at the top corners
// so, we'll make it slightly smaller
r = bounds.insetBy(dx: 0, dy: 2).offsetBy(dx: 0, dy: 2)
pth = UIBezierPath(roundedRect: r, cornerRadius: 8)
UIColor.darkGray.setFill()
pth.fill()
// "filled" rounded rect should be
// 1-point shorter than height
r = bounds
r.size.height -= 1.0
pth = UIBezierPath(roundedRect: r, cornerRadius: 8)
UIColor.white.setFill()
pth.fill()
}
}