Search code examples
iosswiftobjective-ccore-graphicscore-animation

Creating inner shadows in UIView to replicate Neumorphic Style


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.

Current state of the view

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
}
}

Neumorphism information


Solution

  • 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...

    1. draw() is definitely not where you want to be creating and adding/inserting sublayers.
    2. you can use a CAGradientLayer to get the "inner" appearance (based on your goal image)
    3. you can add a 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):

    enter image description here

    This is the result:

    enter image description here

    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:

    enter image description here