I have the task to implement neumorphic design styles in my UIKit application. I have successfully implemented the double outer shadows (dark and light), but I somehow, can't figure out how to implement the inner shadows inside the view. I've tried approaches like CAGradientLayer
that goes from .black
to .clear
to .white
, but it isn't looking the way it is supposed to. I've searched the internet for various solutions, but couldn't seem to find one appropriate one.
How should I create the inner shadows? What approach should I use? I am fine with a solution in either Swift or Objective-C.
The state that I am trying to reach
A simplified version of the current state (for the outer shadows):
class DebossedView: UIView {
private var outerDarkShadow = CALayer()
private var outerLightShadow = CALayer()
override func draw(_ rect: CGRect) {
outerDarkShadow = shadowLayer(color: UIColor.black, shadowOffset: 10, shadowRadius: 12)
outerLightShadow = shadowLayer(color: UIColor.white, shadowOffset: -10, shadowRadius: 10)
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 3
layer.insertSublayer(outerDarkShadow, at: 0)
layer.insertSublayer(outerLightShadow, at: 0)
}
private func shadowLayer(color: UIColor, shadowOffset: CGFloat, shadowRadius: CGFloat) -> CALayer {
let shadowLayer = CALayer()
shadowLayer.frame = bounds
shadowLayer.backgroundColor = UIColor.gray.cgColor
shadowLayer.shadowColor = color.cgColor
shadowLayer.cornerRadius = 16
shadowLayer.shadowOffset = CGSize(width: shadowOffset, height: shadowOffset)
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = shadowRadius
return shadowLayer
}
}
Your "goal" image does not really match the examples in the link you posted for "Neumorphism informations" so not sure this will give you the exact results you want.
However, couple notes...
draw()
is definitely not where you want to be creating and adding/inserting sublayers.CAGradientLayer
to get the "inner" appearance (based on your goal image)UIImageView
as a subview to hold the image.So, here's an example UIView
subclass:
class NeuView: UIView {
public var image: UIImage? {
didSet {
imgView.image = image
}
}
private let imgView = UIImageView()
private let darkShadow = CALayer()
private let lightShadow = CALayer()
private let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
// add sublayers
self.layer.addSublayer(darkShadow)
self.layer.addSublayer(lightShadow)
self.layer.addSublayer(gradientLayer)
darkShadow.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
darkShadow.shadowOffset = CGSize(width: 5, height: 5)
darkShadow.shadowOpacity = 1
darkShadow.shadowRadius = 10
lightShadow.shadowColor = UIColor.white.withAlphaComponent(0.9).cgColor
lightShadow.shadowOffset = CGSize(width: -5, height: -5)
lightShadow.shadowOpacity = 1
lightShadow.shadowRadius = 10
// 45-degree gradient layer
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
self.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor
layer.borderWidth = 3
// very light gray background color
let bkgColor = UIColor(white: 0.95, alpha: 1.0)
darkShadow.backgroundColor = bkgColor.cgColor
lightShadow.backgroundColor = bkgColor.cgColor
// set gradient colors from
// slightly darker than background to
// slightly lighter than background
let c1 = UIColor(white: 0.92, alpha: 1.0)
let c2 = UIColor(white: 0.97, alpha: 1.0)
gradientLayer.colors = [c1.cgColor, c2.cgColor]
// image view properties
imgView.contentMode = .scaleAspectFit
imgView.translatesAutoresizingMaskIntoConstraints = false
//imgView.layer.masksToBounds = true
addSubview(imgView)
NSLayoutConstraint.activate([
// let's make the image view 60% of self
imgView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.centerXAnchor.constraint(equalTo: centerXAnchor),
imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// set all layers' frames to bounds
darkShadow.frame = bounds
lightShadow.frame = bounds
gradientLayer.frame = bounds
// set all layers' cornerRadius to one-half height
let cr = bounds.height * 0.5
darkShadow.cornerRadius = cr
lightShadow.cornerRadius = cr
gradientLayer.cornerRadius = cr
layer.cornerRadius = cr
}
}
and an example view controller:
class NeuTestVC: UIViewController {
let neuView = NeuView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
guard let img = UIImage(named: "neu01") else {
print("Could not load image!")
return
}
neuView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(neuView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
neuView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
neuView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
neuView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
neuView.heightAnchor.constraint(equalTo: neuView.widthAnchor),
])
// set the image
neuView.image = img
}
}
Using this image (transparent background):
This is the result:
By tweaking the colors, shadow properties, etc, this may give you your desired result.
Edit
Here is a modified version of the NewView
class.
Instead of using a gradient layer, we add a CAShapeLayer
with a "hole" cutout, and use that layer to cast the "inner shadow":
class NeuView: UIView {
public var image: UIImage? {
didSet {
imgView.image = image
}
}
private let imgView = UIImageView()
// "outer" shadows
private let darkShadow = CALayer()
private let lightShadow = CALayer()
// "inner" shadow
private let innerShadowLayer = CAShapeLayer()
private let innerShadowMaskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
// add sublayers
self.layer.addSublayer(darkShadow)
self.layer.addSublayer(lightShadow)
self.layer.addSublayer(innerShadowLayer)
darkShadow.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
darkShadow.shadowOffset = CGSize(width: 5, height: 5)
darkShadow.shadowOpacity = 1
darkShadow.shadowRadius = 10
lightShadow.shadowColor = UIColor.white.withAlphaComponent(0.9).cgColor
lightShadow.shadowOffset = CGSize(width: -5, height: -5)
lightShadow.shadowOpacity = 1
lightShadow.shadowRadius = 10
self.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor
//layer.borderWidth = 3
// very light gray background color
let bkgColor = UIColor(red: 0.94, green: 0.95, blue: 0.99, alpha: 1.0) // UIColor(white: 0.95, alpha: 1.0)
darkShadow.backgroundColor = bkgColor.cgColor
lightShadow.backgroundColor = bkgColor.cgColor // UIColor(white: 0.98, alpha: 1.0).cgColor
// image view properties
imgView.contentMode = .scaleAspectFit
imgView.translatesAutoresizingMaskIntoConstraints = false
//imgView.layer.masksToBounds = true
addSubview(imgView)
NSLayoutConstraint.activate([
// let's make the image view 60% of self
imgView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.centerXAnchor.constraint(equalTo: centerXAnchor),
imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// set dark and light shadow layers' frames to bounds
darkShadow.frame = bounds
lightShadow.frame = bounds
// set self.layer and dark and light shadow layers' cornerRadius to one-half height
let cr = bounds.height * 0.5
darkShadow.cornerRadius = cr
lightShadow.cornerRadius = cr
self.layer.cornerRadius = cr
// for the "inner" shadow,
// rectangle path needs to be larger than
// bounds + shadow offset + shadow raidus
// so the shadow doesn't "bleed" from all sides
let path = UIBezierPath(rect: bounds.insetBy(dx: -40, dy: -40))
// create a path for the "hole" in the layer
let circularHolePath = UIBezierPath(ovalIn: bounds)
// this "cuts a hole" in the path
path.append(circularHolePath)
path.usesEvenOddFillRule = true
innerShadowLayer.path = path.cgPath
innerShadowLayer.fillRule = .evenOdd
// fillColor doesn't matter - just needs to be opaque
innerShadowLayer.fillColor = UIColor.white.cgColor
// mask the layer, so we only "see through the hole"
innerShadowMaskLayer.path = circularHolePath.cgPath
innerShadowLayer.mask = innerShadowMaskLayer
// adjust properties as desired
innerShadowLayer.shadowOffset = CGSize(width: 15, height: 15)
innerShadowLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
innerShadowLayer.shadowRadius = 5
// setting .shadowOpacity to a very small value (such as 0.025)
// results in very light shadow
// set .shadowOpacity to 1.0 to clearly see
// what the shadow is doing
innerShadowLayer.shadowOpacity = 0.025
}
}
Example view controller:
class NeuTestVC: UIViewController {
let neuView = NeuView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.94, green: 0.95, blue: 0.99, alpha: 1.0)
guard let img = UIImage(named: "neu01") else {
print("Could not load image!")
return
}
neuView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(neuView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
neuView.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),
neuView.widthAnchor.constraint(equalToConstant: 125.0),
neuView.heightAnchor.constraint(equalTo: neuView.widthAnchor),
neuView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
// set the image
neuView.image = img
}
}
Results - the top instance has the "inner shadow" opacity set to 0.9
(to make it clearly visible)... the bottom instance is set to 0.025
: