I want to change the thumb color same as the minimum track color. I need to extract color at the thumb location as it moves along the slider. I want my slider thumb to look something like this, and change color in accordance with the minimum track gradient color as it moves along the slider.
Following is my code for the gradient I created
func setSlider(slider:UISlider) {
let tgl = CAGradientLayer()
let frame = CGRect(x: 0.0, y: 0.0, width: slider.bounds.width, height: 10.0)
tgl.frame = frame
tgl.colors = [UIColor.black.cgColor, UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor]
tgl.borderWidth = 1.0
tgl.borderColor = UIColor.gray.cgColor
tgl.cornerRadius = 5.0
tgl.endPoint = CGPoint(x: 1.0, y: 1.0)
tgl.startPoint = CGPoint(x: 0.0, y: 1.0)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, false, 10.0)
tgl.render(in: UIGraphicsGetCurrentContext()!)
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
slider.setMaximumTrackImage(getBgImage(width: slider.bounds.width), for: .normal)
slider.setMinimumTrackImage(backgroundImage, for: .normal)
}
I tried to fetch color using the following code:
let color = sliderRating.minimumTrackImage(for: .normal)?.getPixelColor(point: CGPoint(x: Int(sender.value), y: 1))
func getPixelColor(point: CGPoint) -> UIColor? {
guard let cgImage = cgImage else { return nil }
let width = Int(size.width)
let height = Int(size.height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
else {
return nil
}
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
guard let pixelBuffer = context.data else { return nil }
let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)
let pixel = pointer[Int(point.y) * width + Int(point.x)]
let r: CGFloat = CGFloat(red(for: pixel)) / 255
let g: CGFloat = CGFloat(green(for: pixel)) / 255
let b: CGFloat = CGFloat(blue(for: pixel)) / 255
let a: CGFloat = CGFloat(alpha(for: pixel)) / 255
return UIColor(red: r, green: g, blue: b, alpha: a)
}
private func alpha(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 24) & 255)
}
private func red(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 16) & 255)
}
private func green(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 8) & 255)
}
private func blue(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 0) & 255)
}
private func rgba(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) -> UInt32 {
return (UInt32(alpha) << 24) | (UInt32(red) << 16) | (UInt32(green) << 8) | (UInt32(blue) << 0)
}
Here is a similar question I found on stack overflow for more reference:
How can I extract the uislider gradient color at the thumb position?
I tried extracting pixel color from the image but it gives me only white, gray, and darker gray shades but my track has colors ranging from black to green.
I'm getting output like this:
The issue is how you're determining the point for your color.
The code you show:
let color = sliderRating.minimumTrackImage(for: .normal)?.getPixelColor(point: CGPoint(x: Int(sender.value), y: 1))
is difficult to debug.
Let's split that up:
// safely unwrap the optional to make sure we get a valid image
if let minImg = sender.minimumTrackImage(for: .normal) {
let x = Int(sender.value)
let y = 1
let point = CGPoint(x: x, y: y)
// to debug this:
print("point:", point)
// safely unwrap the optional returned color
if let color = minImg.getPixelColor(pos: point) {
// do something with color
}
}
By default, a slider has values between 0.0 and 1.0. So as you drag the thumb, you'll see this output in the debug console:
// lots of these
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
// then when you final get all the way to the right
point: (1.0, 1.0)
As you see, you're not getting the point that you want on your image.
You don't mention it, but if did something like this:
sliderRating.minimumValue = 100
sliderRating.maximumValue = 120
Your x will range from 100 to 120. Again, not the point you want.
Instead of using the .value, you want to get the horizontal center of the thumb rect for x, and the vertical center of the image size for y.
Try it like this:
#objc func sliderRatingValueChanged(_ sender: UISlider) {
// get the slider's trackRect
let trackR = sender.trackRect(forBounds: sender.bounds)
// get the slider's thumbRect
let thumbR = sender.thumbRect(forBounds: sender.bounds, trackRect: trackR, value: sender.value)
// get the slider's minimumTrackImage
if let minImg = sender.minimumTrackImage(for: .normal) {
// we want point.x to be thumb rect's midX
// we want point.y to be 1/2 the height of the min track image
let point = CGPoint(x: thumbR.midX, y: minImg.size.height * 0.5)
// for debugging:
print("point:", point)
// get the color at point
if let color = minImg.getPixelColor(point: point) {
// set tint color of thumb
sender.thumbTintColor = color
}
}
// now do something with the slider's value?
let value = sender.value
}
I have this UIImageView where I am only changing the white color of the image. When the white color changes it doesn't change again because the white color is no longer white anymore. I want to access the new color and change it to a different color every time I press a button. Im using this func I found on github.
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
#IBAction func changeColors(_ sender: Any) {
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
var newColor = UIColor.init(red: randomRGB3, green: randomRGB2, blue: randomRGB, alpha: 1)
let changeColor = replaceColor(color: currentColor, withColor: newColor, image: mainImage.image!, tolerance: 0.5)
mainImage.image = changeColor
//change current color to new color
currentColor = newColor
}
extension ViewController {
func replaceColor(color: UIColor, withColor: UIColor, image: UIImage, tolerance: CGFloat) -> UIImage {
// This function expects to get source color(color which is supposed to be replaced)
// and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
assert(color.cgColor.numberOfComponents == 4 && withColor.cgColor.numberOfComponents == 4,
"Must be RGBA colorspace")
// Allocate bitmap in memory with the same width and size as source image
let imageRef = image.cgImage!
let width = imageRef.width
let height = imageRef.height
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width;
let bitsPerComponent = 8
let bitmapByteCount = bytesPerRow * height
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)
let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpace(name: CGColorSpace.genericRGBLinear)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
let rc = CGRect(x: 0, y: 0, width: width, height: height)
// Draw source image on created context
context!.draw(imageRef, in: rc)
// Get color components from replacement color
let withColorComponents = withColor.cgColor.components
let r2 = UInt8(withColorComponents![0] * 255)
let g2 = UInt8(withColorComponents![1] * 255)
let b2 = UInt8(withColorComponents![2] * 255)
let a2 = UInt8(withColorComponents![3] * 255)
// Prepare to iterate over image pixels
var byteIndex = 0
while byteIndex < bitmapByteCount {
// Get color of current pixel
let red = CGFloat(rawData[byteIndex + 0]) / 255
let green = CGFloat(rawData[byteIndex + 1]) / 255
let blue = CGFloat(rawData[byteIndex + 2]) / 255
let alpha = CGFloat(rawData[byteIndex + 3]) / 255
let currentColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
// Compare two colors using given tolerance value
if compareColor(color: color, withColor: currentColor , withTolerance: tolerance) {
// If the're 'similar', then replace pixel color with given target color
rawData[byteIndex + 0] = r2
rawData[byteIndex + 1] = g2
rawData[byteIndex + 2] = b2
rawData[byteIndex + 3] = a2
}
byteIndex = byteIndex + 4;
}
// Retrieve image from memory context
let imgref = context!.makeImage()
let result = UIImage(cgImage: imgref!)
// Clean up a bit
rawData.deallocate()
return result
}
func compareColor(color: UIColor, withColor: UIColor, withTolerance: CGFloat) -> Bool
{
var r1: CGFloat = 0.0, g1: CGFloat = 0.0, b1: CGFloat = 0.0, a1: CGFloat = 0.0;
var r2: CGFloat = 0.0, g2: CGFloat = 0.0, b2: CGFloat = 0.0, a2: CGFloat = 0.0;
color.getRed(&r1, green: &g1, blue: &b1, alpha: &a1);
withColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2);
return abs(r1 - r2) <= withTolerance &&
abs(g1 - g2) <= withTolerance &&
abs(b1 - b2) <= withTolerance &&
abs(a1 - a2) <= withTolerance;
}
}
Here are a few observations that I made which might be impacting the results you see:
As we discussed in the comments, if you want to start from the color which was changed previously, you need to hold on the color after the image has been updated beyond the scope of your function (you did this)
The next issue about ending up with one color probably has a lot to do with the fault tolerance
When you try to change a color in an image with 0.5 (50%) fault tolerance of a given color, you are changing a huge number of colors in an image in the first pass
If there were 100 colors in a color system, you are going to look for 50 of those colors in the image and change them to 1 specific color
In the first pass, you start with white. Lets say that 75% of the image has colors that are similar to white with a 50% fault tolerance - 75% of the image is going to change to that color
With such a high fault tolerance, soon enough one color will appear that will be close to most of the colors in the image with a 50% fault tolerance and you will end up with colors with 1 image
Some ideas to improve the results
Set a lower fault tolerance - you will see smaller changes and the same result could occur with 1 color but it will happen over a longer period of time
If you really want to randomize and no get this 1 color results, I suggest to change how you use the currentColor and make changes to the original image, not the updated image (I have this example below)
This will not impact the solution but better to handle optionals more safely as I see a lot of ! so I would recommend changing that
Perform the image processing in a background thread (also in the example below)
Here is an update with an example
class ImageColorVC: UIViewController
{
// UI Related
private var loaderController: UIAlertController!
let mainImage = UIImageView(image: UIImage(named: "art"))
// Save the current color and the original image
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
var originalImage: UIImage!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration, you can ignore
view.backgroundColor = .white
title = "Image Color"
configureBarButton()
configureImageView()
// Store the original image
originalImage = mainImage.image!
}
// MARK: AUTO LAYOUT
private func configureBarButton()
{
let barButton = UIBarButtonItem(barButtonSystemItem: .refresh,
target: self,
action: #selector(changeColors))
navigationItem.rightBarButtonItem = barButton
}
private func configureImageView()
{
mainImage.translatesAutoresizingMaskIntoConstraints = false
mainImage.contentMode = .scaleAspectFit
mainImage.clipsToBounds = true
view.addSubview(mainImage)
view.addConstraints([
mainImage.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
mainImage.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mainImage.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
mainImage.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
// Configures a loader to show while image is processing
private func configureLoaderController()
{
loaderController = UIAlertController(title: nil,
message: "Processing",
preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10,
y: 5,
width: 50,
height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.style = UIActivityIndicatorView.Style.medium
loadingIndicator.startAnimating();
loaderController.view.addSubview(loadingIndicator)
}
//MARK: FACTORY
// Similar to your function, only difference is that it uses
// the original image
private func performChangeColors()
{
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
let newColor = UIColor.init(red: randomRGB3,
green: randomRGB2,
blue: randomRGB,
alpha: 1)
// Do work in the back ground
DispatchQueue.global(qos: .background).async
{
let imageWithNewColor = self.replaceColor(color: self.currentColor,
withColor: newColor,
image: self.originalImage!,
tolerance: 0.5)
// Update the UI on the main thread
DispatchQueue.main.async
{
self.updateImageView(with: imageWithNewColor)
//change current color to new color
self.currentColor = newColor
}
}
}
#objc
private func changeColors()
{
// Configure a loader to show while image is processing
configureLoaderController()
present(loaderController, animated: true) { [weak self] in
self?.performChangeColors()
}
}
// Update the UI
private func updateImageView(with image: UIImage)
{
dismiss(animated: true) { [weak self] in
self?.mainImage.image = image
}
}
}
After starting with this:
About 50 tries later, it still seems to work well:
You can watch a longer video here to see a few more color changes that happen without leading to one single color
Hope this gives you enough to create the required workaround for your solution
I've created a function with variable number of parameteres. This is my function:
func setColors(colors: UIColor...) {
self.Colors = colors
}
Now when I pass some values to it:
setColors(UIColor.blackColor(), UIColor.redColor())
The black color will be white color in the function. This thing only happens to black color, Other colors work fine.
This thing is used in a class to generate gradients:
var Colors: [UIColor] = []
func setColors(colors: UIColor...) {
self.Colors = colors
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceRGB()
var colorComponent: [CGFloat] = []
var colorLocations: [CGFloat] = []
var i: CGFloat = 0.0;
for color in Colors {
let c = CGColorGetComponents(color.CGColor)
colorComponent.append(c[0])
colorComponent.append(c[1])
colorComponent.append(c[2])
colorComponent.append(c[3])
colorLocations.append(i)
i += CGFloat(1.0) / CGFloat(self.Colors.count)
}
let gradient: CGGradientRef = CGGradientCreateWithColorComponents(colorSpace, colorComponent, colorLocations, self.Colors.count)
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0.0, y: self.frame.size.height / 2), CGPoint(x: self.frame.size.width, y: self.frame.size.height / 2), 0)
}
The problem is that you are using CGColorGetComponents wrong:
let c = CGColorGetComponents(color.CGColor)
colorComponent.append(c[0])
colorComponent.append(c[1])
colorComponent.append(c[2])
colorComponent.append(c[3])
You're making a false assumption here.
That code happens to work if this color is in the UIDeviceRGBColorSpace, which has four components.
But it does not work if this color in the UIDeviceWhiteColorSpace, which has just two components - and black is in the UIDeviceWhiteColorSpace.
You could try something like this:
var colors: [UIColor] = []
func setColors(colors: UIColor...) {
self.colors = colors
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceRGB()
var gradientColors: [CGColor] = []
var colorLocations: [CGFloat] = []
var i: CGFloat = 0.0;
for color in colors {
gradientColors.append(color.CGColor)
colorLocations.append(i)
i += CGFloat(1.0) / CGFloat(colors.count)
}
let gradient: CGGradientRef = CGGradientCreateWithColors(colorSpace, gradientColors, colorLocations)
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0.0, y: self.frame.size.height / 2), CGPoint(x: self.frame.size.width, y: self.frame.size.height / 2), 0)
}
Side Note: You don't want to capitalize variable names. So I changed self.Colors to self.colors
hey, I want to be able to have a gradient fill on the text in a UILabel I know about CGGradient but i dont know how i would use it on a UILabel's text
i found this on google but i cant manage to get it to work
http://silverity.livejournal.com/26436.html
I was looking for a solution and DotSlashSlash has the answer hidden in one of the comments!
For the sake of completeness, the answer and the simplest solution is:
UIImage *myGradient = [UIImage imageNamed:#"textGradient.png"];
myLabel.textColor = [UIColor colorWithPatternImage:myGradient];
(Skip to bottom for full class source code)
Really useful answers by both Brad Larson and Bach. The second worked for me but it requires an image to be present in advance. I wanted something more dynamic so I combined both solutions into one:
draw the desired gradient on a UIImage
use the UIImage to set the color pattern
The result works and in the screenshot below you can see some Greek characters rendered fine too. (I have also added a stroke and a shadow on top of the gradient)
Here's the custom init method of my label along with the method that renders a gradient on a UIImage (part of the code for that functionality I got from a blog post I can not find now to reference it):
- (id)initWithFrame:(CGRect)frame text:(NSString *)aText {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.text = aText;
self.textColor = [UIColor colorWithPatternImage:[self gradientImage]];
}
return self;
}
- (UIImage *)gradientImage
{
CGSize textSize = [self.text sizeWithFont:self.font];
CGFloat width = textSize.width; // max 1024 due to Core Graphics limitations
CGFloat height = textSize.height; // max 1024 due to Core Graphics limitations
// create a new bitmap image context
UIGraphicsBeginImageContext(CGSizeMake(width, height));
// get context
CGContextRef context = UIGraphicsGetCurrentContext();
// push context to make it current (need to do this manually because we are not drawing in a UIView)
UIGraphicsPushContext(context);
//draw gradient
CGGradientRef glossGradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 0.0, 1.0, 1.0, 1.0, // Start color
1.0, 1.0, 0.0, 1.0 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGPoint topCenter = CGPointMake(0, 0);
CGPoint bottomCenter = CGPointMake(0, textSize.height);
CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, 0);
CGGradientRelease(glossGradient);
CGColorSpaceRelease(rgbColorspace);
// pop context
UIGraphicsPopContext();
// get a UIImage from the image context
UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();
// clean up drawing environment
UIGraphicsEndImageContext();
return gradientImage;
}
I'll try to complete that UILabel subclass and post it.
EDIT:
The class is done and it's on my GitHub repository. Read about it here!
Swift 4.1
class GradientLabel: UILabel {
var gradientColors: [CGColor] = []
override func drawText(in rect: CGRect) {
if let gradientColor = drawGradientColor(in: rect, colors: gradientColors) {
self.textColor = gradientColor
}
super.drawText(in: rect)
}
private func drawGradientColor(in rect: CGRect, colors: [CGColor]) -> UIColor? {
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.saveGState()
defer { currentContext?.restoreGState() }
let size = rect.size
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
colors: colors as CFArray,
locations: nil) else { return nil }
let context = UIGraphicsGetCurrentContext()
context?.drawLinearGradient(gradient,
start: CGPoint.zero,
end: CGPoint(x: size.width, y: 0),
options: [])
let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let image = gradientImage else { return nil }
return UIColor(patternImage: image)
}
}
Usage:
label.gradientColors = [UIColor.blue.cgColor, UIColor.red.cgColor]
SWIFT 3+
This solution is based on #Dimitris's answer. It is an extension on the UILabel class that will create a gradient over the label's text per your passed startColor and endColor. The UILabel extension is below:
extension UILabel {
func applyGradientWith(startColor: UIColor, endColor: UIColor) -> Bool {
var startColorRed:CGFloat = 0
var startColorGreen:CGFloat = 0
var startColorBlue:CGFloat = 0
var startAlpha:CGFloat = 0
if !startColor.getRed(&startColorRed, green: &startColorGreen, blue: &startColorBlue, alpha: &startAlpha) {
return false
}
var endColorRed:CGFloat = 0
var endColorGreen:CGFloat = 0
var endColorBlue:CGFloat = 0
var endAlpha:CGFloat = 0
if !endColor.getRed(&endColorRed, green: &endColorGreen, blue: &endColorBlue, alpha: &endAlpha) {
return false
}
let gradientText = self.text ?? ""
let name:String = NSFontAttributeName
let textSize: CGSize = gradientText.size(attributes: [name:self.font])
let width:CGFloat = textSize.width
let height:CGFloat = textSize.height
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
guard let context = UIGraphicsGetCurrentContext() else {
UIGraphicsEndImageContext()
return false
}
UIGraphicsPushContext(context)
let glossGradient:CGGradient?
let rgbColorspace:CGColorSpace?
let num_locations:size_t = 2
let locations:[CGFloat] = [ 0.0, 1.0 ]
let components:[CGFloat] = [startColorRed, startColorGreen, startColorBlue, startAlpha, endColorRed, endColorGreen, endColorBlue, endAlpha]
rgbColorspace = CGColorSpaceCreateDeviceRGB()
glossGradient = CGGradient(colorSpace: rgbColorspace!, colorComponents: components, locations: locations, count: num_locations)
let topCenter = CGPoint.zero
let bottomCenter = CGPoint(x: 0, y: textSize.height)
context.drawLinearGradient(glossGradient!, start: topCenter, end: bottomCenter, options: CGGradientDrawingOptions.drawsBeforeStartLocation)
UIGraphicsPopContext()
guard let gradientImage = UIGraphicsGetImageFromCurrentImageContext() else {
UIGraphicsEndImageContext()
return false
}
UIGraphicsEndImageContext()
self.textColor = UIColor(patternImage: gradientImage)
return true
}
}
And usage:
let text = "YAAASSSSS!"
label.text = text
if label.applyGradientWith(startColor: .red, endColor: .blue) {
print("Gradient applied!")
}
else {
print("Could not apply gradient")
label.textColor = .black
}
SWIFT 2
class func getGradientForText(text: NSString) -> UIImage {
let font:UIFont = UIFont(name: "YourFontName", size: 50.0)!
let name:String = NSFontAttributeName
let textSize: CGSize = text.sizeWithAttributes([name:font])
let width:CGFloat = textSize.width // max 1024 due to Core Graphics limitations
let height:CGFloat = textSize.height // max 1024 due to Core Graphics limitations
//create a new bitmap image context
UIGraphicsBeginImageContext(CGSizeMake(width, height))
// get context
let context = UIGraphicsGetCurrentContext()
// push context to make it current (need to do this manually because we are not drawing in a UIView)
UIGraphicsPushContext(context!)
//draw gradient
let glossGradient:CGGradientRef?
let rgbColorspace:CGColorSpaceRef?
let num_locations:size_t = 2
let locations:[CGFloat] = [ 0.0, 1.0 ]
let components:[CGFloat] = [(202 / 255.0), (197 / 255.0), (52 / 255.0), 1.0, // Start color
(253 / 255.0), (248 / 255.0), (101 / 255.0), 1.0] // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
let topCenter = CGPointMake(0, 0);
let bottomCenter = CGPointMake(0, textSize.height);
CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, CGGradientDrawingOptions.DrawsBeforeStartLocation);
// pop context
UIGraphicsPopContext();
// get a UIImage from the image context
let gradientImage = UIGraphicsGetImageFromCurrentImageContext();
// clean up drawing environment
UIGraphicsEndImageContext();
return gradientImage;
}
Props to #Dimitris
The example you provide relies on private text drawing functions that you don't have access to on the iPhone. The author provides an example of how to do this using public API in a subsequent post. His later example uses a gradient image for the color of the text. (Unfortunately, it appears his blog has since been removed, but see Bach's answer here for the approach he used.)
If you still want to draw the gradient for your text color in code, it can be done by subclassing UILabel and overriding -drawRect: to have code like the following within it:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGContextSelectFont(context, "Helvetica", 20.0f, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextClip);
CGContextSetTextPosition(context, 0.0f, round(20.0f / 4.0f));
CGContextShowText(context, [self.text UTF8String], strlen([self.text UTF8String]));
CGContextClip(context);
CGGradientRef gradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 1.0, 1.0, 1.0, // Start color
1.0, 1.0, 1.0, 0.1 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
CGGradientRelease(gradient);
CGColorSpaceRelease(rgbColorspace);
CGContextRestoreGState(context);
One shortcoming of this approach is that the Core Graphics functions I use don't handle Unicode text properly.
What the code does is it flips the drawing context vertically (the iPhone inverts the normal Quartz coordinate system on for the Y axis), sets the text drawing mode to intersect the drawn text with the clipping path, clips the area to draw to the text, and then draws a gradient. The gradient will only fill the text, not the background.
I tried using NSString's -drawAtPoint: method for this, which does support Unicode, but all the characters ran on top of one another when I switched the text mode to kCGTextClip.
Here's what I'm doing in Swift 3
override func viewDidLoad() {
super.viewDidLoad()
timerLabel.textColor = UIColor(patternImage: gradientImage(size: timerLabel.frame.size, color1: CIColor(color: UIColor.green), color2: CIColor(color: UIColor.red), direction: .Left))
}
func gradientImage(size: CGSize, color1: CIColor, color2: CIColor, direction: GradientDirection = .Up) -> UIImage {
let context = CIContext(options: nil)
let filter = CIFilter(name: "CILinearGradient")
var startVector: CIVector
var endVector: CIVector
filter!.setDefaults()
switch direction {
case .Up:
startVector = CIVector(x: size.width * 0.5, y: 0)
endVector = CIVector(x: size.width * 0.5, y: size.height)
case .Left:
startVector = CIVector(x: size.width, y: size.height * 0.5)
endVector = CIVector(x: 0, y: size.height * 0.5)
case .UpLeft:
startVector = CIVector(x: size.width, y: 0)
endVector = CIVector(x: 0, y: size.height)
case .UpRight:
startVector = CIVector(x: 0, y: 0)
endVector = CIVector(x: size.width, y: size.height)
}
filter!.setValue(startVector, forKey: "inputPoint0")
filter!.setValue(endVector, forKey: "inputPoint1")
filter!.setValue(color1, forKey: "inputColor0")
filter!.setValue(color2, forKey: "inputColor1")
let image = UIImage(cgImage: context.createCGImage(filter!.outputImage!, from: CGRect(x: 0, y: 0, width: size.width, height: size.height))!)
return image
}
There is a really simple solution for this! Here's how you add gradient colors to UILabel text.
We will achieve this in just two steps:
Create Gradient Image
Apply Gradient Image As textColor to UILabel
1.Create Gradient Image
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
Use this as follows:
let gradientImage = UIImage.gradientImageWithBounds(bounds: myLabel.bounds, colors: [firstColor.cgColor, secondColor.cgColor])
⠀
2.Apply Gradient Image As textColor to UILabel
myLabel.textColor = UIColor.init(patternImage: gradientImage)
⠀
Note:
If you want the gradient to be horizontal, just add these two lines to gradientLayer instance:
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
⠀
Note 2:
The UIImage extension function works with other UIViews too; not just UILabel! So feel free to use this method no matter which UIView you use to apply gradient color.
yourLabel.textColor = UIColor(patternImage: UIImage(named: "ur gradient image name ")!)
SwiftUI
Although we use Text in SwiftUI instead of UILabel, If you consider how to apply a gradient on a Text, you should apply it as a mask. But since gradients are stretchable, you can make a simple extension like this:
extension View {
func selfSizeMask<T: View>(_ mask: T) -> some View {
ZStack {
self.opacity(0)
mask.mask(self)
}.fixedSize()
}
}
Demo
And then you can assign any gradient or other type of view as a self-size mask like:
Text("Gradient is on FIRE !!!")
.selfSizeMask(
LinearGradient(
gradient: Gradient(colors: [.red, .yellow]),
startPoint: .bottom,
endPoint: .top)
)
This method contains some bonus advantages that you can see here in this answer
Simplest Swift 3 Solution
Add an image to your project assets or create one programmatically then do the following:
let image = UIImage(named: "myGradient.png")!
label.textColor = UIColor.init(patternImage: image)
You could sub-class out UILable and do the draw method yourself. That would probably be the more difficult approach, there might be an easier way.