Reputation: 3706
I have tried to search for solutions for this problem, but I am not even able to put it correctly in words I guess.
Basically I have a bar that gets filled up with a color while an operation proceeds. I have a label with the progress percentage that has the same color has the fill color, so I need it to change when the fill color is on the back. Something like this:
Is it possible in anyway to achieve this result? And in case, how?
Upvotes: 23
Views: 3644
Reputation: 9395
Swift 4 version of Morten's answer:
class ProgressLabel: UIView {
var progressBarColor = UIColor(red:108.0/255.0, green:200.0/255.0, blue:226.0/255.0, alpha:1.0)
var textColor = UIColor.white
var font = UIFont.boldSystemFont(ofSize: 42)
var progress: Float = 0 {
didSet {
progress = Float.minimum(100.0, Float.maximum(progress, 0.0))
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Set up environment.
let size = self.bounds.size
// Prepare progress as a string.
let progressMessage = NSString(format:"%d %%", Int(progress))
var attributes: [NSAttributedStringKey:Any] = [ NSAttributedStringKey.font : font ]
let textSize = progressMessage.size(withAttributes: attributes)
let progressX = ceil(CGFloat(progress) / 100 * size.width)
let textPoint = CGPoint(x: ceil((size.width - textSize.width) / 2.0), y: ceil((size.height - textSize.height) / 2.0))
// Draw background + foreground text
progressBarColor.setFill()
context.fill(self.bounds)
attributes[NSAttributedStringKey.foregroundColor] = textColor
progressMessage.draw(at: textPoint, withAttributes: attributes)
// Clip the drawing that follows to the remaining progress' frame.
context.saveGState()
let remainingProgressRect = CGRect(x: progressX, y: 0.0, width: size.width - progressX, height: size.height)
context.addRect(remainingProgressRect)
context.clip()
// Draw again with inverted colors.
textColor.setFill()
context.fill(self.bounds)
attributes[NSAttributedStringKey.foregroundColor] = progressBarColor
progressMessage.draw(at: textPoint, withAttributes: attributes)
context.restoreGState()
}
}
Upvotes: 2
Reputation: 745
The best and the cleanest way to do that is to use masks. The view hierarchy of your progress bar should look like this:
Subview at index #1
View showing progressSubview at index #2
UILabel
with text color the same as baseSubview at index #3
UILabel
with text color the same as the view showing progressWe will be applying mask to #3
. We start applying mask as soon as the view showing progress reaches the frame of #3
. The frame of the mask has to follow the progress. When #3
is gradually masked, the label underneath reveals and you achieve the effect nicely and with a little effort.
Here you are how to create masks with CALayers.
Upvotes: 0
Reputation: 6320
The easiest way is to create a UIView
subclass that has a progress
property and overwrites -drawRect:
.
All the code you need is this:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// Set up environment.
CGSize size = [self bounds].size;
UIColor *backgroundColor = [UIColor colorWithRed:108.0/255.0 green:200.0/255.0 blue:226.0/255.0 alpha:1.0];
UIColor *foregroundColor = [UIColor whiteColor];
UIFont *font = [UIFont boldSystemFontOfSize:42.0];
// Prepare progress as a string.
NSString *progress = [NSString stringWithFormat:@"%d%%", (int)round([self progress] * 100)];
NSMutableDictionary *attributes = [@{ NSFontAttributeName : font } mutableCopy];
CGSize textSize = [progress sizeWithAttributes:attributes];
CGFloat progressX = ceil([self progress] * size.width);
CGPoint textPoint = CGPointMake(ceil((size.width - textSize.width) / 2.0), ceil((size.height - textSize.height) / 2.0));
// Draw background + foreground text
[backgroundColor setFill];
CGContextFillRect(context, [self bounds]);
attributes[NSForegroundColorAttributeName] = foregroundColor;
[progress drawAtPoint:textPoint withAttributes:attributes];
// Clip the drawing that follows to the remaining progress' frame.
CGContextSaveGState(context);
CGRect remainingProgressRect = CGRectMake(progressX, 0.0, size.width - progressX, size.height);
CGContextAddRect(context, remainingProgressRect);
CGContextClip(context);
// Draw again with inverted colors.
[foregroundColor setFill];
CGContextFillRect(context, [self bounds]);
attributes[NSForegroundColorAttributeName] = backgroundColor;
[progress drawAtPoint:textPoint withAttributes:attributes];
CGContextRestoreGState(context);
}
- (void)setProgress:(CGFloat)progress {
_progress = fminf(1.0, fmaxf(progress, 0.0));
[self setNeedsDisplay];
}
You can expand the class as needed with properties for background color, text color, font, etc.
Upvotes: 9
Reputation: 3928
Two UIViews.
Let's call one the background and the other the progressBar. progressBar is stacked on top of background with the same origin on their common superview.
They both have a UILabel as subview, and both labels at the same origin relative to their parent. background has a dark backgroundColor and it's label has light textColor and the progress view has things the other way around.
progressBar has a narrower frame width than background and has clipsToBounds==YES
The trick is, with the views' origins the same and the labels' origins the same, and clipsToBounds on the top view, everything is going to look right.
Drop those two views into a new UIView subclass called ReallyCoolProgressView, and give it one public method:
-(void)setProgress:(float)progress
progress is a number from 0.0 to 1.0. The method scales the progressBar width and sets both label's text @"Progress %f", progress*100
Upvotes: 0
Reputation: 126
To make it easier without overriding drawRect:
, you can create 2 UIViews to work around that.
Blue background UIView (A) contains white UILabel. (Clipping subviews turns on)
White background UIView (B) contains blue UILabel.
A will overlay on B.
Note: 2 UILabels will have the same sizes, same fonts and same positions.
By adjusting width of UIView A, you will make the process bar works as you wish.
Upvotes: 1
Reputation: 1718
Just an idea of "faking" the effect you want. You may create a subclass of UIView with one background label (white and blue text) and a subview in front of it with blue background color and white text. You may animate after the width of the front label from 0 to 100% on the background label. You may have to check if you need to pu the label inside a subview to avoid displacement of the text while increasing width.
Upvotes: 2