Circular UIView with multiple coloured border working like a pie chart - objective-c

I am trying to set a circular avatar of a player of a game with a piechart representation on the avatar's circular border.
Player 1 -
Wins 25%
Lost 70%
Drawn 5%
cell.selectedPhoto.frame = CGRectMake(cell.selectedPhoto.frame.origin.x, cell.selectedPhoto.frame.origin.y, 75, 75);
cell.selectedPhoto.clipsToBounds = YES;
cell.selectedPhoto.layer.cornerRadius = 75/2.0f;
cell.selectedPhoto.layer.borderColor=[UIColor orangeColor].CGColor;
cell.selectedPhoto.layer.borderWidth=2.5f;
cell.selectedBadge.layer.cornerRadius = 15;
I have the UIImageView as a circle already with a single border colour.
At first guess perhaps I will need to clear the border of my UIImageView and have instead a UIView sitting behind my UIImageView that is a standard piechart, but is there a smarter way of doing this?
Thank you in advance.

I would recommend you create a custom UIView subclass for this, that manages various CALayer objects to create this effect. I was going to set about doing this in Core Graphics, but if you ever want to add some nice animations to this, you'll want to stick with Core Animation.
So let's first define our interface.
/// Provides a simple interface for creating an avatar icon, with a pie-chart style border.
#interface AvatarView : UIView
/// The avatar image, to be displayed in the center.
#property (nonatomic) UIImage* avatarImage;
/// An array of float values to define the values of each portion of the border.
#property (nonatomic) NSArray* borderValues;
/// An array of UIColors to define the colors of the border portions.
#property (nonatomic) NSArray* borderColors;
/// The width of the outer border.
#property (nonatomic) CGFloat borderWidth;
/// Animates the border values from their current values to a new set of values.
-(void) animateToBorderValues:(NSArray*)borderValues duration:(CGFloat)duration;
#end
Here we can set the avatar image, border width, and provide an array of colors and values. Next, lets work on implementing this. First we'll want to define some variables that we'll want to keep track of.
#implementation AvatarView {
CALayer* avatarImageLayer; // the avatar image layer
NSMutableArray* borderLayers; // the array containing the portion border layers
UIBezierPath* borderLayerPath; // the path used to stroke the border layers
CGFloat radius; // the radius of the view
}
Next, lets setup our avatarImageLayer, as well as a couple other variables in the initWithFrame method:
-(instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
radius = frame.size.width*0.5;
// create border layer array
borderLayers = [NSMutableArray array];
// create avatar image layer
avatarImageLayer = [CALayer layer];
avatarImageLayer.frame = frame;
avatarImageLayer.contentsScale = [UIScreen mainScreen].nativeScale; // scales the layer to the screen scale
[self.layer addSublayer:avatarImageLayer];
}
return self;
}
Next let's define our method that will populate the border layers when the borderValues property updates, allowing the view to have a dynamic number of border layers.
-(void) populateBorderLayers {
while (borderLayers.count > _borderValues.count) { // remove layers if the number of border layers got reduced
[(CAShapeLayer*)[borderLayers lastObject] removeFromSuperlayer];
[borderLayers removeLastObject];
}
NSUInteger colorCount = _borderColors.count;
NSUInteger borderLayerCount = borderLayers.count;
while (borderLayerCount < _borderValues.count) { // add layers if the number of border layers got increased
CAShapeLayer* borderLayer = [CAShapeLayer layer];
borderLayer.path = borderLayerPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = _borderWidth;
borderLayer.strokeColor = (borderLayerCount < colorCount)? ((UIColor*)_borderColors[borderLayerCount]).CGColor : [UIColor clearColor].CGColor;
if (borderLayerCount != 0) { // set pre-animation border stroke positions.
CAShapeLayer* previousLayer = borderLayers[borderLayerCount-1];
borderLayer.strokeStart = previousLayer.strokeEnd;
borderLayer.strokeEnd = previousLayer.strokeEnd;
} else borderLayer.strokeEnd = 0.0; // default value for first layer.
[self.layer insertSublayer:borderLayer atIndex:0]; // not strictly necessary, should work fine with `addSublayer`, but nice to have to ensure the layers don't unexpectedly overlap.
[borderLayers addObject:borderLayer];
borderLayerCount++;
}
}
Next, we want to make a method that can update the layer's stroke start and end values when borderValues gets updated. This could be merged into previous method, but if you want to setup animation you'll want to keep it separate.
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
Next, we just need to override the setters in order to update certain aspects of the border and avatar image when the values change:
-(void) setAvatarImage:(UIImage *)avatarImage {
_avatarImage = avatarImage;
avatarImageLayer.contents = (id)avatarImage.CGImage; // update contents if image changed
}
-(void) setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
CGFloat halfBorderWidth = borderWidth*0.5; // we're gonna use this a bunch, so might as well pre-calculate
// set the new border layer path
borderLayerPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius, radius} radius:radius-halfBorderWidth startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
for (CAShapeLayer* s in borderLayers) { // apply the new border layer path
s.path = borderLayerPath.CGPath;
s.lineWidth = borderWidth;
}
// update avatar masking
CAShapeLayer* s = [CAShapeLayer layer];
avatarImageLayer.frame = CGRectMake(halfBorderWidth, halfBorderWidth, self.frame.size.width-borderWidth, self.frame.size.height-borderWidth); // update avatar image frame
s.path = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius-halfBorderWidth, radius-halfBorderWidth} radius:radius-borderWidth startAngle:0 endAngle:M_PI*2.0 clockwise:YES].CGPath;
avatarImageLayer.mask = s;
}
-(void) setBorderColors:(NSArray *)borderColors {
_borderColors = borderColors;
NSUInteger i = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeColor = ((UIColor*)borderColors[i]).CGColor;
i++;
}
}
-(void) setBorderValues:(NSArray *)borderValues {
_borderValues = borderValues;
[self populateBorderLayers];
[self updateBorderStrokeValues];
}
Finally, we can even take one step further by animating the layers! Let's just add a single of method that can handle this for us.
-(void) animateToBorderValues:(NSArray *)borderValues duration:(CGFloat)duration {
_borderValues = borderValues; // update border values
[self populateBorderLayers]; // do a 'soft' layer update, making sure that the correct number of layers are generated pre-animation. Pre-sets stroke positions to a pre-animation state.
// define stroke animation
CABasicAnimation* strokeAnim = [CABasicAnimation animation];
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokeAnim.duration = duration;
CGFloat cumulativeValue = 0;
for (int i = 0; i < borderLayers.count; i++) {
cumulativeValue += [borderValues[i] floatValue];
CAShapeLayer* s = borderLayers[i];
if (i != 0) [s addAnimation:strokeAnim forKey:#"startStrokeAnim"];
// define stroke end animation
strokeAnim.keyPath = #"strokeEnd";
strokeAnim.fromValue = #(s.strokeEnd);
strokeAnim.toValue = #(cumulativeValue);
[s addAnimation:strokeAnim forKey:#"endStrokeAnim"];
strokeAnim.keyPath = #"strokeStart"; // re-use the previous animation, as the values are the same (in the next iteration).
}
// update presentation layer values
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self updateBorderStrokeValues]; // sets stroke positions.
[CATransaction commit];
}
And that's it! Here's an example of the usage:
AvatarView* v = [[AvatarView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
v.avatarImage = [UIImage imageNamed:#"photo.png"];
v.borderWidth = 10;
v.borderColors = #[[UIColor colorWithRed:122.0/255.0 green:108.0/255.0 blue:255.0/255.0 alpha:1],
[UIColor colorWithRed:100.0/255.0 green:241.0/255.0 blue:183.0/255.0 alpha:1],
[UIColor colorWithRed:0 green:222.0/255.0 blue:255.0/255.0 alpha:1]];
// because the border values default to 0, you can add this without even setting the border values initially!
[v animateToBorderValues:#[#(0.4), #(0.35), #(0.25)] duration:2];
Results
Full project: https://github.com/hamishknight/Pie-Chart-Avatar

Actually you can directly create your own layer from CALayer. here is a sample Animation layer from my own project.
AnimationLayer.h
#import <QuartzCore/QuartzCore.h>
#interface AnimationLayer : CALayer
#property (nonatomic,assign ) float percent;
#property (nonatomic, strong) NSArray *percentValues;
#property (nonatomic, strong) NSArray *percentColours;
#end
percentValues are your values for which part is gotten.
it should be #[#(35),#(75),#(100)] for win ratio:%35, loose:%40 and draw:%25.
percentColors are UIColor objects for win, loose and draw.
in `AnimationLayer.m`
#import "AnimationLayer.h"
#import <UIKit/UIKit.h>
#implementation AnimationLayer
#dynamic percent,percentValues,percentColours;
+ (BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:#"percent"]){
return YES;
}else
return [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)ctx
{
CGFloat arcStep = (M_PI *2) / 100 * (1.0-self.percent); // M_PI*2 is equivalent of full cirle
BOOL clockwise = NO;
CGFloat x = CGRectGetWidth(self.bounds) / 2; // circle's center
CGFloat y = CGRectGetHeight(self.bounds) / 2; // circle's center
CGFloat radius = MIN(x, y);
UIGraphicsPushContext(ctx);
// draw colorful circle
CGContextSetLineWidth(ctx, 12);//12 is the width of circle.
CGFloat toDraw = (1-self.percent)*100.0f;
for (CGFloat i = 0; i < toDraw; i++)
{
UIColor *c;
for (int j = 0; j<[self.percentValues count]; j++)
{
if (i <= [self.percentValues[j] intValue]) {
c = self.percentColours[j];
break;
}
}
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep+0.02;
CGContextAddArc(ctx, x, y, radius-6, startAngle, endAngle, clockwise);//set the radius as radius-(half of your line width.)
CGContextStrokePath(ctx);
}
UIGraphicsPopContext();
}
#end
and in some place where you will use this effect, you should call this like
+(void)addAnimationLayerToView:(UIView *)imageOfPlayer withColors:(NSArray *)colors andValues:(NSArray *)values
{
AnimationLayer *animLayer = [AnimationLayer layer];
animLayer.frame = imageOfPlayer.bounds;
animLayer.percentColours = colors;
animLayer.percentValues = values;
[imageOfPlayer.layer insertSublayer:animLayer atIndex:0];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percent"];
[animation setFromValue:#1];
[animation setToValue:#0];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setDuration:6];
[animLayer addAnimation:animation forKey:#"imageAnimation"];
}

Related

Method to resize CALayer frame on window resize?

I draw a series of images to various CALayer sublayers, then add those sublayers to a superlayer:
- (void)renderImagesFromArray:(NSArray *)array {
CALayer *superLayer = [CALayer layer];
for (id object in array) {
CALayer* subLayer = [CALayer layer];
// Disregard...
NSURL *path = [NSURL fileURLWithPathComponents:#[NSHomeDirectory(), #"Desktop", object]];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:path];
[self positionImage:image layer:subLayer];
subLayer.contents = image;
subLayer.hidden = YES;
[superLayer addSublayer:subLayer];
}
[self.view setLayer:superLayer];
[self.view setWantsLayer:YES];
// Show top layer
CALayer *top = superLayer.sublayers[0];
top.hidden = NO;
}
I then call [self positionImage: layer:] to stretch the CALayer to it's maximum bounds (essentially using the algorithm for the CSS cover property), and position it in the center of the window:
- (void)positionImage:(NSImage *)image layer:(CALayer *)layer{
float imageWidth = image.size.width;
float imageHeight = image.size.height;
float frameWidth = self.view.frame.size.width;
float frameHeight = self.view.frame.size.height;
float aspectRatioFrame = frameWidth/frameHeight;
float aspectRatioImage = imageWidth/imageHeight;
float computedImageWidth;
float computedImageHeight;
float verticalSpace;
float horizontalSpace;
if (aspectRatioImage <= aspectRatioFrame){
computedImageWidth = frameHeight * aspectRatioImage;
computedImageHeight = frameHeight;
verticalSpace = 0;
horizontalSpace = (frameWidth - computedImageWidth)/2;
} else {
computedImageWidth = frameWidth;
computedImageHeight = frameWidth / aspectRatioImage;
horizontalSpace = 0;
verticalSpace = (frameHeight - computedImageHeight)/2;
}
[CATransaction flush];
[CATransaction begin];
CATransaction.disableActions = YES;
layer.frame = CGRectMake(horizontalSpace, verticalSpace, computedImageWidth, computedImageHeight);
[CATransaction commit];
}
This all works fine, except when the window gets resized. I solved this (in a very ugly way) by subclassing NSView, then implementing the only method that was actually called when the window resized, viewWillDraw::
- (void)viewWillDraw{
[super viewWillDraw];
[self redraw];
}
- (void)redraw{
AppDelegate *appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
CALayer *superLayer = self.layer;
NSArray *sublayers = superLayer.sublayers;
NSImage *image;
CALayer *current;
for (CALayer *view in sublayers){
if (!view.isHidden){
current = view;
image = view.contents;
}
}
[appDelegate positionImage:image layer:current];
}
So... what's the right way to do this? viewWillDraw: get's called too many times which means I have to do unnecessary and redundant calculations, and I can't use viewWillStartLiveResize: because I need to constantly keep the image in its correct position. What am I overlooking?
Peter Hosey was right; my original method was clunky, and I shouldn't have been overriding setNeedsDisplayInRect:. I first made sure that I was using an auto layout in my app, then implemented the following:
subLayer.layoutManager = [CAConstraintLayoutManager layoutManager];
subLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
subLayer.contentsGravity = kCAGravityResizeAspect;
Basically, I set the sublayer's autoResizingMask to stretch both horizontally and vertically, and then set contentsGravity to preserve the aspect ratio.
That last variable I found by chance, but it's worth noting that you can only use a few contentsGravity constants if, like in my case, you're setting an NSImage as the layer's contents:
That method creates an image that is suited for use as the contents of a layer and that is supports all of the layer’s gravity modes. By contrast, the NSImage class supports only the kCAGravityResize, kCAGravityResizeAspect, and kCAGravityResizeAspectFill modes.
Always fun when a complicated solution can be simplified to 3 lines of code.

Changing a circle's size with UIPinchGestureRecognizer

I'm drawing a simple circle in the center of the screen:
int radius = 100;
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
[self.view.layer addSublayer:self.circle];
}
Using the pinch gesture, I allow the user to increase/decrease the radius of the shape:
- (void)scale:(UIPinchGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
if (gestureRecognizer.scale < lastScale) {
--radius;
}
else if (gestureRecognizer.scale > lastScale) {
++radius;
}
// Center the shape in self.view
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius, CGRectGetMidY(self.view.frame)-radius);
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
}
lastScale = gestureRecognizer.scale;
}
However, the circle doesn't stay dead center. Instead, it bounces around the middle and doesn't settle until the gesture finishes.
Does anyone know why this is happening and if so, how I can prevent it?
There are a few problems in your code. As #tc. said, you're not setting the shape layer's frame (or bounds). The default layer size is CGSizeZero, which is why you're having to offset the layer's position by the radius every time you change the radius.
Also, the position and path properties of a shape layer are animatable. So by default, when you change them, Core Animation will animate them to their new values. The path animation is contributing to your unwanted behavior.
Also, you should set the layer's position or frame based on self.view.bounds, not self.view.frame, because the layer's position/frame is the coordinate system of self.view, not the coordinate system of self.view.superview. This will matter if self.view is the top-level view and you support interface autorotation.
I would suggest revising how you're implementing this. Make radius a CGFloat property, and make setting the property update the layer's bounds and path:
#interface ViewController ()
#property (nonatomic, strong) CAShapeLayer *circle;
#property (nonatomic) CGFloat radius;
#end
#implementation ViewController
- (void)setRadius:(CGFloat)radius {
_radius = radius;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
}
If you really want to force the radius to be an integer, I suggest internally tracking it as a float anyway, because the user interaction is smoother if it's a float. Just round it in a temporary variable before creating the CGRect for the bounds and path:
CGFloat intRadius = roundf(radius);
self.circle.bounds = CGRectMake(0, 0, 2 * intRadius, 2 * intRadius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
In addCircle, just set the radius property and let that setter take care of setting the layer's bounds and path. Also defer setting the layer's position until the system's layout phase. That way, you'll reposition the circle in the center again after an interface rotation.
- (void)viewDidLoad {
[super viewDidLoad];
[self addCircle];
}
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
self.radius = 100;
[self.view.layer addSublayer:self.circle];
[self.view setNeedsLayout];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.circle.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
}
Finally, to handle a pinch gesture, just set the new radius to the old radius times the gesture's scale. The radius setter will take care of updating the layer's path and bounds. Then reset the gesture's scale to 1. This is simpler than tracking the gesture's prior scale. Also, use CATransaction to disable animation of the path property.
- (IBAction)pinchGestureWasRecognized:(UIPinchGestureRecognizer *)recognizer {
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
self.radius *= recognizer.scale;
recognizer.scale = 1;
} [CATransaction commit];
}

NSScrollView infinite / endless scroll | subview reuse

I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.

Erasing Cocoa Drawing done by NSRectFill?

I have an NSBox, inside of which I am drawing small rectangles, with NSRectFill(). My code for this looks like this:
for (int i = 0; i <= 100; i++){
int x = (rand() % 640) + 20;
int y = (rand() % 315) + 196;
array[i] = NSMakeRect(x, y, 4, 4);
NSRectFill(array[i]);
}
This for loop creates 100 randomly placed rectangles within the grid. What I have been trying to do is create a sort of animation, created by this code running over and over, creating an animation of randomly appearing rectangles, with this code:
for (int i = 0; i <= 10; i++) {
[self performSelector:#selector(executeFrame) withObject:nil afterDelay:(.05*i)];
}
The first for loop is the only thing inside the executeFrame function, by the way. So, what I need to do is to erase all the rectangles between frames, so the number of them stays the same and they look like they are moving. I tried doing this by just drawing the background again, by calling [myNsBox display]; before calling executeFrame, but that made it seem as though no rectangles were being drawn. Calling it after did the same thing, so did switching in setNeedsDisplay instead of display. I cannot figure this one out, any help would be appreciated.
By the way, an additional thing is that when I try to run my code for executing the frames, without trying to erase the rectangles in between, all that happens is that 100 more rectangles are drawn. Even if I have requested that 1000 be drawn, or 10,000. Then though, if I leave the window and come back to it (immediately, time is not a factor here), the page updates and the rectangles are there. I attempted to overcome that by with [box setNeedsDisplayInRect:array[i]]; which worked in a strange way, causing it to update every frame, but erasing portions of the rectangles. Any help in this would also be appreciated.
It sounds like you're drawing outside drawRect: . If that's the case, move your drawing code into a view's (the box's or some subview's) drawRect: method. Otherwise your drawing will get stomped on by the Cocoa drawing system like you're seeing. You'll also want to use timers or animations rather than loops to do the repeated drawing.
I recently wrote an example program for someone trying to do something similar with circles. The approach I took was to create an array of circle specifications and to draw them in drawRect. It works pretty well. Maybe it will help. If you want the whole project, you can download it from here
#implementation CircleView
#synthesize maxCircles, circleSize;
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
maxCircles = 1000;
circles = [[NSMutableArray alloc] initWithCapacity:maxCircles];
}
return self;
}
- (void)dealloc {
[circles release];
[super dealloc];
}
- (void)drawRect:(NSRect)dirtyRect {
NSArray *myCircles;
#synchronized(circles) {
myCircles = [circles copy];
}
NSRect bounds = [self bounds];
NSRect circleBounds;
for (NSDictionary *circleSpecs in myCircles) {
NSColor *color = [circleSpecs objectForKey:colorKey];
float size = [[circleSpecs objectForKey:sizeKey] floatValue];
NSPoint origin = NSPointFromString([circleSpecs objectForKey:originKey]);
circleBounds.size.width = size * bounds.size.width;
circleBounds.size.height = size * bounds.size.height;
circleBounds.origin.x = origin.x * bounds.size.width - (circleBounds.size.width / 2);
circleBounds.origin.y = origin.y * bounds.size.height - (circleBounds.size.height / 2);
NSBezierPath *drawingPath = [NSBezierPath bezierPath];
[color set];
[drawingPath appendBezierPathWithOvalInRect:circleBounds];
[drawingPath fill];
}
[myCircles release];
}
#pragma mark Public Methods
-(void)makeMoreCircles:(BOOL)flag {
if (flag) {
circleTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(makeACircle:) userInfo:nil repeats:YES];
}
else {
[circleTimer invalidate];
}
}
-(void)makeACircle:(NSTimer*)theTimer {
// Calculate a random color
NSColor *color;
color = [NSColor colorWithCalibratedRed:(arc4random() % 255) / 255.0
green:(arc4random() % 255) / 255.0
blue:(arc4random() % 255) / 255.0
alpha:(arc4random() % 255) / 255.0];
//Calculate a random origin from 0 to 1
NSPoint origin;
origin.x = (double)arc4random() / (double)0xFFFFFFFF;
origin.y = (double)arc4random() / (double)0xFFFFFFFF;
NSDictionary *circleSpecs = [NSDictionary dictionaryWithObjectsAndKeys:color, colorKey,
[NSNumber numberWithFloat:circleSize], sizeKey,
NSStringFromPoint(origin), originKey,
nil];
#synchronized(circles) {
[circles addObject:circleSpecs];
if ([circles count] > maxCircles) {
[circles removeObjectsInRange:NSMakeRange(0, [circles count] - maxCircles)];
}
}
[self setNeedsDisplay:YES];
}
#end

UISlider with ProgressView combined

Is there an apple-house-made way to get a UISlider with a ProgressView. This is used by many streaming applications e.g. native quicktimeplayer or youtube.
(Just to be sure: i'm only in the visualization interested)
cheers Simon
Here's a simple version of what you're describing.
It is "simple" in the sense that I didn't bother trying to add the shading and other subtleties. But it's easy to construct and you can tweak it to draw in a more subtle way if you like. For example, you could make your own image and use it as the slider's thumb.
This is actually a UISlider subclass lying on top of a UIView subclass (MyTherm) that draws the thermometer, plus two UILabels that draw the numbers.
The UISlider subclass eliminates the built-in track, so that the thermometer behind it shows through. But the UISlider's thumb (knob) is still draggable in the normal way, and you can set it to a custom image, get the Value Changed event when the user drags it, and so on. Here is the code for the UISlider subclass that eliminates its own track:
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect result = [super trackRectForBounds:bounds];
result.size.height = 0;
return result;
}
The thermometer is an instance of a custom UIView subclass, MyTherm. I instantiated it in the nib and unchecked its Opaque and gave it a background color of Clear Color. It has a value property so it knows how much to fill the thermometer. Here's its drawRect: code:
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
[[UIColor whiteColor] set];
CGFloat ins = 2.0;
CGRect r = CGRectInset(self.bounds, ins, ins);
CGFloat radius = r.size.height / 2.0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, CGRectGetMaxX(r) - radius, ins);
CGPathAddArc(path, NULL, radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true);
CGPathAddArc(path, NULL, CGRectGetMaxX(r) - radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true);
CGPathCloseSubpath(path);
CGContextAddPath(c, path);
CGContextSetLineWidth(c, 2);
CGContextStrokePath(c);
CGContextAddPath(c, path);
CGContextClip(c);
CGContextFillRect(c, CGRectMake(r.origin.x, r.origin.y, r.size.width * self.value, r.size.height));
}
To change the thermometer value, change the MyTherm instance's value to a number between 0 and 1, and tell it to redraw itself with setNeedsDisplay.
This is doable using the standard controls.
In Interface Builder place your UISlider immediately on top of your UIProgressView and make them the same size.
On a UISlider the background horizontal line is called the track, the trick is to make it invisible. We do this with a transparent PNG and the UISlider methods setMinimumTrackImage:forState: and setMaximumTrackImage:forState:.
In the viewDidLoad method of your view controller add:
[self.slider setMinimumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
[self.slider setMaximumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
where self.slider refers to your UISlider.
I've tested the code in Xcode, and this will give you a slider with an independent progress bar.
Solution that suits my design:
class SliderBuffering:UISlider {
let bufferProgress = UIProgressView(progressViewStyle: .Default)
override init (frame : CGRect) {
super.init(frame : frame)
}
convenience init () {
self.init(frame:CGRect.zero)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.minimumTrackTintColor = UIColor.clearColor()
self.maximumTrackTintColor = UIColor.clearColor()
bufferProgress.backgroundColor = UIColor.clearColor()
bufferProgress.userInteractionEnabled = false
bufferProgress.progress = 0.0
bufferProgress.progressTintColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5)
bufferProgress.trackTintColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
self.addSubview(bufferProgress)
}
}
Create a UISlider:
// 1
// Make the slider as a public propriety so you can access it
playerSlider = [[UISlider alloc] init];
[playerSlider setContinuous:YES];
[playerSlider setHighlighted:YES];
// remove the slider filling default blue color
[playerSlider setMaximumTrackTintColor:[UIColor clearColor]];
[playerSlider setMinimumTrackTintColor:[UIColor clearColor]];
// Chose your frame
playerSlider.frame = CGRectMake(--- , -- , yourSliderWith , ----);
// 2
// create a UIView that u can access and make it the shadow of your slider
shadowSlider = [[UIView alloc] init];
shadowSlider.backgroundColor = [UIColor lightTextColor];
shadowSlider.frame = CGRectMake(playerSlider.frame.origin.x , playerSlider.frame.origin.y , playerSlider.frame.size.width , playerSlider.frame.origin.size.height);
shadowSlider.layer.cornerRadius = 4;
shadowSlider.layer.masksToBounds = YES;
[playerSlider addSubview:shadowSlider];
[playerSlider sendSubviewToBack:shadowSlider];
// 3
// Add a timer Update your slider and shadow slider programatically
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(updateSlider) userInfo:nil repeats:YES];
-(void)updateSlider {
// Update the slider about the music time
playerSlider.value = audioPlayer.currentTime; // based on ur case
playerSlider.maximumValue = audioPlayer.duration;
float smartWidth = 0.0;
smartWidth = (yourSliderFullWidth * audioPlayer.duration ) / 100;
shadowSlider.frame = CGRectMake( shadowSlider.frame.origin.x , shadowSlider.frame.origin.y , smartWidth , shadowSlider.frame.size.height);
}
Enjoy! P.S. I might have some typos.
Idea 1:
You could easily use the UISlider as a progress view by subclassing it. It responds to methods such as 'setValue:animated:' with which you can set the value (i.e: progress) of the view.
Your only 'restriction' creating what you see in your example is the buffer bar, which you could create by 'creatively' skinning the UISlider (because you can add custom skins to it), and perhaps set that skin programmatically.
Idea 2:
Another (easier) option is to subclass UIProgressView, and create a UISlider inside that subclass. You can skin the UISlider to have a see-through skin (no bar, just the knob visible) and lay it over the UIProgressView.
You can use the UIProgressView for the pre-loading (buffering) and the UISlider for movie control / progress indication.
Seems fairly easy :-)
Edit: to actually answer your question, there is no in-house way, but it would be easy to accomplish with the tools given.
You can do some trick like this, it's more easy and understanding. Just insert the code bellow in your UISlider subclass.
- (void)layoutSubviews
{
[super layoutSubviews];
if (_availableDurationImageView == nil) {
// step 1
// get max length that our "availableDurationImageView" will show
UIView *maxTrackView = [self.subviews objectAtIndex:self.subviews.count - 3];
UIImageView *maxTrackImageView = [maxTrackView.subviews objectAtIndex:0];
_maxLength = maxTrackImageView.width;
// step 2
// get the right frame where our "availableDurationImageView" will place in superView
UIView *minTrackView = [self.subviews objectAtIndex:self.subviews.count - 2];
_availableDurationImageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"MediaSlider.bundle/4_jindu_huancun.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]];
_availableDurationImageView.opaque = NO;
_availableDurationImageView.frame = minTrackView.frame;
[self insertSubview:_availableDurationImageView belowSubview:minTrackView];
}
}
- (void)setAvailableValue:(NSTimeInterval)availableValue
{
if (availableValue >=0 && availableValue <= 1) {
// use "maxLength" and percentage to set our "availableDurationImageView" 's length
_availableDurationImageView.width = _maxLength * availableValue;
}
}
Adding on matt's solution, note that as of iOS 7.0, implementing trackRectForBounds: is rendered impossible. Here is my solution to this problem :
In your UISlider subclass, do this :
-(void)awakeFromNib
{
[super awakeFromNib];
UIImage* clearColorImage = [UIImage imageWithColor:[UIColor clearColor]];
[self setMinimumTrackImage:clearColorImage forState:UIControlStateNormal];
[self setMaximumTrackImage:clearColorImage forState:UIControlStateNormal];
}
with imageWithColor as this function :
+ (UIImage*) imageWithColor:(UIColor*)color
{
return [UIImage imageWithColor:color andSize:CGSizeMake(1.0f, 1.0f)];
}
That will properly take care of this annoying trackRectangle.
I spent too much time looking for a solution to this problem, here's hoping that'll save some time to another poor soul ;).
Here is a solution in Objective C. https://github.com/abhimuralidharan/BufferSlider
The idea is to create a UIProgressview as a property in the UISlider subclass and add the required constraints programatically.
#import <UIKit/UIKit.h> //.h file
#interface BufferSlider : UISlider
#property(strong,nonatomic) UIProgressView *bufferProgress;
#end
#import "BufferSlider.h" //.m file
#implementation BufferSlider
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
-(void)setup {
self.bufferProgress = [[UIProgressView alloc] initWithFrame:self.bounds];
self.minimumTrackTintColor = [UIColor redColor];
self.maximumTrackTintColor = [UIColor clearColor];
self.value = 0.2;
self.bufferProgress.backgroundColor = [UIColor clearColor];
self.bufferProgress.userInteractionEnabled = NO;
self.bufferProgress.progress = 0.7;
self.bufferProgress.progressTintColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
self.bufferProgress.trackTintColor = [[UIColor lightGrayColor] colorWithAlphaComponent:2];
[self addSubview:self.bufferProgress];
[self setThumbImage:[UIImage imageNamed:#"redThumb"] forState:UIControlStateNormal];
self.bufferProgress.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0.75]; // edit the constant value based on the thumb image
NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
[self addConstraints:#[left,right,centerY]];
[self sendSubviewToBack:self.bufferProgress];
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
#end
for Swift5
First, add tap gesture to slider:
let tap_gesture = UITapGestureRecognizer(target: self, action: #selector(self.sliderTapped(gestureRecognizer:)))
self.slider.addGestureRecognizer(tap_gesture)
Then, implement this function:
#objc func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
let locationOnSlider = gestureRecognizer.location(in: self.slider)
let maxWidth = self.slider.frame.size.width
let touchLocationRatio = locationOnSlider.x * CGFloat(self.slider.maximumValue) / CGFloat(maxWidth)
self.slider.value = Float(touchLocationRatio)
print("New value: ", round(self.slider.value))
}