快速-问题与角半径和下降阴影

我试图创建一个按钮与 圆角投影。无论我如何切换,按钮都不会正确显示。我已经尝试了 masksToBounds = falsemasksToBounds = true,但要么角半径的工作和阴影不或阴影的工作和角半径不夹角按钮。

import UIKit
import QuartzCore


@IBDesignable
class Button : UIButton
{
@IBInspectable var masksToBounds: Bool    = false                {didSet{updateLayerProperties()}}
@IBInspectable var cornerRadius : CGFloat = 0                    {didSet{updateLayerProperties()}}
@IBInspectable var borderWidth  : CGFloat = 0                    {didSet{updateLayerProperties()}}
@IBInspectable var borderColor  : UIColor = UIColor.clearColor() {didSet{updateLayerProperties()}}
@IBInspectable var shadowColor  : UIColor = UIColor.clearColor() {didSet{updateLayerProperties()}}
@IBInspectable var shadowOpacity: CGFloat = 0                    {didSet{updateLayerProperties()}}
@IBInspectable var shadowRadius : CGFloat = 0                    {didSet{updateLayerProperties()}}
@IBInspectable var shadowOffset : CGSize  = CGSizeMake(0, 0)     {didSet{updateLayerProperties()}}


override func drawRect(rect: CGRect)
{
updateLayerProperties()
}


func updateLayerProperties()
{
self.layer.masksToBounds = masksToBounds
self.layer.cornerRadius = cornerRadius
self.layer.borderWidth = borderWidth
self.layer.borderColor = borderColor.CGColor
self.layer.shadowColor = shadowColor.CGColor
self.layer.shadowOpacity = CFloat(shadowOpacity)
self.layer.shadowRadius = shadowRadius
self.layer.shadowOffset = shadowOffset
}
}
116104 次浏览

The following Swift 5 / iOS 12 code shows how to set a subclass of UIButton that allows to create instances with rounded corners and shadow around it:

import UIKit


final class CustomButton: UIButton {


private var shadowLayer: CAShapeLayer!


override func layoutSubviews() {
super.layoutSubviews()


if shadowLayer == nil {
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
shadowLayer.fillColor = UIColor.white.cgColor


shadowLayer.shadowColor = UIColor.darkGray.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2


layer.insertSublayer(shadowLayer, at: 0)
//layer.insertSublayer(shadowLayer, below: nil) // also works
}
}


}

According to your needs, you may add a UIButton in your Storyboard and set its class to CustomButton or you may create an instance of CustomButton programmatically. The following UIViewController implementation shows how to create and use a CustomButton instance programmatically:

import UIKit


class ViewController: UIViewController {


override func viewDidLoad() {
super.viewDidLoad()


let button = CustomButton(type: .system)
button.setTitle("Button", for: .normal)
view.addSubview(button)


button.translatesAutoresizingMaskIntoConstraints = false
let horizontalConstraint = button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let verticalConstraint = button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let widthConstraint = button.widthAnchor.constraint(equalToConstant: 100)
let heightConstraint = button.heightAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
}


}

The previous code produces the image below in the iPhone simulator:

enter image description here

To expand on Imanou's post, it's possible to programmatically add the shadow layer in the custom button class

@IBDesignable class CustomButton: UIButton {
var shadowAdded: Bool = false


@IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = cornerRadius > 0
}
}


override func drawRect(rect: CGRect) {
super.drawRect(rect)


if shadowAdded { return }
shadowAdded = true


let shadowLayer = UIView(frame: self.frame)
shadowLayer.backgroundColor = UIColor.clearColor()
shadowLayer.layer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: self.cornerRadius).CGPath
shadowLayer.layer.shadowOffset = CGSize(width: 1.0, height: 1.0)
shadowLayer.layer.shadowOpacity = 0.5
shadowLayer.layer.shadowRadius = 1
shadowLayer.layer.masksToBounds = true
shadowLayer.clipsToBounds = false


self.superview?.addSubview(shadowLayer)
self.superview?.bringSubviewToFront(self)
}
}

An alternative way to get more usable and consistent button.

Swift 2:

func getImageWithColor(color: UIColor, size: CGSize, cornerRadius:CGFloat) -> UIImage {
let rect = CGRectMake(0, 0, size.width, size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 1)
UIBezierPath(
roundedRect: rect,
cornerRadius: cornerRadius
).addClip()
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}


let button = UIButton(type: .Custom)
button.frame = CGRectMake(20, 20, 200, 50)
button.setTitle("My Button", forState: UIControlState.Normal)
button.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
self.addSubview(button)


let image = getImageWithColor(UIColor.whiteColor(), size: button.frame.size, cornerRadius: 5)
button.setBackgroundImage(image, forState: UIControlState.Normal)


button.layer.shadowRadius = 5
button.layer.shadowColor = UIColor.blackColor().CGColor
button.layer.shadowOpacity = 0.5
button.layer.shadowOffset = CGSizeMake(0, 1)
button.layer.masksToBounds = false

Swift 3:

func getImageWithColor(_ color: UIColor, size: CGSize, cornerRadius:CGFloat) -> UIImage? {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}


let button = UIButton(type: .custom)
button.frame = CGRect(x:20, y:20, width:200, height:50)
button.setTitle("My Button", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
self.addSubview(button)


if let image = getImageWithColor(UIColor.white, size: button.frame.size, cornerRadius: 5) {
button.setBackgroundImage(image, for: .normal)
}


button.layer.shadowRadius = 5
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOpacity = 0.5
button.layer.shadowOffset = CGSize(width:0, height:1)
button.layer.masksToBounds = false

If somebody need add shadows to rounded buttons in Swift 3.0, here is a good method to do it.

func addShadowForRoundedButton(view: UIView, button: UIButton, shadowColor: UIColor, shadowOffset: CGSize, opacity: Float = 1) {
let shadowView = UIView()
shadowView.backgroundColor = shadowColor
shadowView.layer.opacity = opacity
shadowView.layer.cornerRadius = button.bounds.size.width / 2
shadowView.frame = CGRect(origin: CGPoint(x: button.frame.origin.x + shadowOffset.width, y: button.frame.origin.y + shadowOffset.height), size: CGSize(width: button.bouds.width, height: button.bounds.height))
self.view.addSubview(shadowView)
view.bringSubview(toFront: button)
}

Use this method in func viewDidLayoutSubviews() as bellow:

override func viewDidLayoutSubviews() {
addShadowForRoundedButton(view: self.view, button: button, shadowColor: .black, shadowOffset: CGSize(width: 2, height: 2), opacity: 0.5)
}

The effect of this method is: enter image description here

To improve PiterPan's answer and show a real shadow (not just a background with no blur) with a circular button in Swift 3:

override func viewDidLoad() {
super.viewDidLoad()
myButton.layer.masksToBounds = false
myButton.layer.cornerRadius = myButton.frame.height/2
myButton.clipsToBounds = true
}


override func viewDidLayoutSubviews() {
addShadowForRoundedButton(view: self.view, button: myButton, opacity: 0.5)
}


func addShadowForRoundedButton(view: UIView, button: UIButton, opacity: Float = 1) {
let shadowView = UIView()
shadowView.backgroundColor = UIColor.black
shadowView.layer.opacity = opacity
shadowView.layer.shadowRadius = 5
shadowView.layer.shadowOpacity = 0.35
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowView.layer.cornerRadius = button.bounds.size.width / 2
shadowView.frame = CGRect(origin: CGPoint(x: button.frame.origin.x, y: button.frame.origin.y), size: CGSize(width: button.bounds.width, height: button.bounds.height))
self.view.addSubview(shadowView)
view.bringSubview(toFront: button)
}

Refactored this to support any view. Subclass your view from this and it should have rounded corners. If you add something like a UIVisualEffectView as a subview to this view you likely need to use the same rounded corners on that UIVisualEffectView or it won't have rounded corners.

Rounded corners with shadow for UIView - screenshot also uses a blur which is a normal UIVisualEffectView which also has rounded corners

/// Inspiration: https://stackoverflow.com/a/25475536/129202
class ViewWithRoundedcornersAndShadow: UIView {
private var theShadowLayer: CAShapeLayer?


override func layoutSubviews() {
super.layoutSubviews()


if self.theShadowLayer == nil {
let rounding = CGFloat.init(22.0)


let shadowLayer = CAShapeLayer.init()
self.theShadowLayer = shadowLayer
shadowLayer.path = UIBezierPath.init(roundedRect: bounds, cornerRadius: rounding).cgPath
shadowLayer.fillColor = UIColor.clear.cgColor


shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowRadius = CGFloat.init(3.0)
shadowLayer.shadowOpacity = Float.init(0.2)
shadowLayer.shadowOffset = CGSize.init(width: 0.0, height: 4.0)


self.layer.insertSublayer(shadowLayer, at: 0)
}
}
}

My custom button with some shadow and rounded corners, I use it directly within the Storyboard with no need to touch it programmatically.

Swift 4

class RoundedButtonWithShadow: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.layer.masksToBounds = false
self.layer.cornerRadius = self.frame.height/2
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
self.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 1.0
}
}

enter image description here

Extension to drop shadow and corner radius

extension UIView {


func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, shadowRadius: CGFloat = 1, scale: Bool = true, cornerRadius: CGFloat) {
let shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = color.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = offSet
shadowLayer.shadowOpacity = opacity
shadowLayer.shadowRadius = shadowRadius
layer.insertSublayer(shadowLayer, at: 0)
}


}

Here is the solution that will work!


extension UIView {


func applyShadowWithCornerRadius(color:UIColor, opacity:Float, radius: CGFloat, edge:AIEdge, shadowSpace:CGFloat)    {


var sizeOffset:CGSize = CGSize.zero
switch edge {
case .Top:
sizeOffset = CGSize(width: 0, height: -shadowSpace)
case .Left:
sizeOffset = CGSize(width: -shadowSpace, height: 0)
case .Bottom:
sizeOffset = CGSize(width: 0, height: shadowSpace)
case .Right:
sizeOffset = CGSize(width: shadowSpace, height: 0)




case .Top_Left:
sizeOffset = CGSize(width: -shadowSpace, height: -shadowSpace)
case .Top_Right:
sizeOffset = CGSize(width: shadowSpace, height: -shadowSpace)
case .Bottom_Left:
sizeOffset = CGSize(width: -shadowSpace, height: shadowSpace)
case .Bottom_Right:
sizeOffset = CGSize(width: shadowSpace, height: shadowSpace)




case .All:
sizeOffset = CGSize(width: 0, height: 0)
case .None:
sizeOffset = CGSize.zero
}


self.layer.cornerRadius = self.frame.size.height / 2
self.layer.masksToBounds = true;


self.layer.shadowColor = color.cgColor
self.layer.shadowOpacity = opacity
self.layer.shadowOffset = sizeOffset
self.layer.shadowRadius = radius
self.layer.masksToBounds = false


self.layer.shadowPath = UIBezierPath(roundedRect:self.bounds, cornerRadius:self.layer.cornerRadius).cgPath
}
}


enum AIEdge:Int {
case
Top,
Left,
Bottom,
Right,
Top_Left,
Top_Right,
Bottom_Left,
Bottom_Right,
All,
None
}

Finally, to apply shadow with corner radius call as per below:

viewRounded.applyShadowWithCornerRadius(color: .gray, opacity: 1, radius: 15, edge: AIEdge.All, shadowSpace: 15)


Result Image

enter image description here

UPDATE: If you don't see the expected output then try calling the extension method from Main Thread, that will work for sure!

DispatchQueue.main.async {
viewRounded.applyShadowWithCornerRadius(color: .gray, opacity: 1, radius: 15, edge: AIEdge.All, shadowSpace: 15)
}

Exact solution for 2020 syntax

import UIKit
class ColorAndShadowButton: UIButton {
override init(frame: CGRect) { super.init(frame: frame), common() }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder), common() }
private func common() {
// UIButton is tricky: you MUST set the clear bg in bringup;  NOT in layout
backgroundColor = .clear
clipsToBounds = false
layer.insertSublayer(colorAndShadow, below: layer)
}
    

lazy var colorAndShadow: CAShapeLayer = {
let s = CAShapeLayer()
// set your button color HERE (NOT on storyboard)
s.fillColor = UIColor.black.cgColor
// now set your shadow color/values
s.shadowColor = UIColor.red.cgColor
s.shadowOffset = CGSize(width: 0, height: 10)
s.shadowOpacity = 1
s.shadowRadius = 10
// now add the shadow
layer.insertSublayer(s, at: 0)
return s
}()
    

override func layoutSubviews() {
super.layoutSubviews()
// you MUST layout these two EVERY layout cycle:
colorAndShadow.frame = bounds
colorAndShadow.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
}
}

enter image description here

  • Note that the very old top answer here is correct but has a critical error

Note that UIButton is unfortunately quite different from UIView in iOS.

  • Due to a strange behavior in iOS, you must set the background color (which of course must be clear in this case) in initialization, not in layout. You could just set it clear in storyboard (but you usually click it to be some solid color simply so you can see it when working in storyboard.)

In general combos of shadows/rounding are a real pain in iOS. Similar solutions:

https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
https://stackoverflow.com/a/41553784/294884 - two-corner problem
https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers

Swift 5 & No need of "UIBezierPath"

    view.layer.cornerRadius = 15
view.clipsToBounds = true
view.layer.masksToBounds = false
view.layer.shadowRadius = 7
view.layer.shadowOpacity = 0.6
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowColor = UIColor.red.cgColor

You can create a protocol and conform it to you UIView, UIButton, Cell or whatever you want like that:

protocol RoundedShadowable: class {
var shadowLayer: CAShapeLayer? { get set }
var layer: CALayer { get }
var bounds: CGRect { get }
}
​
extension RoundedShadowable {
func applyShadowOnce(withCornerRadius cornerRadius: CGFloat, andFillColor fillColor: UIColor) {
if self.shadowLayer == nil {
let shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
shadowLayer.fillColor = fillColor.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0)
shadowLayer.shadowOpacity = 0.2
shadowLayer.shadowRadius = 3
self.layer.insertSublayer(shadowLayer, at: 0)
self.shadowLayer = shadowLayer
}
}
}
​
class RoundShadowView: UIView, RoundedShadowable {


var shadowLayer: CAShapeLayer?
private let cornerRadius: CGFloat
private let fillColor: UIColor


init(cornerRadius: CGFloat, fillColor: UIColor) {
self.cornerRadius = cornerRadius
self.fillColor = fillColor
super.init(frame: .zero)
}


required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func layoutSubviews() {
super.layoutSubviews()
self.applyShadowOnce(withCornerRadius: self.cornerRadius, andFillColor: self.fillColor)
}
}
​
class RoundShadowButton: UIButton, RoundedShadowable {


var shadowLayer: CAShapeLayer?
private let cornerRadius: CGFloat
private let fillColor: UIColor


init(cornerRadius: CGFloat, fillColor: UIColor) {
self.cornerRadius = cornerRadius
self.fillColor = fillColor
super.init(frame: .zero)
}


required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func layoutSubviews() {
super.layoutSubviews()
self.applyShadowOnce(withCornerRadius: self.cornerRadius, andFillColor: self.fillColor)
}
}

Corner Radius with Shadow

Short and simple way !!!!!

extension CALayer {
func applyCornerRadiusShadow(
color: UIColor = .black,
alpha: Float = 0.5,
x: CGFloat = 0,
y: CGFloat = 2,
blur: CGFloat = 4,
spread: CGFloat = 0,
cornerRadiusValue: CGFloat = 0)
{
cornerRadius = cornerRadiusValue
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2.0
if spread == 0 {
shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
}

Use of code

btn.layer.applyCornerRadiusShadow(color: .black,
alpha: 0.38,
x: 0, y: 3,
blur: 10,
spread: 0,
cornerRadiusValue: 24)

No need maskToBound

Please verify clipsToBounds is false.

OUTPUT

enter image description here

UIButton Extension

Many people have proposed using a custom class of UIButton which is totally fine. Just in case you want an extension, like me, here's one. Written in Swift 5.

extension UIButton {
/// Adds a shadow to the button, with a corner radius
/// - Parameters:
///   - corner: The corner radius to apply to the shadow and button
///   - color: The color of the shaodw
///   - opacity: The opacity of the shadow
///   - offset: The offset of the shadow
///   - radius: The radius of the shadow
func addShadow(corner: CGFloat = 20, color: UIColor = .black, opacity: Float = 0.3, offset: CGSize = CGSize(width: 0, height: 5), radius: CGFloat = 5) {
let shadowLayer = CAShapeLayer()
layer.cornerRadius = corner
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: corner).cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.shadowColor = color.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = offset
shadowLayer.shadowOpacity = opacity
shadowLayer.shadowRadius = radius
        

layer.insertSublayer(shadowLayer, at: 0)
}
}