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.
Related
I'm currently trying to add a background image to my navigation bar but the background image itself is not stretching to fill the bounds of the specified space (the pink button should be covering the blue square or atleast getting close to same size).
How do I get the background image to stretch/ fill the space?
Screenshot:
How I add the button:
let newsButton = UIButton(type: .custom)
newsButton.translatesAutoresizingMaskIntoConstraints = false
newsButton.backgroundColor = .blue
newsButton.setTitle(NSLocalizedString("News", comment: "News button"), for: .normal)
newsButton.layer.cornerRadius = 7
newsButton.titleLabel?.font = .systemFont(ofSize: 20)
newsButton.addTarget(self, action: #selector(onClick(_:)), for: .touchUpInside)
if let image = UIImage(named: "pink_button") {
newsButton.setBackgroundImage(image, for: .normal)
}
NSLayoutConstraint.activate([
newsButton.widthAnchor.constraint(equalToConstant: 128),
newsButton.heightAnchor.constraint(equalToConstant: 43)
])
navigationItem.titleView = newsButton
Your image has a lot of transparent space around the "red" shape.
If you replace it with this image (I trimmed out the alpha areas):
It will look like this:
As an alternative to using a "stretched image" you could use this custom view (that draws your shape) and embed the button as a subview:
class MyBottomShadowView: UIView {
var radius: CGFloat = 8
var primaryColor: UIColor = .red
var shadowColor: UIColor = .gray
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// background color needs to be .clear
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
var r: CGRect!
var pth: UIBezierPath!
// if rounded rect for "bottom shadow line"
// goes all the way to the top, we'll get
// anti-alias artifacts at the top corners
// so, we'll make it slightly smaller and
// move it down from the top
r = bounds.insetBy(dx: 0, dy: 2).offsetBy(dx: 0, dy: 2)
pth = UIBezierPath(roundedRect: r, cornerRadius: radius)
shadowColor.setFill()
pth.fill()
// "filled" rounded rect should be
// 2-points shorter than height
r = bounds
r.size.height -= 2.0
pth = UIBezierPath(roundedRect: r, cornerRadius: radius)
primaryColor.setFill()
pth.fill()
}
}
Your setup then becomes:
let newsButton = UIButton(type: .custom)
newsButton.translatesAutoresizingMaskIntoConstraints = false
newsButton.setTitleColor(.white, for: .normal)
newsButton.setTitleColor(.lightGray, for: .highlighted)
newsButton.setTitle(NSLocalizedString("News", comment: "News button"), for: .normal)
newsButton.titleLabel?.font = .systemFont(ofSize: 20)
// set button background to clear
newsButton.backgroundColor = .clear
newsButton.addTarget(self, action: #selector(onClick(_:)), for: .touchUpInside)
// create "bottom shadow view"
let shadView = MyBottomShadowView()
// set radius, primary and shadow colors as desired
shadView.radius = 12
shadView.primaryColor = UIColor(red: 1.00, green: 0.25, blue: 0.40, alpha: 1.0)
shadView.shadowColor = UIColor(red: 0.65, green: 0.20, blue: 0.30, alpha: 1.0)
shadView.translatesAutoresizingMaskIntoConstraints = false
shadView.addSubview(newsButton)
NSLayoutConstraint.activate([
shadView.widthAnchor.constraint(equalToConstant: 128),
shadView.heightAnchor.constraint(equalToConstant: 43),
newsButton.topAnchor.constraint(equalTo: shadView.topAnchor),
newsButton.leadingAnchor.constraint(equalTo: shadView.leadingAnchor),
newsButton.trailingAnchor.constraint(equalTo: shadView.trailingAnchor),
newsButton.bottomAnchor.constraint(equalTo: shadView.bottomAnchor),
])
navigationItem.titleView = shadView
And it looks like this:
I really thought it would be easy to set the background color of my UISearchBar's text field to white. But no matter what I try, it always stays offwhite / light gray (#efeff0).
import UIKit
class ViewController: UIViewController {
private let searchController = UISearchController()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Hello World"
view.backgroundColor = #colorLiteral(red: 0.9588784575, green: 0.9528519511, blue: 0.9350754619, alpha: 1)
searchController.searchBar.searchTextField.backgroundColor = .white
navigationItem.searchController = searchController
}
}
How can I make the search bar have a pure white background color? App is iOS 13+, if that helps.
Tiny test project: https://github.com/kevinrenskers/WhiteSearch.
It's possible. Set the background of the search field with a white image.
let size = CGSize(width: searchController.searchBar.frame.size.width - 12, height: searchController.searchBar.frame.size.height - 12)
let backgroundImage = createWhiteBG(size)!
let imageWithCorner = backgroundImage.createImageWithRoundBorder(cornerRadiuos: 10)!
searchController.searchBar.setSearchFieldBackgroundImage(imageWithCorner, for: UIControl.State.normal)
If you don't want to input an image to app. Try this for create one programmatically.
func createWhiteBG(_ frame : CGSize) -> UIImage? {
var rect = CGRect(x: 0, y: 0, width: 0, height: 0)
rect.size = frame
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.white.cgColor)
context?.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
extension UIImage {
func createImageWithRoundBorder(cornerRadiuos : CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.size, false, scale)
let rect = CGRect(origin:CGPoint(x: 0, y: 0), size: self.size)
let context = UIGraphicsGetCurrentContext()
let path = UIBezierPath(
roundedRect: rect,
cornerRadius: cornerRadiuos
)
context?.beginPath()
context?.addPath(path.cgPath)
context?.closePath()
context?.clip()
self.draw(at: CGPoint.zero)
context?.restoreGState()
path.lineWidth = 1.5
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Try this ... Change colors and images according to your preference
DispatchQueue.main.async {
searchBar.backgroundImage = UIImage()
for s in searchBar.subviews[0].subviews {
if s is UITextField {
s.layer.borderWidth = 1.0
s.layer.borderColor = UIColor.lightGray.cgColor
}
}
let searchTextField:UITextField = searchBar.subviews[0].subviews.last as? UITextField ?? UITextField()
searchTextField.layer.cornerRadius = 10
searchTextField.textAlignment = NSTextAlignment.left
let image:UIImage = UIImage(named: "search")!
let imageView:UIImageView = UIImageView.init(image: image)
searchTextField.leftView = nil
searchTextField.placeholder = "Search..."
searchTextField.font = UIFont.textFieldText
searchTextField.rightView = imageView
searchTextField.rightViewMode = UITextField.ViewMode.always
}
Here is My complete Custom Search Bar Which you can define the searchbar backgroundColor and TextField background Color
Tested
import Foundation
class SearchBar: UISearchBar {
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
makeUI()
}
private func makeUI( ) {
//SearchBar BackgroundColor
self.backgroundImage = UIImage(color: UIColor.white)
//Border Width
self.layer.borderWidth = 1
//Border Color
self.layer.borderColor = UIColor("DEDEDE")?.cgColor
//Corner Radius
self.layer.cornerRadius = 3
self.layer.masksToBounds = true
//Change Icon
self.setImage(UIImage(named: "search")?
.byResize(to: CGSize(width: 30, height: 30)), for: .search, state: .normal)
if let searchTextField = self.value(forKey: "searchField") as? UISearchTextField {
//TextField Background !!!!!
searchTextField.backgroundColor = UIColor.white
//TextField Font
searchTextField.font = UIFont(name: "Poppins-Regular", size: 21)
searchTextField.textColor = .black
}
}
}
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.
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.
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.