Horizantal slider with tick marks on iOS - ios

Can i realise a horizontal slider with tick marks on iOS? There is not such option like in MacOS.

I actually wrote a tutorial on how to do this on my company's website:
http://fragmentlabs.com/blog/making-talking2trees-part-3-77
The quick answer for this is a custom method I wrote, and which is explained in the article above:
-(UIView*)tickMarksViewForSlider:(UISlider*)slider View:(UIView *)view
{
// set up vars
int ticksDivider = (slider.maximumValue > 10) ? 10 : 1;
int ticks = (int) slider.maximumValue / ticksDivider;
int sliderWidth = 364;
float offsetOffset = (ticks < 10) ? 1.7 : 1.1;
offsetOffset = (ticks > 10) ? 0 : offsetOffset;
float offset = sliderWidth / ticks - offsetOffset;
float xPos = 0;
// initialize view to return
view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y+1,
slider.frame.size.width, slider.frame.size.height);
view.backgroundColor = [UIColor clearColor];
// make a UIImageView with tick for each tick in the slider
for (int i=0; i < ticks; i++)
{
if (i == 0) {
xPos += offset+5.25;
}
else
{
UIView *tick = [[UIView alloc] initWithFrame:CGRectMake(xPos, 3, 2, 16)];
tick.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1];
tick.layer.shadowColor = [[UIColor whiteColor] CGColor];
tick.layer.shadowOffset = CGSizeMake(0.0f, 1.0f);
tick.layer.shadowOpacity = 1.0f;
tick.layer.shadowRadius = 0.0f;
[view insertSubview:tick belowSubview:slider];
xPos += offset - 0.4;
}
}
// return the view
return view;
}
You'd basically apply this method to a view that sits behind your UISlider, and it creates the tick marks at the appropriate intervals. You can change how many ticks are visible by modifying the ticksDivider variable. The above example creates one per slider value unless the slider's maximumValue property is greater than 10 (my sliders had values of 5, 40 and 120), in which case I divided the value by 10.
You have to play with the offsetOffset value and the float value after xPos += offset+... to account for the width of your thumb image and slider cap insets (so the thumb button appears to be centered over each).

From #CarlGoldsmith - You'd have to program that yourself; UISliders on iOS don't have that functionality.

While ctlockey's solution certainly works, I wanted something simpler but also that allowed the thumb to snap to whichever tick mark it is closest to when the user releases it.
My solution is below. It is a subclass of UISlider that only overrides two methods. It is bare bones but would be a foundation for a more complex version.
-(void)endTrackingWithTouch:(UITouch *)touch
withEvent:(UIEvent *)event
{
[super endTrackingWithTouch:touch withEvent:event];
if (self.value != floorf(self.value))
{
CGFloat roundedFloat = (float)roundf(self.value);
[self setValue:roundedFloat animated:YES];
}
}
-(void)drawRect:(CGRect)rect
{
CGFloat thumbOffset = self.currentThumbImage.size.width / 2.0f;
NSInteger numberOfTicksToDraw = roundf(self.maximumValue - self.minimumValue) + 1;
CGFloat distMinTickToMax = self.frame.size.width - (2 * thumbOffset);
CGFloat distBetweenTicks = distMinTickToMax / ((float)numberOfTicksToDraw - 1);
CGFloat xTickMarkPosition = thumbOffset; //will change as tick marks are drawn across slider
CGFloat yTickMarkStartingPosition = self.frame.size.height / 2; //will not change
CGFloat yTickMarkEndingPosition = self.frame.size.height; //will not change
UIBezierPath *tickPath = [UIBezierPath bezierPath];
for (int i = 0; i < numberOfTicksToDraw; i++)
{
[tickPath moveToPoint:CGPointMake(xTickMarkPosition, yTickMarkStartingPosition)];
[tickPath addLineToPoint:CGPointMake(xTickMarkPosition, yTickMarkEndingPosition)];
xTickMarkPosition += distBetweenTicks;
}
[tickPath stroke];
}

Related

Unable to scale shapes to match the dimensions of UIView

I have created a custom view that draws shapes of varying sides. The view is added to main view as a subview as given below. The shapes are of different dimensions.
My source code is given below
-(instancetype) initWithSides:(NSUInteger) sides andFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
self.sides = sides;
self.radius = CGRectGetWidth(self.frame) * 1.5 / sides);
}
return self;
}
-(void) drawRect:(CGRect) frame {
// Sides is passed as constructor argument. 4 sides means a quadrilateral etc.
// Get CGPAthRef instance
CGFloat angle = DEGREES_TO_RADIANS(360 / ((CGFloat) self.sides));
int count = 0;
CGFloat xCoord = CGRectGetMidX(self.frame);
CGFloat yCoord = CGRectGetMidY(self.frame);
while (count < self.sides) {
CGFloat xPosition =
xCoord + self.radius * cos(angle * ((CGFloat) count));
CGFloat yPosition =
yCoord + self.radius * sin(angle * ((CGFloat) count));
[self.points addObject:[NSValue valueWithCGPoint:CGPointMake(xPosition, yPosition)]];
count ++;
}
NSValue* v = [self.points firstObject];
CGPoint first = v.CGPointValue;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, first.x, first.y);
for (int ix = 1; ix < [self.points count]; ix++) {
NSValue* pValue = [self.points objectAtIndex:ix];
CGPoint p = pValue.CGPointValue;
CGPathAddLineToPoint(path, nil, p.x, p.y);
}
CGPathCloseSubpath(path);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path);
[self colourView:context withPath:path];
}
-(void) colourView:(CGContextRef) context withPath:(CGPathRef) ref {
NSUInteger num = arc4random_uniform(8) + 1;
UIColor* color = nil;
switch (num) {
case 1:
color = [UIColor redColor];
break;
case 2:
color = [UIColor greenColor];
break;
case 3:
color = [UIColor yellowColor];
break;
case 4:
color = [UIColor blueColor];
break;
case 5:
color = [UIColor orangeColor];
break;
case 6:
color = [UIColor brownColor];
break;
case 7:
color = [UIColor purpleColor];
break;
case 8:
color = [UIColor blackColor];
break;
default:
break;
}
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillPath(context);
}
This constitutes one single shape. This is how I am drawing the rest of the views.
-(void) initDrawView {
NSUInteger width = CGRectGetWidth(self.view.bounds) / 8;
NSUInteger height = (CGRectGetHeight(self.view.frame))/ 8;
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleSingleTap:)];
for (int i = 0 ; i < 8; i++) {
CGFloat yCoord = i * height + i * 15;
for (int j = 0; j < 8; j ++) {
int side = 3 + arc4random() % 8;
CGFloat xCoord = j * width;
SimpleRegularPolygonView* view =
[[SimpleRegularPolygonView alloc] initWithSides:side andFrame:CGRectMake(xCoord, yCoord, width, height)];
[view sizeToFit];
view.viewEffectsDelegate = self;
[view setTag: (8 * i + j)];
[self.view addGestureRecognizer:singleFingerTap];
[self.view addSubview:view];
}
}
}
1) I don't know how to make them have the same dimensions. How can I do that? (First image)
2) the images don't scale up to the size of the UIView (Second image).
Your current code makes the radius inversely proportional to the number of sides:
self.radius = CGRectGetWidth(self.frame) * 1.5 / sides;
so the more sides, the smaller the image. A quick fix would be to just make the radius half the frame width:
self.radius = CGRectGetWidth(self.frame) /2;
This will mean shapes with an even number of sides fill the frame width. But those with an odd number of sides will appear to have space to the left. If you want to adjust for that you will need more detailed calculations for the width, and you will also need to move the "centre" of the shape. For an odd number sides, the radius would need to be:
self.radius = CGRectGetWidth(self.frame) /(1 + cos(angle / 2));
and xCoord would need to be:
CGFloat xCoord = CGRectGetMinX(self.frame) + self.radius * cos(angle/2);

Adding UIImage's as "Tick Marks" to UISlider

To sum up my question beforehand: I'm trying to determine where on the slider I can place the image based upon knowing only the UISlider's duration, and having an array of times to loop through, placing the images accordingly.
I've been reading through the Apple Docs on UISlider, and it appears that there is no native way to add "Tick marks" on a UISlider based upon an array of floats. "Tick marks" meaning lines upon a slider, such as those used to place advertisements on scrubbers. Here is a visualization:
Now, I have an array full of floats; Floats in which I will use to drop the tick marks based upon the UISlider's value. The values of the floats in the array will be different every time. I would like to loop through the .value property of my UISlider, dropping the UIImages accordingly. The UIImage's are the tick marks that are just little png's assets I created. What I cannot figure out is the logic behind looping through the .value property of the UISlider and placing the UIImage in accordance with the UISlider's future position. The values of the floats in the array will be different every time, so I can't place them statically. Does anyone know where to start? I'm still a little new to Objective-C programming.
I know that it may be possible utilize retrieving the slider's beginning X coordinate on the screen, like so:
- (float)xPositionFromSliderValue:(UISlider *)aSlider;
{
float sliderRange = aSlider.frame.size.width - aSlider.currentThumbImage.size.width;
float sliderOrigin = aSlider.frame.origin.x + (aSlider.currentThumbImage.size.width / 2.0);
float sliderValueToPixels = (((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValu‌​e)) * sliderRange) + sliderOrigin);
return sliderValueToPixels;
}
Maybe I could add in a calculation in the for loop to place the image in accordance to that instead. I'm just not too sure where even to begin here...
The methods trackRectForBounds and thumbRectForBounds are provided for subclassing UISlider, but you can call them directly, and they will get your tick centers up front.
- (float)sliderThumbCenter:(UISlider *)slider forValue:(float)value{
CGRect trackRect = [slider trackRectForBounds:slider.bounds];
CGRect thumbRect = [slider thumbRectForBounds:slider.bounds trackRect:trackRect value:value];
CGFloat centerThumb = CGRectGetMidX(thumbRect);
return centerThumb;
}
And it might be easier to do a custom view to draw the track rather than Image views, then just put the slider on top of it and hide the track. Just make the slider frame equal to the TickView's bounds. Really I suppose a UISlider subclass would be better, but this works!
#interface TickView : UIView
#property UIColor *tickColor;
#property int tickCount;
#property CGFloat tickHeight;
#property (weak) UISlider *slider;
#property float *ticks;
-(void)setTicks:(float *)ticks count:(int)tickCount;
#end
#implementation TickView{
__weak UISlider *_slider;
}
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.tickColor = [UIColor grayColor];
self.backgroundColor = [UIColor clearColor];
self.tickCount = 7;
self.ticks = malloc(sizeof(float) * self.tickCount);
self.tickHeight = 10;
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, self.tickColor.CGColor);
CGContextBeginPath(context);
CGFloat centerY = rect.size.height / 2;
CGContextMoveToPoint(context, 0, centerY);
CGContextAddLineToPoint(context, rect.size.width, centerY);
CGFloat tickTop = centerY - self.tickHeight / 2;
CGFloat tickBottom = centerY + self.tickHeight / 2;
CGFloat tickX = 0;
if (self.slider) {
for (int i = 0; i < self.tickCount; i++) {
tickX = [self sliderThumbCenter:self.slider forValue:self.ticks[i]];
CGContextMoveToPoint(context, tickX, tickTop);
CGContextAddLineToPoint(context, tickX, tickBottom);
}
}
else{
CGFloat tickSpacing = rect.size.width / (self.tickCount - 1);
for (int i = 0; i < self.tickCount; i++) {
CGContextMoveToPoint(context, tickX, tickTop);
CGContextAddLineToPoint(context, tickX, tickBottom);
tickX += tickSpacing;
}
}
CGContextStrokePath(context);
}
-(void)setTicks:(float *)ticks count:(int)tickCount{
free(_ticks);
_ticks = malloc(sizeof(float) * tickCount);
memcpy(_ticks, ticks, sizeof(float) * tickCount);
_tickCount = tickCount;
[self setNeedsDisplay];
}
- (float)sliderThumbCenter:(UISlider *)slider forValue:(float)value{
CGRect trackRect = [slider trackRectForBounds:slider.bounds];
CGRect thumbRect = [slider thumbRectForBounds:slider.bounds trackRect:trackRect value:value];
CGFloat centerThumb = CGRectGetMidX(thumbRect);
return centerThumb;
}
-(void)setSlider:(UISlider *)slider{
_slider = slider;
}
-(UISlider *)slider{
return _slider;
}
-(void)dealloc{
free(_ticks);
}
#end
I think you will have trouble positioning the tick marks. However, if the parent view of your UISlider is "view", you add a subview like this:
[view addSubView:myTickView];
The position of the added subview is determined by its frame property, which is in the parent's view coordinate space.
To remove a view, you do this:
[myTickView removeFromSuperView];
You can also loop through your tick views and change there frames, but these changes will be animated, so the ticks will appear to slide if you do that, unless you turn animations off.

change position of Slide To Cancel

I'm using SlideToCancel as per requirements to my project but i'm unable to find how to change the position of button.
I have gone through all the code but cannot figure.
Here is the main code:
import
#import "SlideToCancelViewController.h"
#interface SlideToCancelViewController()
- (void) setGradientLocations:(CGFloat)leftEdge;
- (void) startTimer;
- (void) stopTimer;
#end
static const CGFloat gradientWidth = 0.2;
static const CGFloat gradientDimAlpha = 0.5;
static const int animationFramesPerSec = 8;
#implementation SlideToCancelViewController
#synthesize delegate;
// Implement the "enabled" property
- (BOOL) enabled {
return slider.enabled;
}
- (void) setEnabled:(BOOL)enabled{
slider.enabled = enabled;
label.enabled = enabled;
if (enabled) {
slider.value = 0.0;
label.alpha = 1.0;
touchIsDown = NO;
[self startTimer];
} else {
[self stopTimer];
}
}
- (UILabel *)label {
// Access the view, which will force loadView to be called
// if it hasn't already been, which will create the label
(void)[self view];
return label;
}
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
// Load the track background
UIImage *trackImage = [UIImage imageNamed:#"sliderTrack.png"];
sliderBackground = [[UIImageView alloc] initWithImage:trackImage];
// Create the superview same size as track backround, and add the background image to it
UIView *view = [[UIView alloc] initWithFrame:sliderBackground.frame];
[view addSubview:sliderBackground];
// Add the slider with correct geometry centered over the track
slider = [[UISlider alloc] initWithFrame:sliderBackground.frame];
CGRect sliderFrame = slider.frame;
sliderFrame.size.width -= 46; //each "edge" of the track is 23 pixels wide
slider.frame = sliderFrame;
slider.center = sliderBackground.center;
slider.backgroundColor = [UIColor clearColor];
[slider setMinimumTrackImage:[UIImage imageNamed:#"sliderMaxMin-02.png"] forState:UIControlStateNormal];
[slider setMaximumTrackImage:[UIImage imageNamed:#"sliderMaxMin-02.png"] forState:UIControlStateNormal];
UIImage *thumbImage = [UIImage imageNamed:#"sliderThumb.png"];
[slider setThumbImage:thumbImage forState:UIControlStateNormal];
slider.minimumValue = 0.0;
slider.maximumValue = 1.0;
slider.continuous = YES;
slider.value = 0.0;
// Set the slider action methods
[slider addTarget:self
action:#selector(sliderUp:)
forControlEvents:UIControlEventTouchUpInside];
[slider addTarget:self
action:#selector(sliderDown:)
forControlEvents:UIControlEventTouchDown];
[slider addTarget:self
action:#selector(sliderChanged:)
forControlEvents:UIControlEventValueChanged];
// Create the label with the actual size required by the text
// If you change the text, font, or font size by using the "label" property,
// you may need to recalculate the label's frame.
NSString *labelText = NSLocalizedString(#"slide to cancel", #"SlideToCancel label");
UIFont *labelFont = [UIFont systemFontOfSize:24];
CGSize labelSize = [labelText sizeWithFont:labelFont];
label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, labelSize.width, labelSize.height)];
// Center the label over the slidable portion of the track
CGFloat labelHorizontalCenter = slider.center.x + (thumbImage.size.width / 2);
label.center = CGPointMake(labelHorizontalCenter, slider.center.y);
// Set other label attributes and add it to the view
label.textColor = [UIColor whiteColor];
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
label.font = labelFont;
label.text = labelText;
[view addSubview:label];
[view addSubview:slider];
// This property is set to NO (disabled) on creation.
// The caller must set it to YES to animate the slider.
// It should be set to NO (disabled) when the view is not visible, in order
// to turn off the timer and conserve CPU resources.
self.enabled = NO;
// Render the label text animation using our custom drawing code in
// the label's layer.
label.layer.delegate = self;
// Set the view controller's view property to all of the above
self.view = view;
// The view is retained by the superclass, so release our copy
[view release];
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
[self stopTimer];
[sliderBackground release], sliderBackground = nil;
[slider release], slider = nil;
[label release], label = nil;
}
// UISlider actions
- (void) sliderUp: (UISlider *) sender
{
//filter out duplicate sliderUp events
if (touchIsDown) {
touchIsDown = NO;
if (slider.value != 1.0) //if the value is not the max, slide this bad boy back to zero
{
[slider setValue: 0 animated: YES];
label.alpha = 1.0;
[self startTimer];
}
else {
//tell the delagate we are slid all the way to the right
[delegate cancelled];
}
}
}
- (void) sliderDown: (UISlider *) sender
{
touchIsDown = YES;
}
- (void) sliderChanged: (UISlider *) sender
{
// Fade the text as the slider moves to the right. This code makes the
// text totally dissapear when the slider is 35% of the way to the right.
label.alpha = MAX(0.0, 1.0 - (slider.value * 3.5));
// Stop the animation if the slider moved off the zero point
if (slider.value != 0) {
[self stopTimer];
[label.layer setNeedsDisplay];
}
}
// animationTimer methods
- (void)animationTimerFired:(NSTimer*)theTimer {
// Let the timer run for 2 * FPS rate before resetting.
// This gives one second of sliding the highlight off to the right, plus one
// additional second of uniform dimness
if (++animationTimerCount == (2 * animationFramesPerSec)) {
animationTimerCount = 0;
}
// Update the gradient for the next frame
[self setGradientLocations:((CGFloat)animationTimerCount/(CGFloat)animationFramesPerSec)];
}
- (void) startTimer {
if (!animationTimer) {
animationTimerCount = 0;
[self setGradientLocations:0];
animationTimer = [[NSTimer
scheduledTimerWithTimeInterval:1.0/animationFramesPerSec
target:self
selector:#selector(animationTimerFired:)
userInfo:nil
repeats:YES] retain];
}
}
- (void) stopTimer {
if (animationTimer) {
[animationTimer invalidate];
[animationTimer release], animationTimer = nil;
}
}
// label's layer delegate method
- (void)drawLayer:(CALayer *)theLayer
inContext:(CGContextRef)theContext
{
// Set the font
const char *labelFontName = [label.font.fontName UTF8String];
// Note: due to use of kCGEncodingMacRoman, this code only works with Roman alphabets!
// In order to support non-Roman alphabets, you need to add code generate glyphs,
// and use CGContextShowGlyphsAtPoint
CGContextSelectFont(theContext, labelFontName, label.font.pointSize, kCGEncodingMacRoman);
// Set Text Matrix
CGAffineTransform xform = CGAffineTransformMake(1.0, 0.0,
0.0, -1.0,
0.0, 0.0);
CGContextSetTextMatrix(theContext, xform);
// Set Drawing Mode to clipping path, to clip the gradient created below
CGContextSetTextDrawingMode (theContext, kCGTextClip);
// Draw the label's text
const char *text = [label.text cStringUsingEncoding:NSMacOSRomanStringEncoding];
CGContextShowTextAtPoint(
theContext,
0,
(size_t)label.font.ascender,
text,
strlen(text));
// Calculate text width
CGPoint textEnd = CGContextGetTextPosition(theContext);
// Get the foreground text color from the UILabel.
// Note: UIColor color space may be either monochrome or RGB.
// If monochrome, there are 2 components, including alpha.
// If RGB, there are 4 components, including alpha.
CGColorRef textColor = label.textColor.CGColor;
const CGFloat *components = CGColorGetComponents(textColor);
size_t numberOfComponents = CGColorGetNumberOfComponents(textColor);
BOOL isRGB = (numberOfComponents == 4);
CGFloat red = components[0];
CGFloat green = isRGB ? components[1] : components[0];
CGFloat blue = isRGB ? components[2] : components[0];
CGFloat alpha = isRGB ? components[3] : components[1];
// The gradient has 4 sections, whose relative positions are defined by
// the "gradientLocations" array:
// 1) from 0.0 to gradientLocations[0] (dim)
// 2) from gradientLocations[0] to gradientLocations[1] (increasing brightness)
// 3) from gradientLocations[1] to gradientLocations[2] (decreasing brightness)
// 4) from gradientLocations[3] to 1.0 (dim)
size_t num_locations = 3;
// The gradientComponents array is a 4 x 3 matrix. Each row of the matrix
// defines the R, G, B, and alpha values to be used by the corresponding
// element of the gradientLocations array
CGFloat gradientComponents[12];
for (int row = 0; row < num_locations; row++) {
int index = 4 * row;
gradientComponents[index++] = red;
gradientComponents[index++] = green;
gradientComponents[index++] = blue;
gradientComponents[index] = alpha * gradientDimAlpha;
}
// If animating, set the center of the gradient to be bright (maximum alpha)
// Otherwise it stays dim (as set above) leaving the text at uniform
// dim brightness
if (animationTimer) {
gradientComponents[7] = alpha;
}
// Load RGB Colorspace
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
// Create Gradient
CGGradientRef gradient = CGGradientCreateWithColorComponents (colorspace, gradientComponents,
gradientLocations, num_locations);
// Draw the gradient (using label text as the clipping path)
CGContextDrawLinearGradient (theContext, gradient, label.bounds.origin, textEnd, 0);
// Cleanup
CGGradientRelease(gradient);
CGColorSpaceRelease(colorspace);
}
- (void) setGradientLocations:(CGFloat) leftEdge {
// Subtract the gradient width to start the animation with the brightest
// part (center) of the gradient at left edge of the label text
leftEdge -= gradientWidth;
//position the bright segment of the gradient, keeping all segments within the range 0..1
gradientLocations[0] = leftEdge < 0.0 ? 0.0 : (leftEdge > 1.0 ? 1.0 : leftEdge);
gradientLocations[1] = MIN(leftEdge + gradientWidth, 1.0);
gradientLocations[2] = MIN(gradientLocations[1] + gradientWidth, 1.0);
// Re-render the label text
[label.layer setNeedsDisplay];
}
- (void)dealloc {
[self stopTimer];
[self viewDidUnload];
[super dealloc];
}
#end
Sorry for posting heavy code stuff.
P.S.: I know AppStore doesn't accepts this code but i'm interested to learn & do more on QuartzCore graphics than using interface builders.
Check this answer https://stackoverflow.com/a/25894355/1344237
You have to remove the label.layer.delegate = self;

How to add "Copy" option to text?

I have a text in a "bubble" by fitting the text inside an image as shown below. it works fine except the text is not copyable. I need to have "copy" option so that when user tab on the text (or bubble image") "cop" options is displayed, just like in when user tab into the built in SMS Message bubble.
#import "SpeechBubbleView.h"
static UIFont* font = nil;
static UIImage* lefthandImage = nil;
static UIImage* righthandImage = nil;
const CGFloat VertPadding = 4; // additional padding around the edges
const CGFloat HorzPadding = 4;
const CGFloat TextLeftMargin = 17; // insets for the text
const CGFloat TextRightMargin = 15;
const CGFloat TextTopMargin = 10;
const CGFloat TextBottomMargin = 11;
const CGFloat MinBubbleWidth = 50; // minimum width of the bubble
const CGFloat MinBubbleHeight = 40; // minimum height of the bubble
const CGFloat WrapWidth = 200; // maximum width of text in the bubble
#implementation SpeechBubbleView
+ (void)initialize
{
if (self == [SpeechBubbleView class])
{
font = /*[*/ [UIFont systemFontOfSize:[UIFont systemFontSize]] /*retain]*/;
lefthandImage = /*[*/ [[UIImage imageNamed:/*#"BubbleLefthand"*/#"lefttest4"]
stretchableImageWithLeftCapWidth:20 topCapHeight:19] /*retain]*/;
righthandImage = /*[*/[[UIImage imageNamed:/*#"BubbleRighthand"*/#"righttest4"]
stretchableImageWithLeftCapWidth:20 topCapHeight:19] /*retain]*/;
}
}
+ (CGSize)sizeForText:(NSString*)text
{
CGSize textSize = [text sizeWithFont:font
constrainedToSize:CGSizeMake(WrapWidth, 9999)
lineBreakMode:UILineBreakModeWordWrap];
CGSize bubbleSize;
bubbleSize.width = textSize.width + TextLeftMargin + TextRightMargin;
bubbleSize.height = textSize.height + TextTopMargin + TextBottomMargin;
if (bubbleSize.width < MinBubbleWidth)
bubbleSize.width = MinBubbleWidth;
if (bubbleSize.height < MinBubbleHeight)
bubbleSize.height = MinBubbleHeight;
bubbleSize.width += HorzPadding*2;
bubbleSize.height += VertPadding*2;
return bubbleSize;
}
- (void)drawRect:(CGRect)rect
{
[self.backgroundColor setFill];
UIRectFill(rect);
CGRect bubbleRect = CGRectInset(self.bounds, VertPadding, HorzPadding);
CGRect textRect;
textRect.origin.y = bubbleRect.origin.y + TextTopMargin;
textRect.size.width = bubbleRect.size.width - TextLeftMargin - TextRightMargin;
textRect.size.height = bubbleRect.size.height - TextTopMargin - TextBottomMargin;
if (bubbleType == BubbleTypeLefthand)
{
[lefthandImage drawInRect:bubbleRect];
textRect.origin.x = bubbleRect.origin.x + TextLeftMargin;
}
else
{
[righthandImage drawInRect:bubbleRect];
textRect.origin.x = bubbleRect.origin.x + TextRightMargin;
}
[[UIColor blackColor] set];
[text drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeWordWrap];
}
- (void)setText:(NSString*)newText bubbleType:(BubbleType)newBubbleType
{
//[text release];
/**
handle local
**/
// text = [newText copy];
text = [newText copy];
bubbleType = newBubbleType;
[self setNeedsDisplay];
}
- (void)dealloc
{
//[text release];
//[super dealloc];
}
#end
If you want the system cut/copy/paste menu (or some subset thereof) without the full suite of text editability features from UITextField or UITextView, you need to present your own UIMenuController. Doing this requires:
the control to be affected by the menu can become first responder
said control (or something sensible in the responder chain above it) implements the editing methods (e.g. copy:) you want to support
you perform some event handling to present the menu controller at an appropriate time (e.g. a gesture recognizer action)
As is often the case, there's a good tutorial on NSHipster with both Swift and ObjC sample code. Kudos to #mattt, though I'd say this line needs a correction:
If you’re wondering why, oh why, this isn’t just built into UILabel, well… join the club.
"Join the club" should be replaced with "file a bug". It even sorta almost rhymes!

How to redraw a certain rect in drawRect

Here is my problem. I am planning to draw 999 circles inside a view at the time of setup, which is done successfully in drawRect. After that, I am planning to edit the color of each circle when user touches (which i am handling using the tap gesture in the view controller). The problem is, when I call [self.pegView setNeedsDisplay]; the drawRect is getting called and goes to the else part of the if in drawRect (which it should), but all the other beads are removed, and I see only 1 bead instead of 999 beads.
Below is my code.
- (void)drawRect:(CGRect)rect
{
if(!self.isEditingHappening) // For the initial setup
{
CGRect beadRect = CGRectMake(BEAD_ORIGIN_X, BEAD_ORIGIN_Y, BEAD_VIEW_SIZE, BEAD_VIEW_SIZE);
self.ctx = UIGraphicsGetCurrentContext();
for(int i = 0 ; i < 999 ; i++)
{
// To set up the bead view for each column
if (i !=0 && i % 37 !=0)
{
beadRect.origin = CGPointMake((beadRect.origin.x + BEAD_VIEW_SIZE), beadRect.origin.y);
}
// To set up bead view for each row
if (i !=0 && i %37 ==0 )
{
beadRect.origin.y += BEAD_VIEW_SIZE;
beadRect.origin = CGPointMake(BEAD_ORIGIN_X, beadRect.origin.y);
}
BeadPattern *pattern = [self.beadPatternsArray objectAtIndex:(i/37)];
int beadType=[[pattern.eachRowPattern objectAtIndex:i%37] intValue];
BeadColor *color=[self.beadColorsArray objectAtIndex:beadType];
CGPoint center;
center.x = beadRect.origin.x + beadRect.size.width / 2.0;
center.y = beadRect.origin.y + beadRect.size.height / 2.0;
CGRect newInnerRect;
newInnerRect.size.width = beadRect.size.width * 0.6;
newInnerRect.size.height = beadRect.size.height * 0.6;
newInnerRect.origin.x = center.x - newInnerRect.size.width/2;
newInnerRect.origin.y = center.y - newInnerRect.size.height/2;
[[UIColor whiteColor] set];
UIRectFill(beadRect);
// Adds the outer circle
CGContextAddEllipseInRect(self.ctx, beadRect);
// Fill the outer circle with any color
CGContextSetRGBFillColor(self.ctx, color.redValue, color.greenValue, color.blueValue, 1);
CGContextFillEllipseInRect(self.ctx, beadRect);
//Add inner circle
CGContextAddEllipseInRect(self.ctx, newInnerRect);
//Fill inner circle with white color
CGContextSetRGBFillColor(self.ctx, 1, 1, 1, 0.4);
CGContextFillEllipseInRect (self.ctx, newInnerRect);
}
}
else // When editing is happening
{
NSLog(#"The redrawing rect is %#", NSStringFromCGRect(rect));
CGContextSaveGState(self.ctx);
CGRect beadRect;
beadRect.size = CGSizeMake(BEAD_VIEW_SIZE, BEAD_VIEW_SIZE);
beadRect.origin = CGPointMake(self.columnToBeUpdated * BEAD_VIEW_SIZE, self.rowToBeUpdated * BEAD_VIEW_SIZE);
CGPoint center;
center.x = beadRect.origin.x + beadRect.size.width / 2.0;
center.y = beadRect.origin.y + beadRect.size.height / 2.0;
CGRect newInnerRect;
newInnerRect.size.width = beadRect.size.width * 0.6;
newInnerRect.size.height = beadRect.size.height * 0.6;
newInnerRect.origin.x = center.x - newInnerRect.size.width/2;
newInnerRect.origin.y = center.y - newInnerRect.size.height/2;
CGContextRestoreGState(self.ctx);
[[UIColor whiteColor] set];
UIRectFill(beadRect);
// Adds the outer circle
CGContextAddEllipseInRect(self.ctx, beadRect);
// Fill the outer circle with any color
CGContextSetRGBFillColor(self.ctx, self.colorToBeShownWhileEdited.redValue, self.colorToBeShownWhileEdited.greenValue, self.colorToBeShownWhileEdited.blueValue, 1);
CGContextFillEllipseInRect(self.ctx, beadRect);
//Add inner circle
CGContextAddEllipseInRect(self.ctx, newInnerRect);
//Fill inner circle with white color
CGContextSetRGBFillColor(self.ctx, 1, 1, 1, 0.4);
CGContextFillEllipseInRect (self.ctx, newInnerRect);
}
}
What I intend to do is, keep all the 999 beads, but edit only one bead's color for which the user has touched. I tries using [setNeedsDisplayInRect] also, but the rect is not passed correctly. Please help me here.
If I remember correctly, it clears off the other items on the canvas -so your initial setup stuff would need to run each time.
Or, you could create separate views for each bead that would allow you to just follow that one specifically. Each bead could contain its own logic.

Resources