I have a custom button with some shadow effect, following class i am using to create a custom button which i got from stackoverflow. It users CAShapeLayer to give effect of shadow on button.
import UIKit
class CustomButton: UIButton {
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
}
}
}
Following is an image of screen where i have created 4 buttons using this CustomButton Class which works fine.
when any button from above four is clicked i changed it's following property so it can look like an active button.
// to make button active
func setSelectedButton(sender:CustomButton){
//sender.backgroundColor = UIColor.red
sender.shadowLayer.fillColor = UIColor(named: "headerColor")?.cgColor
sender.setTitleColor(UIColor.white, for: .normal)
}
// to make button inactive
func setUnselected(sender:CustomButton){
//sender.backgroundColor = UIColor.init(red: 80/255, green:101/255, blue: 161/255, alpha: 1)
sender.shadowLayer.fillColor = UIColor.white.cgColor
sender.setTitleColor(.black, for: .normal)
}
Everything works fine. Now what i want is whenever view appears i want the first button to be selected by default which is routineButton to do that i have written following code in viewWillAppearMethod
override func viewWillAppear(_ animated: Bool) {
self.navigationItem.title = navigationTitle
self.view.makeToastActivity(.center)
self.routineButton.sendActions(for: .touchUpInside) //default selection of routineButton
loadEvaluationList(userId: 8, evaluationType: "RT") {
self.view.hideToastActivity()
}
}
when self.routineButton.sendActions(for: .touchUpInside) executes it call setSelectedButton(sender:CustomButton) method which gives error at following line
sender.shadowLayer.fillColor = UIColor(named: "headerColor")?.cgColor
which says shadowLayer is nil. This problem occur only when i try to set default selected button on viewWillAppear Method otherwise it works perfact.
I think the problem occur because shadowLayer property of CustomButton is not initialised at time of viewWillAppear.
so anyone knows what should i do? it will be helpful. Thank you in advance.
Move the initialization code for shadowLayer in the init methods, something like this:
import UIKit
class CustomButton: UIButton {
var shadowLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.initShadowLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initShadowLayer()
}
func initShadowLayer() {
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)
}
}
override func layoutSubviews() {
super.layoutSubviews()
// Reset the path because bounds could have been changed
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
shadowLayer.shadowPath = shadowLayer.path
}
}
viewWillAppear is called before layoutSubviews of the button and shadowLayer is not initialized yet. Also try not to call sendActions(for: .touchUpInside), instead call the setSelectedButton function if you just want to configure the button appearance.
Related
Hello i have some views with rounded corners, and I'd like to apply a shadow to this views.
SO first I round the view :
view.layer.cornerRadius = 15
view.clipsToBounds = true
then I apply the shadow :
func dropShadow(scale: Bool = true) {
self.layer.masksToBounds = false
self.layer.cornerRadius = 15
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
self.layer.shadowOffset = CGSize(width: 3, height: 3)
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 5
}
view.dropShadow()
I got my rounded view with a shadow but the shadow is not rounded like my view. The shadow is not rounded at all
You cannot cast a shadow from a view whose clipsToBounds is true. If a view's masksToBounds is true, its clipsToBounds is true; they are the same thing.
If you want a shadow to appear to come from from a view that clips, you need to use two views: one that the user can see, with rounded corners and clipsToBounds set to true, and another that the user can't see because it's behind the first one, also with rounded corners, but with clipsToBounds set to false, to cast the shadow.
class ShadowView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
self.isOpaque = true
self.backgroundColor = .black
self.dropShadow()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dropShadow() {
self.layer.masksToBounds = false
self.layer.cornerRadius = 15
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 3, height: 3)
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 5
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let r = CGRect(x: 100, y: 100, width: 100, height: 100)
let v = UIImageView(frame:r)
v.image = UIImage(named:"marsSurface.jpg")
v.clipsToBounds = true
v.backgroundColor = .red
v.layer.cornerRadius = 15
self.view.addSubview(ShadowView(frame:r))
self.view.addSubview(v)
}
}
Note that neither view is a subview of the other, nor do they have a superview that clips, as that would clip the shadow.
Trying to have a single view handle both corner rounding and shadow logic is not recommended. The best way to create the effect you are looking for is to wrap your rounded view with another parent view that owns the shadow.
This can be best illustrated with a playground. Try this code out in a playground.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let shadowView = UIView()
shadowView.frame = CGRect(x: 50, y: 200, width: 200, height: 100)
shadowView.backgroundColor = nil
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 4.0)
shadowView.layer.shadowRadius = 8.0
shadowView.layer.shadowOpacity = 1
let roundedView = UIView()
roundedView.frame = shadowView.bounds
roundedView.backgroundColor = .gray
roundedView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,.layerMaxXMinYCorner]
roundedView.layer.cornerRadius = 10
roundedView.layer.masksToBounds = true
shadowView.addSubview(roundedView)
view.addSubview(shadowView)
view.backgroundColor = UIColor.white
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Don't forget to set the shadowPath property on your shadow view's layer in either viewWillLayoutSubviews or layoutSubviews. If this property is set to nil, UIKit has to perform off screen rendering to figure out how to draw the shadow rect.
I'm trying to create a UIButton with rounded corners, a background image and a shadow. Before adding the shadow, everything works fine.
But after adding shadow values, the shadow doesn't show up. Obviously due to clipsToBounds property value being set to true. If I remove that, it looks like this.
Since I need the corner radius as well, I cannot have the clipsToBounds be false.
This is my code.
class CustomButton: UIButton {
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
clipsToBounds = true
}
}
var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
var shadowColor: UIColor? {
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
return nil
}
set {
if let color = newValue {
layer.shadowColor = color.cgColor
} else {
layer.shadowColor = nil
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
private lazy var button: CustomButton = {
let button = CustomButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setBackgroundImage(UIImage(named: "Rectangle"), for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitle("Sign Up", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
button.cornerRadius = 20
button.shadowColor = .systemGreen
button.shadowRadius = 10
button.shadowOpacity = 1
button.shadowOffset = CGSize(width: 0, height: 0)
return button
}()
Is there a workaround to have both the shadow and the corner radius?
Demo project
You can do it via adding shadow and background image with different layer.
First, if you don't need the properties, remove all and modify your CustomButton implementation just like below (modify as your need):
class CustomButton: UIButton {
private let cornerRadius: CGFloat = 20
private var imageLayer: CALayer!
private var shadowLayer: CALayer!
override func draw(_ rect: CGRect) {
addShadowsLayers(rect)
}
private func addShadowsLayers(_ rect: CGRect) {
// Add Image
if self.imageLayer == nil {
let imageLayer = CALayer()
imageLayer.frame = rect
imageLayer.contents = UIImage(named: "Rectangle")?.cgImage
imageLayer.cornerRadius = cornerRadius
imageLayer.masksToBounds = true
layer.insertSublayer(imageLayer, at: 0)
self.imageLayer = imageLayer
}
// Set the shadow
if self.shadowLayer == nil {
let shadowLayer = CALayer()
shadowLayer.masksToBounds = false
shadowLayer.shadowColor = UIColor.systemGreen.cgColor
shadowLayer.shadowOffset = .zero
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = 10
shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(shadowLayer, at: 0)
self.shadowLayer = shadowLayer
}
}
}
And initialize your button like below:
private lazy var button: CustomButton = {
let button = CustomButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.white, for: .normal)
button.setTitle("Sign Up", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
return button
}()
You need to use two separate views for shadow and image. I can't find any solution to set image, shadow, and corner radius using the same button layer.
Make button's corner radius(clipsToBounds=true) rounded and set the image on it.
Take a shadow view under the button with proper shadow radius and offset.
You can add view.layer.masksToBounds = false
This will disable clipping for sublayer
https://developer.apple.com/documentation/quartzcore/calayer/1410896-maskstobounds
I created a custom UIButton with this initialiser :
class CustomButton : UIButton{
override init(frame: CGRect) {
super.init(frame: frame)
setUpButtoninClass(frame)
addTarget(self, action: #selector(handleTap), for:.touchUpInside )
}
fileprivate func setUpButtoninClass(_ frame: CGRect) {
let padding : CGFloat = 16
self.frame = frame
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowOpacity = 0.3
layer.shadowOffset = .zero
layer.shadowRadius = 10
layer.cornerRadius = frame.width/2
backgroundColor = UIColor(white: 0.9, alpha: 1)
let buttonView = UIView(frame: frame)
buttonView.layer.cornerRadius = frame.width/2
buttonView.backgroundColor = .white
addSubview(buttonView)
let imageView = UIImageView(image: UIImage(named: "pen")?.withRenderingMode(.alwaysTemplate))
imageView.tintColor = UIColor(white: 0.7, alpha: 1)
buttonView.addSubview(imageView)
imageView.anchor(top: buttonView.topAnchor, leading: buttonView.leadingAnchor, bottom: buttonView.bottomAnchor, trailing: buttonView.trailingAnchor, padding: UIEdgeInsets.init(top: padding, left: padding, bottom: padding, right: padding))
}
#objc func handleTap(){
print("I'm here")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}}
In the initialiser I'm adding a target but when I actually initialise the custom button in the VC the #selector method (handleTap) is not called.
This is the implementation of custom Button in VC:
class ViewController: UIViewController {
let circularButton = CustomButton(frame: CGRect(x: 0, y: 0, width: 70, height: 70))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(circularButton)
circularButton.center = view.center
}
I also tried to add the target when initialising the CustomButton in the VC but nothing changed.
I would like to know where I'm making a mistake in setting up the button.
EDIT 1 :
this is the Debug View Hierarchy
OMG, after debug your code, buttonView and imageView is on the top. Button is behide. You can set the color to debug it more easily. Delete 2 views above make your code works perfectly
I think it's your fault here,
Touch is not detected because you added an ImageView to the top of UIButton.
Try this, or this one,
buttonView.isUserInteractionEnabled = true
imageView.isUserInteractionEnabled = true
This seems to be a tricky problem I cannot find the solution for.
I need a UIButton with:
rounded corners,
background color for state,
drop shadow.
I can achieve the the corner radius and background color for state subclassing a UIButton and setting the parameters:
// The button is set to "type: Custom" in the interface builder.
// 1. rounded corners
self.layer.cornerRadius = 10.0
// 2. Color for state
self.backgroundColor = UIColor.orange // for state .normal
self.setBackgroundColor(color: UIColor.red, forState: .highlighted)
// 3. Add the shadow
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 6.0
self.layer.masksToBounds = false
// I'm also using an extension to be able to set the background color
extension UIButton {
func setBackgroundColor(color: UIColor, forState: UIControl.State) {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(color.cgColor)
context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setBackgroundImage(colorImage, for: forState)
}
}
}
The problem:
Once the button is rendered, seems fine, has the desired background color, rounded corners and the shadow. But once is pressed the setBackgroundImage(image, for: forState) launches and gets rid of the radius, so the button is displayed as a square with the desired color and shadow.
Is there a way to preserve the radius when the button is pressed (.highlighted)?
I did try the solutions from this post (for example) but none consider the setBackgroundImage(image, for: forState). I cannot find anything that works...
If you just want to change the background color for .normal and .highlighted - that is, you don't need a background image - you can override var isHighlighted to handle the color changes.
Here is an example UIButton subclass. It is marked #IBDesignable and has normalBackground and highlightBackground colors marked as #IBInspectable so you can set them in Storyboard / Interface Builder:
#IBDesignable
class HighlightButton: UIButton {
#IBInspectable
var normalBackground: UIColor = .clear {
didSet {
backgroundColor = self.normalBackground
}
}
#IBInspectable
var highlightBackground: UIColor = .clear
override open var isHighlighted: Bool {
didSet {
backgroundColor = isHighlighted ? highlightBackground : normalBackground
}
}
func setBackgroundColor(_ c: UIColor, forState: UIControl.State) -> Void {
if forState == UIControl.State.normal {
normalBackground = c
} else if forState == UIControl.State.highlighted {
highlightBackground = c
} else {
// implement other states as desired
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// 1. rounded corners
self.layer.cornerRadius = 10.0
// 2. Default Colors for state
self.setBackgroundColor(.orange, forState: .normal)
self.setBackgroundColor(.red, forState: .highlighted)
// 3. Add the shadow
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 6.0
self.layer.masksToBounds = false
}
}
I am new to swift. I have two UITextFields email and password. I want to add only bottom border to text fields and for that I am using the following method,
extension UITextField {
func setBottomBorderWithLayer() {
self.borderStyle = .none
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
self.layer.shadowOpacity = 1.0
self.layer.shadowRadius = 0.0
}
}
And this is how I am calling the method
emailTextField.setBottomBorderWithLayer()
passwordTextField.setBottomBorderWithLayer()
Now the problem is setBottomBorderWithLayer() method only setting bottom border for only emailTextField and not for passwordTextField.
Can anyone explain that where is the problem?
Thanks in advance.
We need more details to be able to define where the error is, but I suggest another type of implementation that will meet your need.
Create a class that extends from UITextField, in this class you can define all the necessary layout settings, facilitating reuse, as you will only need to define the class in TextField from Storyboard -> TextField -> Identity Inspector -> Custom Class -> Class.
Class example:
import UIKit
class BottomBorderTextField: UITextField {
override init (frame: CGRect) {
super.init (frame: frame)
addBottomBorder()
}
required init? (coder: NSCoder) {
super.init (coder: coder)
addBottomBorder()
}
func addBottomBorder() {
// Logic for adding style
layer.backgroundColor = UIColor.white.cgColor
layer.borderColor = UIColor.gray.cgColor
layer.borderWidth = 0.0
layer.cornerRadius = 5
layer.masksToBounds = false
layer.shadowRadius = 2.0
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 1.0, height: 1.0)
layer.shadowOpacity = 1.0
layer.shadowRadius = 1.0
}
}
Hope this helps.
I agree with Silas, but just adding on here...
If you want to create a colorful bottom bar to a textfield, this also works:
import Foundation
import UIKit
class Utilities {
static func styleTextField(_ textfield:UITextField) {
// Create the bottom line
let bottomLine = CALayer()
bottomLine.frame = CGRect(x: 0, y: textfield.frame.height - 2, width: textfield.frame.width, height: 2)
bottomLine.backgroundColor = UIColor.init(red: 116/255, green: 203/255, blue: 128/255, alpha: 1).cgColor
// Remove border on text field
textfield.borderStyle = .none
// Add the line to the text field
textfield.layer.addSublayer(bottomLine)
}