iOS UIProgressView with gradient - ios

is it possible to create a custom ui progress view with a gradient from left to right?
I've tried it with the following code:
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.anchorPoint = CGPoint(x: 0, y: 0)
gradientLayer.position = CGPoint(x: 0, y: 0)
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0);
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0);
gradientLayer.colors = [
UIColor.red,
UIColor.green
]
// Convert to UIImage
self.layer.insertSublayer(gradientLayer, at: 0)
self.progressTintColor = UIColor.clear
self.trackTintColor = UIColor.black
But unfortunately the gradient is not visible. Any other ideas?

Looking at UIProgressView documentation, there's this property:
progressImage
If you provide a custom image, the progressTintColor property is ignored.
With that in mind, the laziest way to do this would be to create your gradient image and set it as the progressImage
I adapted this extension to make it a little cleaner, scaleable, and safer.
fileprivate extension UIImage {
static func gradientImage(with bounds: CGRect,
colors: [CGColor],
locations: [NSNumber]?) -> UIImage? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
// This makes it horizontal
gradientLayer.startPoint = CGPoint(x: 0.0,
y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0,
y: 0.5)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image`
}
}
Now that we've got a way to create a gradient image "on the fly", here's how to use it:
let gradientImage = UIImage.gradientImage(with: progressView.frame,
colors: [UIColor.red.cgColor, UIColor.green.cgColor],
locations: nil)
From there, you'd just set your progressView's progressImage, like so:
// I'm lazy...don't force unwrap this
progressView.progressImage = gradientImage!
progressView.setProgress(0.75, animated: true)

I had the same problem and solved it by creating a gradient custom view which I then convert to an image and assign it as the progress view track image.
I then flip the progress horizontally so that the progress bar becomes the background and the track image becomes the foreground.
This has the visual effect of revealing the gradient image underneath.
You just have to remember to invert your percentages which is really simple, see example buttons and code below:
SWIFT 3 Example:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
#IBAction func lessButton(_ sender: UIButton) {
let percentage = 20
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
#IBAction func moreButton(_ sender: UIButton) {
let percentage = 80
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//create gradient view the size of the progress view
let gradientView = GradientView(frame: progressView.bounds)
//convert gradient view to image , flip horizontally and assign as the track image
progressView.trackImage = UIImage(view: gradientView).withHorizontallyFlippedOrientation()
//invert the progress view
progressView.transform = CGAffineTransform(scaleX: -1.0, y: -1.0)
progressView.progressTintColor = UIColor.black
progressView.progress = 1
}
}
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
#IBDesignable
class GradientView: UIView {
private var gradientLayer = CAGradientLayer()
private var vertical: Bool = false
override func draw(_ rect: CGRect) {
super.draw(rect)
// Drawing code
//fill view with gradient layer
gradientLayer.frame = self.bounds
//style and insert layer if not already inserted
if gradientLayer.superlayer == nil {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = vertical ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0)
gradientLayer.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
}

George figured out a very clever method. If you want a more easy solution, open UIProgressView document, there is a property named progressImage.
so, i just make it work like this:
progressView.progressImage = UIImage(named: "your_gradient_progress_icon")
progressView.trackTintColor = UIColor.clear
after that:
progressView.setProgress(currentProgress, animated: true)

Related

UIView with gradient not changing colors when button is tapped

When a user clicks on a button, it randomizes the colors used to create a gradient in a large circle. Beneath it are two small circles that display the solid colors used for the gradient. They all display correctly at the start (main circle is a gradient of the randomized smaller circles' color) but when I click on the button, only the smaller circles change color; the large circle stays at the same gradient colors.
Extensions and View Controller:
extension UIView {
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
extension UIColor {
static var random: UIColor {
return UIColor(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1.0)
}
}
class GradientController: UIViewController {
let gradientView = GradientView()
let leftGradientColor: UIColor = .random
let rightGradientColor: UIColor = .random
}
override func viewDidLoad() {
super.viewDidLoad()
view = gradientView
newGradient()
}
func newGradient() {
gradientView.mainCircleView.setupGradientBackground(colorOne: leftGradientColor, colorTwo: rightGradientColor)
gradientView.colorCircleLeftView.backgroundColor = leftGradientColor
gradientView.colorCircleRightView.backgroundColor = rightGradientColor
gradientView.gradientGenerateButton.addTarget(self, action: #selector(randomGradient(sender:)), for: .touchUpInside)
}
#objc func randomGradient(sender: UIButton)
{
let leftGradient = UIColor.random
let rightGradient = UIColor.random
gradientView.colorCircleLeftView.backgroundColor = leftGradient
gradientView.colorCircleRightView.backgroundColor = rightGradient
//Here is where it's not changing colors. Doesn't seem like the VC recognizes it in this function
gradientView.mainCircleView.setupGradientBackground(colorOne: leftGradient, colorTwo: rightGradient)
}
View:
class GradientView: UIView {
//circle's UIView code in Extensions
let mainCircleView = UIView().circleView(width: 380, height: 380)
let colorCircleLeftView = UIView().circleView(width: 40, height: 40)
let colorCircleRightView = UIView().circleView(width: 40, height: 40)
...
func setupLayout() {
...
}
}
What I've tried is changing the mainCircleView color to solid UIColors, like gradientView.mainCircleView.setupGradientBackground(colorOne: .red, colorTwo: .orange) to see if the main Circle changes to those colors in both func newGradient() and #objc func randomGradient(sender: UIButton). It only changes in func newGradient() what I've set manually, so that means the VC isn't recognizing the main Circle in the #objc func but I'm lost on how to fix it...
Any help is appreciated!
What it looks like when I click the "Generate" button (large circle should be showing brown and purple):
Update your function with this, You have to remove old layer and then insert new Sublayer.
Solution 1 :
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
if let gradientLayer = (self.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
gradientLayer.removeFromSuperlayer()
}
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
Solution 2 :
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer = layer.sublayers?.first as? CAGradientLayer ?? CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
guard gradientLayer.superlayer != self else {
return
}
layer.insertSublayer(gradientLayer, at: 0)
}
Solution 3 :
you can set name to your CAGradientLayer, This will help you for removing that particular layer.
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
for layer in layer.sublayers ?? [] {
if layer.name == "GradientLayer" {
layer.removeFromSuperlayer()
}
}
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.name = "GradientLayer"
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
The gradient does not change because you add a new gradient below the existing one
extension UIView {
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer: CAGradientLayer = CAGradientLayer()
...
// ⬇ There is a mistake.
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
You should change the current gradient with this new one. Or insert a new one above the old one.
Changing the gradient layer colors after setting it
extension CALayer {
func updateGradientColors(_ colors:CGColor...) {
if let layer = sublayers?.first as? CAGradientLayer {
let frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height))
layer.frame = frame
layer.colors = colors
}
}
}

Creating a clear button with gradient border and gradient text

I'm trying to add a gradient to my UIButton Title and to the border of the button. I've gone through most of the solution on here which I cannot get working for the life of me, might be outdated, I'm not sure. So currently I extend the UIView in order to set the gradient of whatever. So how would I add another function for this feature?
func setGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientlayer = CAGradientLayer()
gradientlayer.frame = bounds
gradientlayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientlayer.locations = [0, 1]
gradientlayer.startPoint = CGPoint(x: 1.0, y: 0.0)
gradientlayer.endPoint = CGPoint(x: 0.0, y: 0.0)
layer.insertSublayer(gradientlayer, at: 0)
}
I have created a demo for you, you can do this with the help of CAGradientLayer see the following output and code for this.
Storyboard:
For gradient button text color and border put your UIButton inside UIView, then assign CAGradientLayer to UIview.
Note:- Don't forget to set the button as the views mask, See the following code.
import UIKit
class ViewController: UIViewController {
#IBOutlet var viewForButton: UIView!
#IBOutlet var myButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Create a gradient layer
let gradient = CAGradientLayer()
// gradient colors in order which they will visually appear
gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
// Gradient from left to right
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
// set the gradient layer to the same size as the view
gradient.frame = viewForButton.bounds
// add the gradient layer to the views layer for rendering
viewForButton.layer.insertSublayer(gradient, at: 0)
// Tha magic! Set the button as the views mask
viewForButton.mask = myButton
//Set corner Radius and border Width of button
myButton.layer.cornerRadius = myButton.frame.size.height / 2
myButton.layer.borderWidth = 5.0
}
}
Extension: You can also prefer this extension for the same.
extension UIView{
func gradientButton(_ buttonText:String, startColor:UIColor, endColor:UIColor) {
let button:UIButton = UIButton(frame: self.bounds)
button.setTitle(buttonText, for: .normal)
let gradient = CAGradientLayer()
gradient.colors = [startColor.cgColor, endColor.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = self.bounds
self.layer.insertSublayer(gradient, at: 0)
self.mask = button
button.layer.cornerRadius = button.frame.size.height / 2
button.layer.borderWidth = 5.0
}
}
How to use:
testView.gradientButton("Hello", startColor: .red, endColor: .blue)
You just need to add below UIView extension and call the function to get desire gradient button,
func covertToGradientButtonWith(title: String, radius: CGFloat, borderWidth: CGFloat, gradientStartColor: UIColor, gradientEndColor: UIColor) {
let button:UIButton = UIButton(frame: self.bounds)
button.setTitle(title, for: .normal)
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = self.bounds
self.layer.insertSublayer(gradient, at: 0)
self.mask = button
button.layer.cornerRadius = radius
button.layer.borderWidth = borderWidth
}
Hope, this solution may help you.

How to add gradient layer which fills the view frame exactly

In my app i'm adding the gradient layer to UIView and UIToolBar but it doesn't fill the views exactly
let gradient:CAGradientLayer = CAGradientLayer()
gradient.frame = self.vw_gradientForToolBar.bounds
gradient.colors = [hexStringToUIColor(hex: "#5d8f32").cgColor,hexStringToUIColor(hex: "#04667f").cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 0)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0.0)
let img : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
self.toolBar.setBackgroundImage(img, forToolbarPosition: .any, barMetrics: .default)
vw_gradientForToolBar.layer.addSublayer(gradient)
View Hirarchy
enter image description here
It's a little tough to tell exactly what you have going on, based on the images you posted, however... This may simplify things for you.
First, keep in mind that Layers do not auto-scale, so when your tool bar changes size (different devices, device rotation, etc), you want your gradient layer to also resize. Best way to do that is to use a UIView subclass and override layoutSubviews().
So, add this class to your code:
class GradientView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
let gradientLayer = layer as! CAGradientLayer
gradient.colors = [hexStringToUIColor(hex: "#5d8f32").cgColor,hexStringToUIColor(hex: "#04667f").cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
}
}
Then in your controller's viewDidLoad() function:
override func viewDidLoad() {
super.viewDidLoad()
let vwGrad = GradientView()
vwGrad.frame = toolBar.frame
vwGrad.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.toolBar.insertSubview(vwGrad, at: 0)
}
Note: you would no longer need your vw_gradientForToolBar (which, I'm assuming, is a UIView connected via #IBOutlet).

How do I set a background gradient to a UINavigationBar with iOS 11 and prefersLargeTitles?

In iOS 10, I could do this to make a background gradient:
let gradientColor = UIColor.gradientWithFrame(frame: navigationBar.bounds, colors: [.red, .blue])
navigationBar.barTintColor = gradientColor
Now, navigationBar.bounds returns the size of the UINavigationBar when it doesn't have large titles. This is apparent in this screenshot with the gradient repeating:
You can see that the gradient starts to repeat because the size returned by navigationBar.size returns the incorrect size.
Is there another way to set a gradient on UINavigationBar?
Try this:
let gradientLayer = CAGradientLayer()
var updatedFrame = self.navigationController!.navigationBar.bounds
updatedFrame.size.height += view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
gradientLayer.frame = updatedFrame
gradientLayer.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.navigationController!.navigationBar.setBackgroundImage(image, for: UIBarMetrics.default)
let appearance = navigationController!.navigationBar.standardAppearance.copy()
appearance.backgroundImage = image
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
You need to account for the status bar height which is 20 on my 6s device you can use UIApplication.shared.statusBarFrame.height and simply add this height to the frame.
also, I am setting the gradient by creating a UIImage from a CAGradientLayer so I could use the UIColor(patternImage:) method.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.prefersLargeTitles = true
self.title = "Feed me Seymour"
if let navFrame = self.navigationController?.navigationBar.frame {
//HERE
//Create a new frame with the default offset of the status bar
let newframe = CGRect(origin: .zero, size: CGSize(width: navFrame.width, height: (navFrame.height + UIApplication.shared.statusBarFrame.height) ))
let image = gradientWithFrametoImage(frame: newframe, colors: [UIColor.red.cgColor, UIColor.blue.cgColor])!
self.navigationController?.navigationBar.barTintColor = UIColor(patternImage: image)
}
}
func gradientWithFrametoImage(frame: CGRect, colors: [CGColor]) -> UIImage? {
let gradient: CAGradientLayer = CAGradientLayer(layer: self.view.layer)
gradient.frame = frame
gradient.colors = colors
UIGraphicsBeginImageContext(frame.size)
gradient.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
I would set the navigation bar tint to "clear colour" and add the gradient to the background view.
Simply add extension to UIColor
extension UIColor {
static func gradientColor(startColor: UIColor, endColor: UIColor, frame: CGRect) -> UIColor? {
let gradient = CAGradientLayer(layer: frame.size)
gradient.frame = frame
gradient.colors = [startColor.cgColor, endColor.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 1.0)
gradient.endPoint = CGPoint(x: 1.0, y: 1.0)
UIGraphicsBeginImageContext(frame.size)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
gradient.render(in: context)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return UIColor(patternImage: image)
}
}
execute:
if let navFrame = self.navigationController?.navigationBar.frame {
let newframe = CGRect(origin: .zero, size: CGSize(width: navFrame.width, height: (navFrame.height + UIApplication.shared.statusBarFrame.height) ))
self.navigationController?.navigationBar.barTintColor = UIColor.gradientColor(startColor: .systemPink, endColor: .orange, frame: newframe)
}
and combine with start/end point to add gradient horizontal/vertical

UINavigationBar Title and Buttons disappear after gradient is applied on iOS 11 Swift 4

I have recently upgraded to Xcode 9 with Swift 4 which has in turn updated all the iOS emulators.
Since the update, I have found that after I apply a gradient to the UINavigationBar; the title and buttons do not appear, yet still have functionality.
Here is the code to applying my gradient -
extension UIView {
func applyNavGradient(colours: [UIColor]) -> Void {
self.applyNavGradient(colours: colours, locations: nil)
}
func applyNavGradient(colours: [UIColor], locations: [NSNumber]?) -> Void {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.locations = locations
self.layer.addSublayer(gradient)
self.backgroundColor = UIColor.clear
}
}
class ViewController {
override func viewDidLoad() {
UINavigationBar.applyNavGradient(colours: redFade)
}
This used to work in Xcode 8 with iOS10 -
Now all I get is this;
Can anyone give me some clue as to why this has changed??
Thank you in advance!
Chris.
You can use UIColor.init(patternImage image: UIImage) method to assign barTintColor of navigationBar
Here is the sample code
func applyNavGradient(colours: [UIColor]) -> UIImage? {
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: navigationController!.navigationBar.frame.size.height + navigationController!.navigationBar.frame.origin.y)
gradientLayer.colors = colours.map { $0.cgColor }
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
After that you can call like this to set gradient
if let img = applyNavGradient(colours: [UIColor.red, UIColor.yellow]) {
navigationController?.navigationBar.barTintColor = UIColor(patternImage: img)
}

Resources