setting background highlight on custom button layer - ios

I am working on an app where I have a custom button class (I found this class here on StackOverflow - credit to the original poster). where I have added a CAShapelayer to the button to give it a shape like the rounded rectangular button. This works well, but I would like to highlight the button when the user holds their finger on it, and I am running into a problem. When I override the highlighted color, it only highlights the layer BEHIND my rounded rectangle layer.
Can someone help me understand what I need to do to make my custom button highlight properly ? I am building this in Swift, in iOS8.1. Thank you much!
Custom button code:
public var roundRectCornerRadius: CGFloat = 8 {
didSet {
self.setNeedsLayout()
}
}
/// background of the rounded lyer
public var roundRectColor: UIColor = UIColor.whiteColor() {
didSet {
self.setNeedsLayout()
}
}
override public func layoutSubviews() {
super.layoutSubviews()
layoutRoundRectLayer()
}
// how do i get this into the roundrectlayer?
override public var highlighted: Bool {
get {
return super.highlighted
}
set {
if newValue {
backgroundColor = UIColor.blackColor()
}
else {
backgroundColor = UIColor.whiteColor()
}
super.highlighted = newValue
}
}
private var roundRectLayer: CAShapeLayer?
private func layoutRoundRectLayer() {
if let existingLayer = roundRectLayer {
existingLayer.removeFromSuperlayer()
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: roundRectCornerRadius).CGPath
shapeLayer.fillColor = roundRectColor.CGColor
self.layer.insertSublayer(shapeLayer, atIndex: 0)
self.roundRectLayer = shapeLayer
}

Related

Can't change border colour of switch inside tableview's cell

I've a switch inside table which I'm creating programmatically. I can't change switch's off border colour to gray. I tried tint colour which isn't working either.
How to fix it?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell else {
fatalError("...")
}
//...
let switchView = UISwitch()
switchView.layer.borderColor = UIColor.greyColour.cgColor
cell.accessoryView = switchView
return cell
}
You didn't specify what effect you want to achieve but for layer.borderColor to work you need to setup layer.borderWidth also. However, because switch layer is rectangular it will look like this:
Which might be not what you want. So to make the border follow the switcher's shape you'll need to modify its corner radius:
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.borderWidth = 1.0
switchView.layer.cornerRadius = 16.0
to make it looks like this:
Update
If you want to apply border only for switcher off state it'll be a bit more tricky because you need to handle switcher states changes. The easiest way I could think of is to subclass UISwitch and provide your own behaviour by overriding sendActions method:
class BorderedSwitch: UISwitch {
var borderColor: UIColor = UIColor.gray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var isOn: Bool {
didSet {
updateState()
}
}
override func sendActions(for controlEvents: UIControl.Event) {
super.sendActions(for: controlEvents)
if controlEvents.contains(.valueChanged) {
updateState()
}
}
private func setup() {
layer.borderColor = borderColor.cgColor
layer.cornerRadius = frame.height / 2
layer.borderWidth = 1.0
}
private func updateState() {
layer.borderWidth = isOn ? 0.0 : 1.0
}
}
Notice that I also updated cornerRadius value to frame.height / 2 to avoid magic numbers
If you add a switch with code, it looks just like a switch you add in a storyboard. Neither way of creating a switch has a border color.
The code below adds a switch to a view controller's content view:
#IBOutlet var switchView: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchView = UISwitch()
switchView.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
if false {
//Add a gray border to the switch
switchView.layer.borderWidth = 1.0 // Draw a rounded rect around the switchView so you can see it
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.cornerRadius = 16
}
//Add it to the container view
view.addSubview(switchView)
//Create center x & y layout anchors (with no offset to start)
let switchViewXAnchor = switchView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
switchViewXAnchor.isActive = true
let switchViewYAnchor = switchView.centerYAnchor.constraint(equalTo: view.centerYAnchor,
constant: 0.0)
switchViewYAnchor.isActive = true
}
It looks perfectly normal.

how to set gradient layer width to be the same as button width?

I am new in iOS Development, I want to make gradient rounded button by using this designable class
#IBDesignable
class GradientButton: UIButton {
let gradientLayer = CAGradientLayer()
#IBInspectable
var topGradientColor: UIColor? {
didSet {
setGradient(topGradientColor: topGradientColor, bottomGradientColor: bottomGradientColor)
}
}
#IBInspectable
var bottomGradientColor: UIColor? {
didSet {
setGradient(topGradientColor: topGradientColor, bottomGradientColor: bottomGradientColor)
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
#IBInspectable var fullRounded: Bool = false {
didSet {
updateCornerRadius()
}
}
#IBInspectable var cornerRadiusOfButton : CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadiusOfButton
}
}
func updateCornerRadius() {
layer.cornerRadius = fullRounded ? (frame.size.height / 2) : cornerRadiusOfButton
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
private func setGradient(topGradientColor: UIColor?, bottomGradientColor: UIColor?) {
if let topGradientColor = topGradientColor, let bottomGradientColor = bottomGradientColor {
gradientLayer.frame = bounds
gradientLayer.colors = [topGradientColor.cgColor, bottomGradientColor.cgColor]
gradientLayer.borderColor = layer.borderColor
gradientLayer.borderWidth = layer.borderWidth
gradientLayer.cornerRadius = layer.cornerRadius
layer.insertSublayer(gradientLayer, at: 0)
} else {
gradientLayer.removeFromSuperlayer()
}
}
}
but here is the result:
as you can see the width of the gradient layer button is incorret.
here is the constraint layout I use:
I want that gradient layer to have the same width as the original button, I suspect that the problem is in this line
gradientLayer.frame = bounds
but unfortunately I don't know how to set the gradient layer width to be the same size as the original button programmatically in #IBDesignable class
could you please help me to solve this issue ? thanks in advance
Override layoutSubviews inside GradientButton and update the gradientLayer frame as,
override func layoutSubviews() {
super.layoutSubviews()
self.gradientLayer.frame = bounds
}

Applying corner radius to a specific UIView corner inside Storyboard does not work for all corners

I made a custom class for this. But it works only for top left corner, not the others:
#IBDesignable
public class RoundedView: UIView {
#IBInspectable public var topLeft: Bool = false
#IBInspectable public var topRight: Bool = false
#IBInspectable public var bottomLeft: Bool = false
#IBInspectable public var bottomRight: Bool = false
#IBInspectable public var cornerRadius: Int = 0
public override func awakeFromNib() {
var options = UIRectCorner()
if topLeft { options = options.union(.topLeft) }
if topRight { options = options.union(.topRight) }
if bottomLeft { options = options.union(.bottomLeft) }
if bottomRight { options = options.union(.bottomRight) }
let path = UIBezierPath(roundedRect:self.bounds,
byRoundingCorners:options,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
Here is how it looks when running in simulator and I apply corner radius for all 4 corners:
What's going on here ? TopLeft works, TopRight slightly and bottom corners not at all. I am confused.
You should update the mask in the override of layoutSubviews. As the constraints update the frame of the view, layoutSubviews is called, so that’s the right place to update the mask.
As an aside, I’d discourage the use of awakeFromNib to configure the view. It works fine if you use storyboard, but programmatically created views won’t call this. If you do want to configure the view when it’s created, it’s better to put the code in some private configuration method which is called by init(frame:) and init(coder:). That way it works for both storyboards and programmatically created views.
I might also suggest observers on the inspectable properties, to trigger the update of the corner rounding. You want the corner rounding to update automatically if you change these properties.
Thus:
#IBDesignable
public class RoundedView: UIView {
#IBInspectable public var topLeft: Bool = false { didSet { setNeedsLayout() } }
#IBInspectable public var topRight: Bool = false { didSet { setNeedsLayout() } }
#IBInspectable public var bottomLeft: Bool = false { didSet { setNeedsLayout() } }
#IBInspectable public var bottomRight: Bool = false { didSet { setNeedsLayout() } }
#IBInspectable public var cornerRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
public override func layoutSubviews() {
super.layoutSubviews()
var options = UIRectCorner()
if topLeft { options.formUnion(.topLeft) }
if topRight { options.formUnion(.topRight) }
if bottomLeft { options.formUnion(.bottomLeft) }
if bottomRight { options.formUnion(.bottomRight) }
let path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: options,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}
}
Or, alternatively, if targeting iOS 11 and later, we can let CoreAnimation do the rounding:
#IBDesignable
public class RoundedView: UIView {
#IBInspectable public var topLeft: Bool = false { didSet { updateCorners() } }
#IBInspectable public var topRight: Bool = false { didSet { updateCorners() } }
#IBInspectable public var bottomLeft: Bool = false { didSet { updateCorners() } }
#IBInspectable public var bottomRight: Bool = false { didSet { updateCorners() } }
#IBInspectable public var cornerRadius: CGFloat = 0 { didSet { updateCorners() } }
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
updateCorners()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateCorners()
}
}
private extension RoundedView {
func updateCorners() {
var corners = CACornerMask()
if topLeft { corners.formUnion(.layerMinXMinYCorner) }
if topRight { corners.formUnion(.layerMaxXMinYCorner) }
if bottomLeft { corners.formUnion(.layerMinXMaxYCorner) }
if bottomRight { corners.formUnion(.layerMaxXMaxYCorner) }
layer.maskedCorners = corners
layer.cornerRadius = cornerRadius
}
}
The nice thing about this latter approach is that CoreAnimation will honor our corner rounding if we happen to be animating the frame of the view:
If you use the aforementioned layoutSubviews approach, then you have to manage the animation manually (e.g. with CADisplayLink).

Swift 3.0: Change a button color once pressed

I've created a circular green button. Here is the CircularButton.swift where I've defined color and shape as below:
import UIKit
class CircularButton: UIButton {
#IBInspectable var fillColor: UIColor = UIColor.green
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
fillColor.setFill()
path.fill()
}
}
Here is the screen shot.
On the button pressed I would like to change its color to red. I've defined the function as below:
#IBAction func circularButtonPressed(_ sender: CircularButton) {
sender.fillColor = UIColor.red
sender.draw(CGRect(x: 0, y: 0, width: sender.frame.width,
height: sender.frame.height))
}
Any idea why the color is not changed to red?
Note: If I add below line:
sender.backgroundColor = UIColor.white
in the above function, the button color changes to red.
Thank you for your help.
You should not call draw(_:) from inside your code directly.
You just need to tell iOS that the control needs to be redrawn.
Remove the line calling draw(_:):
#IBAction func circularButtonPressed(_ sender: CircularButton) {
sender.fillColor = UIColor.red
}
And add observer to the fillColor property:
#IBInspectable var fillColor: UIColor = UIColor.green {
didSet(oldColor) {
if fillColor != oldColor {
self.setNeedsDisplay()
}
}
}

Setting the frame of a sublayer inside of layoutSubviews() makes changing it impossible. Any workaround for that?

I'm working on a customizable UITextField (see code below). I have added a border at the bottom (you can set it in the storyboard). However, I had problems setting the frame of the CALayer that this border consists of.
If I set it inside the didSet method of var showBottomBorder it doesn't appear on the screen. I think this is because the frame (of the UITextField) hasn't been calculated yet (maybe didSet gets called before that).
So I moved it to the layoutSubviews() method (see code below). This works perfectly.
But now I have another problem. I can't really change that frame anymore. Every time I change it, it gets reset by layoutSubviews() which I think is called then.
At the bottom of my code, there is the method textFieldDidBeginEditing. In there, I wanted to move up my bottom border (animated). But it doesn't work. The border does not move anywhere. And like I said, I think it's because I set the frame inside the layoutSubviews() method.
Is there a better way to set the frame of the bottom border? A way which allows me to change stuff?
#IBDesignable
class CustomizableTextField: UITextField, UITextFieldDelegate {
// MARK: - Properties
private var bottomBorder = CALayer()
// MARK: - #IBInspectables
#IBInspectable var roundCorners: CGFloat = 0 {
didSet {
self.layer.cornerRadius = roundCorners
self.clipsToBounds = true
}
}
/** -- */
#IBInspectable var borderWidth: CGFloat = 1.0 {
didSet {
self.layer.borderWidth = self.borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.white {
didSet {
self.layer.borderColor = self.borderColor.cgColor
}
}
/** -- */
/** -- */
private var showBottomBorder: Bool = false {
didSet {
switch showBottomBorder {
case true:
bottomBorder.borderColor = self.bottomBorderColor.cgColor
bottomBorder.borderWidth = self.bottomBorderWidth
self.layer.addSublayer(bottomBorder)
self.layer.masksToBounds = true
break
case false:
bottomBorder.removeFromSuperlayer()
break
}
}
}
#IBInspectable var bottomBorderWidth: CGFloat = 1.0 {
didSet {
self.showBottomBorder = false
self.showBottomBorder = true
}
}
#IBInspectable var bottomBorderColor: UIColor = UIColor.white {
didSet {
self.showBottomBorder = false
self.showBottomBorder = true
}
}
/** -- */
/** -- */
// Somwhow, the default panel for my font color doesn't change anything, so I created this
#IBInspectable var fixedFontColor: UIColor = UIColor.white {
didSet {
self.textColor = fixedFontColor
}
}
#IBInspectable var placeholderFontColor: UIColor = UIColor.white {
didSet {
var placeholderTxt = ""
if let txt = self.placeholder {
placeholderTxt = txt
}
self.attributedPlaceholder = NSAttributedString(string: placeholderTxt, attributes: [NSForegroundColorAttributeName: placeholderFontColor])
}
}
/** -- */
// MARK: - Overrides and Initializers
override init(frame: CGRect) {
super.init(frame: frame)
}
override func layoutSubviews() {
super.layoutSubviews()
// HERE
bottomBorder.frame = CGRect(x: 0, y: self.frame.size.height - self.bottomBorderWidth, width: self.frame.size.width, height: self.frame.size.height)
}
// setting the textField delegate to self
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//self.borderStyle = .none
self.delegate = self
}
// MARK: - Events
func textFieldDidBeginEditing(_ textField: UITextField) {
}
You can use and extension on UITextFiled for setting the border.
And keep a reference to it with KVC.
By overriding LayoutSubview, every the layout will change, we'l check if the border exists, if so remove it, and re-create a new one with the new frame:
import UIKit
let MyTopBorder = "myTopBorder"
let MyBottomBorder = "myBottomBorder"
struct Defaults {
static let width = CGFloat(1.0)
static func bottonBorderFrame(view: UIView)->CGRect {
return CGRect(x: CGFloat(0), y: view.frame.size.height - Defaults.width, width: view.frame.size.width, height: view.frame.size.height)
}
static func topBorderFrame(view: UIView)->CGRect {
return CGRect(x: CGFloat(0), y: CGFloat(0) , width: view.frame.size.width, height: Defaults.width)
}
}
extension UITextField
{
func setBottomBorder(color:CGColor)
{
if let isBottomBorder = self.getBottomBorderIfExists() {
isBottomBorder.removeFromSuperlayer()
}
self.setBorderWithFrame(Defaults.bottonBorderFrame(self), color: color, andKey: MyBottomBorder)
}
func setTopBorder(color:CGColor)
{
if let isTopBorder = self.getTopBorderIfExists() {
isTopBorder.removeFromSuperlayer()
}
self.setBorderWithFrame(Defaults.topBorderFrame(self), color: color, andKey: MyTopBorder)
}
func setBorderWithFrame(frame: CGRect, color: CGColor, andKey: String) {
self.borderStyle = UITextBorderStyle.None;
let border = CALayer()
border.borderColor = color
border.frame = frame
border.borderWidth = Defaults.width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
self.layer.setValue(border, forKey: andKey)
}
func removeTopBorder() {
if let isTopBorder = self.getTopBorderIfExists() {
self.layer.setValue(nil, forKey: MyTopBorder)
isTopBorder.removeFromSuperlayer()
}
}
func removeBottomBorder() {
if let isBottomBorder = self.getBottomBorderIfExists() {
self.layer.setValue(nil, forKey: MyBottomBorder)
isBottomBorder.removeFromSuperlayer()
}
}
private func getBorderIfExistsByKey(key: String)->CALayer? {
if let isBorderSet = self.layer.valueForKey(key) {
if let borderIsCALayer = isBorderSet as? CALayer {
return borderIsCALayer
}
}
return nil
}
private func getTopBorderIfExists()->CALayer? {
return self.getBorderIfExistsByKey(MyTopBorder)
}
private func getBottomBorderIfExists()->CALayer? {
return self.getBorderIfExistsByKey(MyBottomBorder)
}
public override func layoutSubviews() {
super.layoutSubviews()
// Update bottom on frame change
if let isBottomBorder = self.getBottomBorderIfExists() {
let borderColor = isBottomBorder .borderColor
self.removeBottomBorder()
self.setBottomBorder(borderColor!)
}
// Update top on frame change
if let isTopBorder = self.getTopBorderIfExists() {
let borderColor = isTopBorder.borderColor
self.removeTopBorder()
self.setTopBorder(borderColor!)
}
}
}
Usage:
let textField = UITextField(frame: CGRect(x: 100,y: 100, width: 100, height: 100))
textField.backgroundColor = UIColor.blueColor() // Thie color is for visulizing better
self.view.addSubview(textField)
textField.setBottomBorder(UIColor.blackColor().CGColor) // Now you have a border
textField.frame = CGRect(x: 150, y: 200, width: 200, height: 200) // And the border updated to the new frame
// Now if you would like to change from bottom to top, simply do this:
textField.removeBottomBorder()
textField.setTopBorder(UIColor.blackColor().CGColor)

Resources