Button gradient not working when added to viewDidLayoutSubviews - ios

I am adding a button gradient using the below code
extension UIView {
func applyGradient(colors: [UIColor]) {
self.applyGradient(colors: colors, locations: nil)
}
func applyGradient(colors: [UIColor], locations: [NSNumber]?) {
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colors.map { $0.cgColor }
gradient.locations = locations
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
self.layer.insertSublayer(gradient, at: 0)
}
}
Calling initStyle() in viewDidLayoutSubviews() is not working.
func initStyle() {
submitBtn.applyGradient(colors: [#colorLiteral(red: 0.1176470588, green: 0.3882352941, blue: 0.5254901961, alpha: 1), #colorLiteral(red: 0.2941176471, green: 0.9098039216, blue: 0.9529411765, alpha: 1)])
submitBtn.layer.cornerRadius = 15.0
submitBtn.layer.masksToBounds = true
}
I am creating all UI elements programatically. I had constraints setup properly and is working.
lazy var submitBtn: UIButton = {
let btn = UIButton(type: .system)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("SUBMIT", for: .normal)
return btn
}()
How to make this working?
The gradient display only if I place initStyle() in viewDidAppear() and not in viewDidLayoutSubviews(), which is creating a delay in displaying the button gradient. I want to avoid this delay. So I am adding it in viewDidLayoutSubviews, but then the gradient does not appear.

I think the problem here is that self.bounds is 0 at the point you are calling your gradient function. Try calling it at a later time, for example viewWillAppear or call view.layoutSubViews to trigger iewDidLayoutSubviews()

lazy var submitBtn: UIButton = {
let btn = UIButton(type: .custom) //Set custom instead of system
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("SUBMIT", for: .normal)
return btn
}()

try using the function inside
viewWillAppeare()
Also why not apply the gradient into the button from the start without a separated function? that will make the button appears w\ the gradient already builtin.
if you want to set the gradient w\ a condition you could use the following
func displayButton(condition: Bool){
lazy var submitBtn: UIButton = {
let btn = UIButton(type: .system)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("SUBMIT", for: .normal)
if condition == true {
//set the gradient here
return btn
} else if condition == false
return btn
}
}()
override func viewDidLoad(){
super.viewDidLoad()
//here you can set the condition to show the gradient or not depending on what you want
displayButton(true) //will show the gradient
displayButton(false) // will show without gradient
}
I can't test this code at the moment so i'm not sure about it, give it a try.

Related

Some views with a CAGradientLayer don't render the gradient

I've been facing some difficulties trying to apply CAGradientLayers on two items in a view controller: a UIView and a UIButton. On investigation, when the gradient is applied to both items, only the UIButton has the gradient on it whereas the UIView appears transparent.
My gradient is defined as such:
import UIKit
struct K {
struct Design {
static let upGradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [upBlue.cgColor, upPurple.cgColor]
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}()
static let upBlue = UIColor(named: "UP Blue") ?? .systemBlue
static let upPurple = UIColor(named: "UP Purple") ?? .systemPurple
}
}
The function that applies the gradients (I used insertSublayer) is separated from the main View Controller file. Here's the code for that function:
import UIKit
extension UIButton {
func applyButtonDesign(_ gradientLayer: CAGradientLayer) {
self.layer.insertSublayer(gradientLayer, at: 0)
self.layer.cornerRadius = 10.0
self.layer.masksToBounds = true
}
}
extension UIView {
func applyHeaderDesign(_ gradientLayer: CAGradientLayer) {
self.layer.insertSublayer(gradientLayer, at: 0)
self.layer.cornerRadius = 10.0
self.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.clipsToBounds = true
}
}
(Note: It appears that addSublayer doesn't work either.)
My UI is being created programmatically (without a Storyboard), and I'm fairly new to it. Here's the code for the view controller where the issue is happening:
import UIKit
class WelcomeViewController: UIViewController {
var headerView: UIView!
var descriptionLabel: UILabel!
var focusImageView: UIImageView!
var promptLabel: UILabel!
var continueButton: UIButton!
let headerGradientLayer = K.Design.upGradientLayer
let buttonGradientLayer = K.Design.upGradientLayer
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let guide = view.safeAreaLayoutGuide
// MARK: Header View
headerView = UIView()
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.applyHeaderDesign(headerGradientLayer)
view.addSubview(headerView)
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: view.topAnchor),
headerView.rightAnchor.constraint(equalTo: view.rightAnchor),
headerView.leftAnchor.constraint(equalTo: view.leftAnchor),
headerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25)
])
// MARK: Other items in ViewController
// MARK: Continue Button
continueButton = UIButton()
continueButton.translatesAutoresizingMaskIntoConstraints = false
continueButton.applyButtonDesign(buttonGradientLayer)
continueButton.setTitle("Let's go!", for: .normal)
continueButton.titleLabel?.font = UIFont(name: "Metropolis Bold", size: 22.0)
continueButton.titleLabel?.textColor = .white
continueButton.addTarget(nil, action: #selector(continueButtonPressed), for: .touchUpInside)
view.addSubview(continueButton)
NSLayoutConstraint.activate([
continueButton.topAnchor.constraint(equalTo: promptLabel.bottomAnchor, constant: K.Layout.someSpaceBetween),
continueButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -K.Layout.someSpaceBetween),
continueButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: K.Layout.someSpaceBetween),
continueButton.bottomAnchor.constraint(equalTo: guide.bottomAnchor),
continueButton.heightAnchor.constraint(equalToConstant: 50.0)
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
headerGradientLayer.frame = headerView.bounds
buttonGradientLayer.frame = continueButton.bounds
}
// MARK: - Functions
#objc func continueButtonPressed() {
let newViewController = NameViewController()
newViewController.modalPresentationStyle = .fullScreen
newViewController.hero.modalAnimationType = .slide(direction: .left)
self.present(newViewController, animated: true, completion: nil)
}
}
When running on the Simulator, I get the below image that shows the issue. Notice that the gradient is not applied on the UIView but is on the UIButton.
What's causing this issue to happen, and how can I resolve this? If there's a fundamental concept that needs to be learned to tackle this issue, do share as well.
Both headerGradientLayer and buttonGradientLayer refer to the same layer instance because upGradientLayer is a stored property, not a computed property (as I'm assuming you meant for it to be). Since they both refer to the same instance, when you call this:
continueButton.applyButtonDesign(buttonGradientLayer)
...which internally calls this:
self.layer.insertSublayer(gradientLayer, at: 0)
...it removes the gradient layer instance from its current view (your header view) and adds it to your button (because layers cannot have more than one superlayer). The result is that you only see the gradient on your button and not your header view.
I imagine you meant for upGradientLayer to be a computed property so that it returns a new layer instance every time it's called, like this:
static var upGradientLayer: CAGradientLayer {
let layer = CAGradientLayer()
layer.colors = [upBlue.cgColor, upPurple.cgColor]
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}

Button Not Fully Drawn

I have a button with text and an image on it. It gets set up in viewDidAppear and then in the IBAction I change the Attributed title. For some reason the button background color doesn't completely cover the button on the initial draw. It leaves a horizontal sliver of white. I found that by running my formatButton function in the IBAction subsequent button presses show a properly drawn button. But I can't get the first loaded view of the button to look right. Any ideas?
I found that by formatting in the IBAction it fixed it for future button draws but a sendAction(.touchUpInside) couldn't even fake it into fixing the draw problem. (It did change the button text like the IBAction makes it though.)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
formatButton(btn: searchTitlesButton)
formatButton(btn: searchPeopleButton)
formatButton(btn: searchCategoryButton)
searchTitlesButton.setTitle("Title", for: .normal)
searchPeopleButton.setTitle("Actor", for: .normal)
//searchCategoryButton.setTitle(categoryList[searchCategoryIndex], for: .normal)
let fullString = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named:"DownArrow")
let imageString = NSAttributedString(attachment: imageAttachment)
fullString.append(NSAttributedString(string: categoryList[searchCategoryIndex]+" "))
fullString.append(imageString)
searchCategoryButton.setAttributedTitle(fullString, for: .normal)
formatButton(btn: searchCategoryButton)
postTableView.rowHeight = CGFloat(120)
}
#IBAction func searchCategoryButton(_ sender: Any) {
if searchCategoryIndex < categoryList.count - 1 {
searchCategoryIndex += 1
} else {
searchCategoryIndex = 0
}
// Going to try and make a formatted label with a string and image of a down arrow.
let fullString = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named:"DownArrow")
let imageString = NSAttributedString(attachment: imageAttachment)
fullString.append(NSAttributedString(string: categoryList[searchCategoryIndex]+" "))
fullString.append(imageString)
searchCategoryButton.setAttributedTitle(fullString, for: .normal)
formatButton(btn: searchCategoryButton)
}
func formatButton(btn:UIButton) {
btn.layer.cornerRadius = 5
btn.layer.borderWidth = 1
btn.layer.borderColor = UIColor.black.cgColor
btn.setTitleColor(UIColor.white, for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.bold)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = btn.bounds
let bottomColor = UIColor(red: CGFloat(25/255.0), green: CGFloat(113/255.0), blue: CGFloat(255/255.0), alpha: CGFloat(1.0))
gradientLayer.colors = [UIColor.white.cgColor, bottomColor.cgColor]
btn.layer.insertSublayer(gradientLayer, at: 0)
btn.clipsToBounds = true
}
The reason why the background gradient doesn't fully cover the button, it probably because the size of the button changes when you set the attributed title. The best way to solve this, is by creating a subclass of UIButton, so that you can update the frame of your custom gradient layer, whenever the button's bounds change. For example:
class GradientButton: UIButton {
private let gradientLayer = CAGradientLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
setTitleColor(UIColor.white, for: .normal)
titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.bold)
let bottomColor = UIColor(red: CGFloat(25/255.0), green: CGFloat(113/255.0), blue: CGFloat(255/255.0), alpha: CGFloat(1.0))
gradientLayer.colors = [UIColor.white.cgColor, bottomColor.cgColor]
layer.insertSublayer(gradientLayer, at: 0)
clipsToBounds = true
}
override var bounds: CGRect {
didSet {
gradientLayer.frame = layer.bounds
}
}
}
Then in the storyboard of nib you can change the class of the button to GradientButton. It should now automatically apply the gradient styling, and update the frame whenever the bounds of the button change.
I hope you find this useful. Let me know if you are still having issues.

Change Button Color on state change

I want for the buttons to have white background and blue title when highlighted. I have an extension of UIButton to set its background color.
extension UIButton {
func setBackgroundColor(color: UIColor, forState: UIControlState) {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
UIGraphicsGetCurrentContext()!.setFillColor(color.cgColor)
UIGraphicsGetCurrentContext()!.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setBackgroundImage(colorImage, for: forState)
self.clipsToBounds = true
}
}
and in the next function, I set up a particular button.
private func stylizingButton(button: UIButton){
button.layer.borderWidth = 2
button.layer.borderColor = textColor.cgColor
button.layer.cornerRadius = 8
button.setTitleColor(textColor, for: .normal)
button.setTitleColor(backgroundColor, for: .highlighted)
button.setBackgroundColor(color: .white, forState: .highlighted)
}
When I change the background color of the button to black, the result is some dark blue color. It is like the screen background color and the button's color are mixing.
Create a custom class for your button and handle your color changing properties on state like below.
class MyButton: UIButton {
fileprivate var titleColorNormal: UIColor = .white
fileprivate var titleColorHighlighted: UIColor = .blue
fileprivate var backgroundColorNormal: UIColor = .blue
fileprivate var backgroundColorHighlighted: UIColor = .white
override var isHighlighted: Bool {
willSet(newValue){
if newValue {
self.setTitleColor(titleColorHighlighted, for: state)
self.backgroundColor = backgroundColorHighlighted
}else {
self.setTitleColor(titleColorNormal, for: state)
self.backgroundColor = backgroundColorNormal
}
}
}
}
Either make image for the whole size, or make it stretchable, so it can fill the whole background:
extension UIButton {
func setBackgroundColor(color: UIColor, for state: UIControlState) {
let rect = CGRect(origin: CGPoint(x: 0, y:0), size: CGSize(width: 1, height: 1))
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(color.cgColor)
context.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let insets = UIEdgeInsetsMake(0, 0, 0, 0)
let stretchable = image!.resizableImage(withCapInsets: insets, resizingMode: .tile)
self.setBackgroundImage(stretchable, for: state)
}
}
I had the same problem with the mixing colors in highlighted state but didn't want to create a custom class. I found out that you can simply change the button type from "System" to "Custom". Then you can use the functions for setting colors by state. The colors will be displayed as defined.
You can change the button type in interface builder.

tableview cell has wrong background layer after reuse

I have a UIView extension which makes a gradient background from two colours. I use this so I add a nice background to my custom tableView cells. But after the reuse, the colour is always wrong (unlike the data inside which is correct). It's not like a plain background colour, and all colors depend on the value from the fetched data. After reuse, the background is always random from the previously generated cells (and their backgrounds).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath) as! TableViewCell
var imgTitle = ""
var color = UIColor()
let title = fetchedUV[indexPath.row].value
if title > 11 {
imgTitle = "flame"
color = UIColor.purple
cell.setGradientBackground(colorOne: .white, colorTwo: color)
} else if title >= 8 {
imgTitle = "sun-protection"
color = UIColor.red
cell.setGradientBackground(colorOne: .white, colorTwo: color)
} else if title >= 6 {
imgTitle = "sunbed"
color = UIColor.orange
cell.setGradientBackground(colorOne: .white, colorTwo: color)
} else if title >= 3 {
imgTitle = "sunglasses"
color = UIColor.yellow
cell.setGradientBackground(colorOne: .white, colorTwo: color)
} else {
imgTitle = "ok"
color = UIColor.green
cell.setGradientBackground(colorOne: .white, colorTwo: color)
}
colors.append(color)
let UVValue = String(describing: title)
cell.backgroundColor = UIColor.orange
cell.commonInit(imgTitle, title: UVValue, time: fetchedUV[indexPath.row].dateIso)
cell.logoImage.layer.cornerRadius = (cell.frame.height) / 2
cell.layer.cornerRadius = cell.frame.height/2
cell.layer.masksToBounds = true
//cell.setGradientBackground(colorOne: .white, colorTwo: color)
return cell
}
extension UIView {
func setGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.insertSublayer(gradientLayer, at: 0)
}
}
Just for an Example try looking at this
extension UIView
{
//This func will add gradient backgroung
//Just sample one
func setGradientBackground()
{
//Colors
let colorTop = UIColor(red: 255.0/255.0, green: 149.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
let colorBottom = UIColor(red: 255.0/255.0, green: 94.0/255.0, blue: 58.0/255.0, alpha: 1.0).cgColor
//Set Gradient layer
let gradientLayer = CAGradientLayer()
//Colors
gradientLayer.colors = [ colorTop, colorBottom]
//Locations
gradientLayer.locations = [ 0.0, 1.0]
//Here is the main Play
//Set Layer name so can be identified while Dequeuing cell
gradientLayer.name = "layerName"
//Set bounds
gradientLayer.frame = self.layer.bounds
//Insert Layer
self.layer.addSublayer(gradientLayer)
}
}
Now in CellForRowAt of TableView
//Setting cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
//Get all the sublayers of cell
for sublayer in cell.layer.sublayers!
{
//Check that sublayer is Already Added or not
if sublayer.name == "layerName"
{
//Sublayer already added
//Print already Added
print("Cell Deque again")
}
else
{
//Sublayer is not added yet
//Time to add Gradient Background
cell.setGradientBackground()
print("Layer Added")
}
}
//setting title
cell.textLabel?.text = items[indexPath.section][indexPath.row]
return cell
}
Hope it Helps , You can Name a layer that is being added as SubLayer and when Adding it to Cell as it gets Deque again and again so you must see that layer is not override on previous added layer
I had find and remove the previously added layer (the CAGradientLayer because it can remove any other layer as well). Do the checks and add another layer if its needed. I also changed the function so the layer now has a name. (thanks to iOS Geek for the suggestion)
for sublayer in cell.layer.sublayers! {
if let _ = sublayer as? CAGradientLayer {
if sublayer.name == name {
print("Cell deque again")
} else {
sublayer.removeFromSuperlayer()
cell.setGradientBackground0(colorOne: .white, colorTwo: color, name: name)
}
} else {
cell.setGradientBackground0(colorOne: .white, colorTwo: color, name: name)
}
}
extension UIView {
func setGradientBackground0(colorOne: UIColor, colorTwo: UIColor, name: String) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.name = name
layer.insertSublayer(gradientLayer, at: 0)
}
}
I'll leave this just in case.

Why does my UIButton's background layer animate in, and how can I stop it?

I have a custom UIButton subclass, and when I set the hidden property of the background layer it animates the alpha from 0 to 1. I want an instantaneous jump from 0 to 1. Why is it animating and how do I stop it from doing this?
Here's my code:
private var subLayer: CALayer!
override func awakeFromNib() {
super.awakeFromNib()
addTarget(self, action: "touchedDown", forControlEvents: .TouchDown)
addTarget(self, action: "canceledTouch", forControlEvents: .TouchUpInside | .TouchUpOutside | .TouchDragExit | .TouchCancel)
}
override func updateConstraints() {
super.updateConstraints()
// Add a background layer that is tight to the text in the button to indicate the button being pressed
subLayer = CALayer()
subLayer.frame = CGRectInset(bounds, -4.0, 5.0)
subLayer.backgroundColor = UIColor(red: 204/255.0, green: 232/255.0, blue: 253/255.0, alpha: 1.0).CGColor
subLayer.cornerRadius = 2.0
subLayer.hidden = true
layer.insertSublayer(subLayer, atIndex: 0)
}
func touchedDown() {
subLayer.hidden = false
}
func canceledTouch() {
subLayer.hidden = true
}
Everything done in Core Animation (that's the "CA" in "CALayer") will be animated unless you disable animation.
There are several ways to disable the default animations, but the only one that works in all situations is:
CATransaction.begin()
CATransaction.setDisableActions(true)
subLayer.hidden = false
CATransaction.commit()
Disable the automatic animation that layers have with CATransaction.setDisableActions(true)

Resources