I'm trying to apply an angle gradient to the dashes created with the code I've written inside a custom UIView class, as below. Although it needs tweaking, I'm happy with the results it produces so far.
Given the input parameters in the view initialisation (below), and a frame of 768 * 768 on an iPad Air2 in portrait mode, it produces the following gauge:
First gauge
What I'd like to do is to cause each of the dashes to step through a user-defined gradient, e.g. green to red, much like this (kludged in Photoshop):
Gauge with colours
I've searched high and low, and cannot find anything to achieve this. The only things that come close use different drawing methods, and I want to keep my drawing routine.
As far as I'm concerned, I should simply be able to call:
CGContextSetStrokeColorWithColor(myContext, [gradient color goes here])
inside the draw loop, and that's it, but I don't know how to create the relevant color array/gradient, and change the line drawing color according to an index into that array.
Any help would be much appreciated.
- (void)drawRect:(CGRect)rect {
myContext = UIGraphicsGetCurrentContext();
UIImage *gaugeImage = [self radials:300 andSteps:3 andLineWidth:10.0];
UIImageView *gaugeImageView = [[UIImageView alloc] initWithImage:gaugeImage];
[self addSubview:gaugeImageView];
}
-(UIImage *)radials:(NSInteger)degrees andSteps:(NSInteger)steps andLineWidth:(CGFloat)lineWidth{
UIGraphicsBeginImageContext(self.bounds.size);
myContext = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(myContext, lineWidth);
CGContextSetStrokeColorWithColor(myContext, [[UIColor blackColor] CGColor]);
CGPoint center = CGPointMake(self.bounds.origin.x+(self.bounds.size.width/2), self.bounds.origin.y+(self.bounds.size.height/2));
CGFloat r1 = center.x * 0.87f;
CGFloat r2 = center.x * 0.95f;
CGContextTranslateCTM(myContext, center.x, center.y);
CGContextBeginPath(myContext);
CGFloat offset = 0;
if(degrees < 360){
offset = (360-degrees) / 2;
}
for(int lp = offset + 0 ; lp < offset + degrees+1 ; lp+=steps){
CGFloat theta = lp * (2 * M_PI / 360);
CGContextMoveToPoint(myContext, 0, 0);
r1 = center.x * 0.87f;
if(lp % 10 == 0){
r1 = center.x * 0.81f;
}
CGContextMoveToPoint(myContext, sin(theta) * r1, cos(theta) * r1);
CGContextAddLineToPoint(myContext, sin(theta) * r2, cos(theta) * r2);
CGContextStrokePath(myContext);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
So, you want something like this:
First, a couple of gentle suggestions:
Don't add subviews inside drawRect:. What if drawRect: gets called a second time, if for example the view's size changes?
Here's what the View Programming Guide for iOS says about implementing drawRect::
The implementation of your drawRect: method should do exactly one thing: draw your content. This method is not the place to be updating your application’s data structures or performing any tasks not related to drawing. It should configure the drawing environment, draw your content, and exit as quickly as possible. And if your drawRect: method might be called frequently, you should do everything you can to optimize your drawing code and draw as little as possible each time the method is called.
If you need to add or remove subviews, you should do that when the view is initialized, or in layoutSubviews at the latest.
There's no need to draw into an image or use an image view at all. The whole point of drawRect: is to draw into the current graphics context, which UIKit has already set up to target the view's backing store.
Those suggestions aside, there is no support for angular gradients in Core Graphics. However, for your graphic, you can set the color for each tick mark separately and get a pretty good approximation, which is how I created the image above. Use +[UIColor colorWithHue:saturation:brightness:alpha:] to create your color, calculating the hue parameter based on the tick angle.
If you factor out the drawing code into a separate class, it's easy to use it to draw either directly to a view (in drawRect:), or to an image if you need to. Here's the interface:
#interface RainbowGaugeAppearance: NSObject
#property (nonatomic) CGFloat startDegrees;
#property (nonatomic) CGFloat endDegrees;
#property (nonatomic) CGFloat degreesPerMajorTick;
#property (nonatomic) int subdivisionsPerMajorTick;
#property (nonatomic) CGFloat tickThickness;
#property (nonatomic) CGFloat startHue;
#property (nonatomic) CGFloat endHue;
#property (nonatomic) CGFloat outerRadiusFraction;
#property (nonatomic) CGFloat minorInnerRadiusFraction;
#property (nonatomic) CGFloat majorInnerRadiusFraction;
- (instancetype _Nonnull)init;
- (void)drawInRect:(CGRect)rect;
#end
And the implementation:
#implementation RainbowGaugeAppearance
static CGFloat radiansForDegrees(CGFloat degrees) { return degrees * M_PI / 180; }
- (instancetype _Nonnull)init {
if (self = [super init]) {
_startDegrees = 120;
_endDegrees = _startDegrees + 300;
_degreesPerMajorTick = 30;
_subdivisionsPerMajorTick = 10;
_tickThickness = 4;
_outerRadiusFraction = 0.95;
_minorInnerRadiusFraction = 0.87;
_majorInnerRadiusFraction = 0.81;
_startHue = 1/ 3.0;
_endHue = 0;
}
return self;
}
- (void)drawInRect:(CGRect)rect {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextTranslateCTM(gc, CGRectGetMidX(rect), CGRectGetMidY(rect));
CGContextSetLineWidth(gc, self.tickThickness);
CGContextSetLineCap(gc, kCGLineCapButt);
CGFloat outerRadius = _outerRadiusFraction / 2 * rect.size.width;
CGFloat minorInnerRadius = _minorInnerRadiusFraction / 2 * rect.size.width;
CGFloat majorInnerRadius = _majorInnerRadiusFraction / 2 * rect.size.width;
CGFloat degreesPerTick = _degreesPerMajorTick / _subdivisionsPerMajorTick;
for (int i = 0; ; ++i) {
CGFloat degrees = _startDegrees + i * degreesPerTick;
if (degrees > _endDegrees) { break; }
CGFloat t = (degrees - _startDegrees) / (_endDegrees - _startDegrees);
CGFloat hue = _startHue + t * (_endHue - _startHue);
CGContextSetStrokeColorWithColor(gc, [UIColor colorWithHue:hue saturation:0.8 brightness:1 alpha:1].CGColor);
CGFloat sine = sin(radiansForDegrees(degrees));
CGFloat cosine = cos(radiansForDegrees(degrees));
CGFloat innerRadius = (i % _subdivisionsPerMajorTick == 0) ? majorInnerRadius : minorInnerRadius;
CGContextMoveToPoint(gc, outerRadius * cosine, outerRadius * sine);
CGContextAddLineToPoint(gc, innerRadius * cosine, innerRadius * sine);
CGContextStrokePath(gc);
}
} CGContextRestoreGState(gc);
}
#end
Using it to draw a view is then trivial:
#implementation RainbowGaugeView {
RainbowGaugeAppearance *_appearance;
}
- (RainbowGaugeAppearance *_Nonnull)appearance {
if (_appearance == nil) { _appearance = [[RainbowGaugeAppearance alloc] init]; }
return _appearance;
}
- (void)drawRect:(CGRect)rect {
[self.appearance drawInRect:self.bounds];
}
#end
As far as I'm concerned, I should simply be able to call CGContextSetStrokeColorWithColor
Reality, however, is not interested in "as far as you're concerned". You are describing an angle gradient. The reality is that there is no built-in Core Graphics facility for creating an angle gradient.
However, you can do it easily with a good library such as AngleGradientLayer. It is then a simple matter to draw the angle gradient and use your gauge drawing as a mask.
In that way, I got this — not kludged in Photoshop, but done entirely live, in iOS, using AngleGradientLayer, plus your radials:andSteps:andLineWidth: method just copied and pasted in and used to generate the mask:
Here's the only code I had to write. First, generating the angle gradient layer:
+ (Class)layerClass {
return [AngleGradientLayer class];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
AngleGradientLayer *l = (AngleGradientLayer *)self.layer;
l.colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithRed:1 green:0 blue:0 alpha:1].CGColor,
(id)[UIColor colorWithRed:0 green:1 blue:0 alpha:1].CGColor,
nil];
l.startAngle = M_PI/2.0;
}
return self;
}
Second, the mask (this part is in Swift, but that's irrelevant):
let im = self.v.radials(300, andSteps: 3, andLineWidth: 10)
let iv = UIImageView(image:im)
self.v.mask = iv
Related
The unusual bottom corners of an iPhoneX are Apple's new (2017) "continuous corners for iPhoneX".
It is trivial for any experienced iOS programmer to approximate the curve, but:
Does anyone know exactly how to achieve these, exactly as Apple does?
Even if it's a private call, it would be good to know.
It does seem bizarre that Apple have not explained this.
Please note that it's trivial to "approximate" the curve:
To repeat,
it is trivial for any experienced iOS programmer to approximate the curve.
The question being asked here is specifically how to do Apple actually do it?
Please do not post any more answers showing beginners how to draw a curve and approximate the iPhone curve.
As of iOS 13, there's an API available for this:
https://developer.apple.com/documentation/quartzcore/calayercornercurve
See CALayerCornerCurve.continuous
I wrote an experimental class which constructs a bezier path which overlaps the border of a CALayer due to #Aflah Bhari's comments. The layer has set its private property continuousCornersto YES. This is the result:
The border of the layer is blue while the color of the path is red.
Here is the code. You can set radius and insets in attribute inspector of Interface Builder. I have created the image above by setting the class of the view controllers view to ArcView, its radius to 30.0 and the insets to (20.0, 20.0).
Here is the code:
ArcView.h
IB_DESIGNABLE
#interface ArcView : UIView
#property(nonatomic) IBInspectable CGFloat radius;
#property(nonatomic) IBInspectable CGSize insets;
#end
ArcView.m
#import "ArcView.h"
#interface CALayer(Private)
#property BOOL continuousCorners;
#end
#interface ArcView()
#property (strong) CALayer *borderLayer;
#end
#implementation ArcView
- (void)setRadius:(CGFloat)inRadius {
if(_radius != inRadius) {
_radius = inRadius;
self.borderLayer.cornerRadius = inRadius;
[self setNeedsDisplay];
}
}
- (void)setInsets:(CGSize)inInsets {
if(!CGSizeEqualToSize(_insets, inInsets)) {
_insets = inInsets;
[self setNeedsLayout];
[self setNeedsDisplay];
}
}
- (void)awakeFromNib {
[super awakeFromNib];
self.borderLayer = [CALayer new];
self.borderLayer.borderColor = [[UIColor blueColor] CGColor];
self.borderLayer.borderWidth = 0.5;
self.borderLayer.continuousCorners = YES;
self.borderLayer.cornerRadius = self.radius;
[self.layer addSublayer:self.borderLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.borderLayer.frame = CGRectInset(self.bounds, self.insets.width, self.insets.height);
}
- (void)drawRect:(CGRect)rect {
CGFloat theRadius = self.radius;
CGFloat theOffset = 1.2 * theRadius;
CGRect theRect = CGRectInset(self.bounds, self.insets.width, self.insets.height);
UIBezierPath *thePath = [UIBezierPath new];
CGPoint thePoint;
[thePath moveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect))];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMinY(theRect))];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect) + theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect) - theOffset)];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMaxY(theRect)) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMaxY(theRect))];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect) - theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect) + theOffset)];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect)) controlPoint:thePoint];
thePath.lineWidth = 0.5;
[[UIColor redColor] set];
[thePath stroke];
}
#end
I hope this helps you with your problem. I've found the factor of 1.2 for theOffset through experiments. You might modify this value if necessary. The value I have chosen for the radius is not optimal and can certainly be improved. But since it depends on the exact distance from the rim, I didn't invest much time for it.
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.minimumValue)) * 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.
I am using the following core graphics code to draw a waveform of audio data as I am recording. I apologize for tons of code, but here it is (I found it here):
//
// WaveformView.m
//
// Created by Edward Majcher on 7/17/14.
//
#import "WaveformView.h"
//Gain applied to incoming samples
static CGFloat kGain = 10.;
//Number of samples displayed
static int kMaxWaveforms = 80.;
#interface WaveformView ()
#property (nonatomic) BOOL addToBuffer;
//Holds kMaxWaveforms number of incoming samples,
//80 is based on half the width of iPhone, adding a 1 pixel line between samples
#property (strong, nonatomic) NSMutableArray* bufferArray;
+ (float)RMS:(float *)buffer length:(int)bufferSize;
#end
#implementation WaveformView
- (void)awakeFromNib
{
[super awakeFromNib];
self.bufferArray = [NSMutableArray array];
}
-(void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize
{
if (!self.addToBuffer) {
self.addToBuffer = YES;
return;
} else {
self.addToBuffer = NO;
}
float rms = [WaveformView RMS:buffer length:bufferSize];
if ([self.bufferArray count] == kMaxWaveforms) {
//##################################################
// [self.bufferArray removeObjectAtIndex:0];
}
[self.bufferArray addObject:#(rms * kGain)];
[self setNeedsDisplay];
}
+ (float)RMS:(float *)buffer length:(int)bufferSize {
float sum = 0.0;
for(int i = 0; i < bufferSize; i++) {
sum += buffer[i] * buffer[i];
}
return sqrtf( sum / bufferSize );
}
// *****************************************************
- (void)drawRect:(CGRect)rect
{
CGFloat midX = CGRectGetMidX(rect);
CGFloat maxX = CGRectGetMaxX(rect);
CGFloat midY = CGRectGetMidY(rect);
CGContextRef context = UIGraphicsGetCurrentContext();
// Draw out center line
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 1.);
CGContextMoveToPoint(context, 0., midY);
CGContextAddLineToPoint(context, maxX, midY);
CGContextStrokePath(context);
CGFloat x = 0.;
for (NSNumber* n in self.bufferArray) {
CGFloat height = 20 * [n floatValue];
CGContextMoveToPoint(context, x, midY - height);
CGContextAddLineToPoint(context, x, midY + height);
CGContextStrokePath(context);
x += 2;
}
if ([self.bufferArray count] >= kMaxWaveforms) {
[self addMarkerInContext:context forX:midX forRect:rect];
} else {
[self addMarkerInContext:context forX:x forRect:rect];
}
}
- (void)addMarkerInContext:(CGContextRef)context forX:(CGFloat)x forRect:(CGRect)rect
{
CGFloat maxY = CGRectGetMaxY(rect);
CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, 0, 3, 3));
CGContextMoveToPoint(context, x, 0 + 3);
CGContextAddLineToPoint(context, x, maxY - 3);
CGContextStrokePath(context);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, maxY - 3, 3, 3));
}
#end
So as I am recording audio, the waveform drawing gets more and more jittery, kind of like a game that has bad frame rates. I tried contacting the owner of this piece of code, but no luck. I have never used core graphics, so I'm trying to figure out why performance is so bad. Performance starts to degrade at around 2-3 seconds worth of audio (the waveform doesn't even fill the screen).
My first question is, is this redrawing the entire audio history every time drawRect is called? If you look in the drawRect function (marked by asterisks), there is a variable called CGRect x. This seems to affect the position at which the waveform is being drawn (if you set it to 60 instead of 0, it starts at x=60 pixels instead of x=0 pixels).
From my viewController, I pass in the audioData which gets stored in the self.bufferArray property. So when that loop goes through to draw the data, it seems like it's starting at zero and working its way up every time drawRect is getting called, which means that for every new piece of audio data added, drawRect gets called, and it redraws the entire waveform plus the new piece of audio data.
If that is the problem, does anyone know how I can optimize this piece of code? I tried emptying the bufferArray after the loop so that it contained only new data, but that didn't work.
If this is not the problem, are there any core graphics experts that can figure out what the problem is?
I should also mention that I commented out a piece of code (marked with ### at signs) because I need the entire waveform. I don't want it to remove pieces of the waveform at the beginning. The iOS Voice Memos app can hold a waveform of audio without performance degradation.
I'm trying to create a UIView in UIScrollView that contains just a simple grid (lines as rows and columns) drown by UIBezierPath or using the CG functions. The problem is, that when I have larger content size of the UIScrollView (as well as the larger subview), during the drawing of the grid huge amount of memory is allocated (50MB or more).
UIViewController which includes just UIScrollView over whole scene - adding subview in viewDidLoad:
#interface TTTTestViewController()
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
#end
#implementation TTTTestViewController
-(void)viewDidLoad
{
[super viewDidLoad];
// create the subview
TTTTestView *testView = [[TTTTestView alloc] init];
[self.scrollView addSubview:testView];
//set its properties
testView.cellSize = 50;
testView.size = 40;
// set the content size and frame of testView by the properties
self.scrollView.contentSize = CGSizeMake(testView.cellSize * testView.size, testView.cellSize * testView.size);
testView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height);
// let it draw the grid
[testView setNeedsDisplay];
}
#end
Inner view that just draw the grid using UIBezierPath/CG functions - depends on properties size(rows/columns count) and cellSize (width/height of one cell in grid):
#define GRID_STROKE_WIDTH 2.0
#implementation TTTTestView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[self drawGrid];
}
-(void)drawGrid
{
UIBezierPath *path = [[UIBezierPath alloc] init];
for (int i = 1; i < self.size; i++) {
//draw row line
[path moveToPoint:CGPointMake(0, self.cellSize * i)];
[path addLineToPoint:CGPointMake(self.bounds.size.width, self.cellSize * i)];
// draw column line
[path moveToPoint:CGPointMake(self.cellSize * i, 0)];
[path addLineToPoint:CGPointMake(self.cellSize * i , self.bounds.size.height)];
}
[path setLineWidth:GRID_STROKE_WIDTH];
[[UIColor blackColor] setStroke];
[path stroke];
/*
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, GRID_STROKE_WIDTH);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
for (int i = 1; i < self.size; i++) {
//draw row line
CGContextMoveToPoint(context, 0, self.cellSize * i );
CGContextAddLineToPoint(context, self.bounds.size.width, self.cellSize * i);
// draw column line
CGContextMoveToPoint(context, self.cellSize * i , 0);
CGContextAddLineToPoint(context, self.cellSize * i , self.bounds.size.height);
}
CGContextStrokePath(context);
*/
}
#end
Example1: self.size is 10, self.cellSize is 200 => contentSize is 2000x2000 points as well as frame of inner view => 18 lines are drown and it allocates ~60MB memory
Example2: self.size is 30, self.cellSize is 70 => contentSize is 2100x2100 points as well as frame of inner view => 58 lines are drown and it allocates ~67MB memory
These memory numbers I can see when debug the drawing method. No matter how I draw the lines, huge amount of memory is allocated when calling [path stroke] resp. CGContextStrokePath(context). In instruments I can see the biggest memory allocation at line:
12658 0x10200000 VM: CoreAnimation 00:04.092.149 • 67,29 MB QuartzCore CA::Render::Shmem::new_shmem(unsigned long)
I'm quite new in iOS programming and I was searching the solution everywhere and I still have no idea :-/ Can anyone please help me find some explanation what is going on here? Thanks :)
After asking on apple developer forum, I find out, that this is properly allocated memory in fact. It's because any view that uses -drawRect: to draw will use memory on the order of (bounds.size.width * bounds.size.height * contentScale * contentScale * 4) bytes.
The simplest way to create a grid that avoids that is to use add a view for each line and use the view's backgroundColor property to color the view. This will use hardly any memory because the view's (which can be plain UIViews) don't need to call -drawRect:, and thus won't use extra memory to store the results of your drawing.
I want to make UBersense like app (http://blog.ubersense.com/2013/01/03/how-to-use-the-drawing-tools-in-ubersense/), there i need to draw two line with some angle, after that i can adjust the angle between two line by dragging any line or intersection point.
can you guys please provide me some idea or code snippet.
screenshots url:
https://picasaweb.google.com/yunusm7/AppScreenshots#slideshow/5952787957718627714
Thanks in advance.
You have a construction with three points, one point is an angle point, and two others are just vertices. First of all you should create a new class like this:
#interface MyAngle : NSObject {
}
#property (nonatomic) CGPoint p1;
#property (nonatomic) CGPoint p2;
#property (nonatomic) CGPoint v; // this is an angle point
#end
You can use the default implementation of this without any tricks with such sample init:
- (id)init {
if (self = [super init]) {
p1 = CGPointMake(1,0);
p2 = CGPointMake(0,1);
v = CGPointZero;
}
return self;
}
But also as I understood you need to know the value of the angle. You can do this using the following way:
- (CGFloat)valueOfAngle {
CGPoint v1 = CGPointMake(p1.x-v.x, p1.y-v.y);
CGPoint v2 = CGPointMake(p2.x-v.x, p2.y-v.y);
CGFloat scalarProduct = v1.x*v2.x + v1.y*v2.y;
CGFloat lengthProduct = sqrt(v1.x*v1.x + v1.y*v1.y)*sqrt(v2.x*v2.x + v2.y*v2.y);
CGFloat fraction = scalarProduct / lengthProduct;
if (fraction < -1) fraction = -1;
if (fraction > 1) fraction = 1;
return acos(fraction);
}
If you want to obtain angles more than 180 degrees you should change the code above a little. But there are too much information about how to do this in the Internet, so I will skip this part.
Then you need to create an instance of MyAngle in your viewController. Let it be called "angle". Knowing coordinates of every three points if enough do draw it (!!!). Implement drawRect method in a view that will contain the MyAngle instance (I strongly recommend do to this on your own subclass of UIView):
- (void)drawRect {
[super drawRect];
// set options of drawing
CGContextRef c = UIGraphicsGetCurrentContext();
CGFloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
CGContextSetLineWidth(c, 3.0);
CGContextSetStrokeColor(c, red);
// draw an angle directly
CGContextBeginPath(c);
CGContextMoveToPoint(c, angle.p1.x, angle.p1.y);
CGContextAddLineToPoint(c, angle.v.x, angle.v.y);
CGContextAddLineToPoint(c, angle.p2.x, angle.p2.y);
CGContextStrokePath(c);
// draw circles around vertices (like on the screenshot you provided)
CGFloat R = 7.0f;
CGContextBeginPath(c);
CGContextAddEllipseInRect(c, CGRectMake(angle.p1.x - R, angle.p1.y - R, 2*R, 2*R));
CGContextStrokePath(c);
CGContextBeginPath(c);
CGContextAddEllipseInRect(c, CGRectMake(angle.p2.x - R, angle.p2.y - R, 2*R, 2*R));
CGContextStrokePath(c);
CGContextBeginPath(c);
CGContextAddEllipseInRect(c, CGRectMake(angle.v.x - R, angle.v.y - R, 2*R, 2*R));
CGContextStrokePath(c);
}
And that's all you need to know for drawing what you want! You can change the stroke color or radius of three circles if you want.
Then you need to have a possibility to change the locations of your angle's points. For this you can just implement panGestureRecognizer in your viewController's viewDidLoad method like this:
UIPanGestureRecognizer *pan = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(moveAngle:)] autorelease];
pan.delegate = self;
[self.view addGestureRecognizer:pan];
Implement UIGestureRecognizerDelegate method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
CGPoint p = [gestureRecognizer locationInView:self.view];
CGFloat d1 = sqrt((p.x-angle.p1.x)*(p.x-angle.p1.x) + (p.y-angle.p1.y)*(p.y-angle.p1.y);
CGFloat d2 = sqrt((p.x-angle.p2.x)*(p.x-angle.p2.x) + (p.y-angle.p2.y)*(p.y-angle.p2.y);
CGFloat d3 = sqrt((p.x-angle.v.x)*(p.x-angle.v.x) + (p.y-angle.v.y)*(p.y-angle.v.y);
// just check if we touched the screen near some of angle's points
CGFloat tolerance = 15.0f;
return (d1 < tolerance) || (d2 < tolerance) || (d3 < tolerance);
}
return YES;
}
and tagret's selector (also in your viewController):
- (void)moveAngle:(UIPanGestureRecognizer *)gr {
CGPoint p = [gr locationInView:self.view];
if (gr.state == UIGestureRecognizerStateBegan) {
CGFloat d1 = sqrt((p.x-angle.p1.x)*(p.x-angle.p1.x) + (p.y-angle.p1.y)*(p.y-angle.p1.y);
CGFloat d2 = sqrt((p.x-angle.p2.x)*(p.x-angle.p2.x) + (p.y-angle.p2.y)*(p.y-angle.p2.y);
CGFloat d3 = sqrt((p.x-angle.v.x)*(p.x-angle.v.x) + (p.y-angle.v.y)*(p.y-angle.v.y);
// pointToMove is your int variable
CGFloat tolerance = 15.0f;
if (d1 < tolerance) {
pointToMove = 1;
}
else if (d2 < tolerance) {
pointToMove = 2;
}
else {
pointToMove = 3;
}
}
else {
if (pointToMove == 1) {
angle.p1 = loc;
}
else if (pointToMove == 2) {
angle.p2 = loc;
}
else {
angle.v = loc;
}
[yourCustomView setNeedsDisplay];
[yourLabel setText:[NSString stringWithFormat:#"%.3f", [angle valueOfangle]*180/PI]];
}
}
Maybe I skip some evident things, but I think it should be enough for you to begin writing some code.