I am totally new to iOS developing and I've got an idea in my mind but I do not know exactly how to implement it correctly.
What I need is my program to draw a line which can be controlled by the user by tapping buttons. It is kind of like the "snake" game. I tried out core graphics but it is not quite the right approach I guess. I did following:
- (void)viewDidLoad{
[NSTimer scheduledTimerWithTimeInterval:.02 target:self selector:#selector(updateGame:) userInfo:nil repeats:YES];
}
-(void)updateGame:(id)sender{
double timeInterval= [self.lastDate timeIntervalSinceNow]*-1;
for (int n=0; n<playerNum; n++) {
CGPoint lastPoint=[[locationArray objectAtIndex:n]CGPointValue];
CGPoint updatedLoc= CGPointMake(lastPoint.x+100*timeInterval*sin([[directionArray objectAtIndex:n]doubleValue]), lastPoint.y+60*timeInterval*cos([[directionArray objectAtIndex:n]doubleValue]));
[locationArray replaceObjectAtIndex:n withObject:[NSValue valueWithCGPoint:updatedLoc]];
[self.drawingView drawToBufferFrom:lastPoint to:updatedLoc withColor:[colorArray objectAtIndex:n]];
}
self.lastDate=[NSDate date];
}
In DrawingView.m
-(void)drawToBufferFrom:(CGPoint)lastLoc to:(CGPoint)currentLoc withColor:(UIColor *)color{
//[color setStroke];
CGContextSetStrokeColorWithColor(offScreenBuffer, color.CGColor);
//CGContextSetRGBStrokeColor(offScreenBuffer, 1, 0, 1, 1);
CGContextBeginPath(offScreenBuffer);
CGContextSetLineWidth(offScreenBuffer, 10);
CGContextSetLineCap(offScreenBuffer, kCGLineCapRound);
CGContextMoveToPoint(offScreenBuffer, lastLoc.x, lastLoc.y);
CGContextAddLineToPoint(offScreenBuffer, currentLoc.x, currentLoc.y);
CGContextDrawPath(offScreenBuffer, kCGPathStroke);
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGImageRef cgImage= CGBitmapContextCreateImage(offScreenBuffer);
UIImage* screenImage= [[UIImage alloc]initWithCGImage:cgImage];
CGImageRelease(cgImage);
[screenImage drawInRect:self.bounds];
}
The direction changes by the user tapping on either side of the screen.
However what I am facing is: Major lags on the device itself so I think there needs to be an easier way to draw those lines without any lags.
UIBezierPath is easier, but a better way especially if you want to include overlays or sprites would be cocos2d or similar game dev framework...
Related
I would like to draw a "disappearing stroke" on a UIImageView, which follows a touch event and self-erases after a fixed time delay. Here's what I have in my ViewController.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
CGPoint lp = lastPoint;
UIColor *color = [UIColor blackColor];
[self drawLine:5 from:lastPoint to:currentPoint color:color blend:kCGBlendModeNormal];
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self drawLine:brush from:lp to:currentPoint color:[UIColor clearColor] blend:kCGBlendModeClear];
});
lastPoint = currentPoint;
}
- (void)drawLine:(CGFloat)width from:(CGPoint)from to:(CGPoint)to color:(UIColor*)color blend:(CGBlendMode)mode {
UIGraphicsBeginImageContext(self.view.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.tempDrawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, width);
CGContextSetStrokeColorWithColor(context, [color CGColor]);
CGContextSetBlendMode(context, mode);
CGContextStrokePath(context);
self.tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext();
[self.tempDrawImage setAlpha:1];
UIGraphicsEndImageContext();
}
The draw phase works nicely, but there are a couple problems with the subsequent erase phase.
While the line "fill" is correctly cleared, a thin stroke around the path remains.
The "erase phase" is choppy, nowhere near as smooth as the drawing phase. My best guess is that this is due to the cost of UIGraphicsBeginImageContext run in dispatch_after.
Is there a better approach to drawing a self-erasing line?
BONUS: What I'd really like is for the path to "shrink and vanish." In other words, after the delay, rather than just clearing the stroked path, I'd like to have it shrink from 5pt to 0pt while simultaneously fading out the opacity.
I would just let the view draw continuously with 60 Hz, and each time draw the entire line using points stored in an array. This way, if you remove the oldest points from the array, they will not be drawn anymore.
to hook up your view to display refresh rate (60 Hz), try this:
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(update)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
Store an age property along with each point, then just loop over the array and remove points which are older than your threshold.
e.g.
#interface AgingPoint <NSObject>
#property CGPoint point;
#property NSTimeInterval birthdate;
#end
// ..... later, in the draw call
NSTimeInterval now = CACurrentMediaTime();
AgingPoint *p = [AgingPoint new];
p.point = touchlocation; // get yr touch
p.birthdate = now;
// remove old points
while(myPoints.count && now - [myPoints[0] birthdate] > 1)
{
[myPoints removeObjectAtIndex: 0];
}
myPoints.add(p);
if(myPoints.count < 2)
return;
UIBezierPath *path = [UIBezierPath path];
[path moveToPoint: [myPoints[0] point]];
for (int i = 1; i < myPoints.count; i++)
{
[path lineToPoint: [myPoints[i] point];
}
[path stroke];
So on each draw call, make a new bezierpath, move to the first point, then add lines to all other points. Finally, stroke the line.
To implement the "shrinking" line, you could draw just short lines between consecutive pairs of points in your array, and use the age property to calculate stroke width. This is not perfect, as the individual segments will have the same width at start and end point, but it's a start.
Important: If you are going to draw a lot of points, performance will become an issue. This kind of path rendering with Quartz is not exactly tuned to render real fast. In fact, it is very, very slow.
Cocoa arrays and objects are also not very fast.
If you run into performance issues and you want to continue this project, look into OpenGL rendering. You will be able to have this run a lot faster with plain C structs pushed into your GPU.
There were a lot of great answers here. I think the ideal solution is to use OpenGL, as it'll inevitably be the most performant and provide the most flexibility in terms of sprites, trails, and other interesting visual effects.
My application is a remote controller of sorts, designed to simply provide a small visual aid to track motion, rather than leave persistent or high fidelity strokes. As such, I ended up creating a simple subclass of UIView which uses CoreGraphics to draw a UIBezierPath. I'll eventually replace this quick-fix solution with an OpenGL solution.
The implementation I used is far from perfect, as it leaves behind white paths which interfere with future strokes, until the user lifts their touch, which resets the canvas. I've posted the solution I used here, in case anyone might find it helpful.
I have been struggling with this problem for over one month trying to to figure out what is causing it with no solution. Since the code is pretty long i wouldn't be able to post it here.
Basically i have made drawing app. When you dubble tap the screen the screen and everything will reset, almost like I am reloading the view. When i reset the scene the processor usage will go down to around 9%, but then when i start drawing again the processor usage will go up to where I last ended. So say for example i draw and image and the processor power goes up to 50%, then dubble tap to reset the view to what is what from the beginning it will go down to 9%. Then when i start drawing again it will go up to 50%, and next time 60%,70% etc.
Maybe it is hard for you to see what is causing the problem due the lack of information so I could send my source code if someone is interested helping me by PMing me.
greentimer = [NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:#selector(movement2) userInfo:nil repeats:YES];
-(void)movement2{
static int intigrer;
intigrer = (intigrer+1)%3;
UIGraphicsBeginImageContext(CGSizeMake(320, 568));
[drawImage.image drawInRect:rekked];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 8.0);
CGContextSetRGBStrokeColor(ctx, r12, g12, b12, 1);
CGContextBeginPath(ctx);
if (intigrer == 1 && integrer2 < greenran - greenran2) {
CGPathMoveToPoint(path, NULL, greentmporary.x, greentmporary.y);
CGPathAddLineToPoint(path, NULL, greenpoint1.x, greenpoint1.y);
}
green.center = greenpoint1;
if (integrer2 < greenran - greenran2) {
CGContextMoveToPoint(ctx, greentmporary.x, greentmporary.y);
CGContextAddLineToPoint(ctx, greenpoint1.x, greenpoint1.y);
}
CGContextStrokePath(ctx);
[drawImage setFrame:CGRectMake(0, 0, 320, 568)];
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// [self updatePoint2:YES];
static BOOL yes;
if (!yes) {
[self.view insertSubview:drawImage atIndex:0];
yes = YES;
}
ctx = nil;
}
You need to release your CGPaths and CGImages
CGPathRelease(path);
CGImageRelease(image);
CGContextRelease(context);
I think your problem is that you are making self.view more complex by each fire of the timer because you keep adding subviews to it. So the complexity of your scene increases each time its rendered until its completely reset. I could not really follow all your code, to be honest, as I am not familiar with what you are trying to achieve.
I think an approach to solving the problem is to run your program with Instruments with the Profile option (instead of doing a 'Run'). Select the 'Automation' template.
There is a way to issue logElementTree() on your running program and it gives a dump of the UIView hierarchy with images. There are lots of good articles on it, e.g.
http://cocoamanifest.net/articles/2011/05/uiautomation-an-introduction.html
This seems like such a basic thing to want, I can't believe I'm not able to find out how to do it. To make the description easy to understand, suppose I simply want to draw a bunch of random rectangles on the screen. These random rectangles would keep adding on top of each other repeatedly until something stopped the process. How would one do that?
The closest explanation I've seen is drawing applications, where the basic scheme is to draw into an image view, first copying the previous image into the new image and then adding the new content. Copying the original image sure seems like a waste of effort, and it sure seems like it should be possible to simply write the new content in place over whatever is there. Am I missing something obvious?
Note that drawRect replaces the entire frame. It works well for drawing a small set of objects, but it quickly becomes awkward when there's an indefinite amount of history that also needs to be displayed.
Edit: I'm attaching some sample images that are screen prints from a Mix C program that does what I'm after. Essentially, there are cellular automata that move around the screen leaving trails. The color of the trail depends upon the logic in the automaton as well as the color of the pixel where the automaton just traveled to. The automata should be able to move at rates of hundreds of pixels per second. Because of the logic used by the automata, I need to be able to not only write quickly to the image but also be able to inquire what the color of a pixel is (or mirrored data).
Typically you do this by either creating separate paths or layers for all your rectangles (if you want to keep track of them), or by drawing repeatedly into a CGBitmapContextRef, and then converting that into an image and drawing it in drawRect:. This is basically the same approach you're describing ("where the basic scheme is to draw into an image view…") except there's no need to copy the image. You just keep using the same context and making new images out of it.
The other tool you could use here is a CGLayer. The Core Graphics team discourages its use because of performance concerns, but it does make this kind of drawing much more convenient. When you look at the docs, and they say "benefit from improved performance," remember that this was written in 2006, and when I asked the Core Graphics team about it, they said that the faster answer today is CGBitmapContext. But you can't beat CGLayer for convenience on this kind of problem.
This should be fine by maintaining a CGBitmapContext that you continually write into (and that allows you to also read from it). When it changes, call setNeedsDisplayInRect:. In drawRect:, create the image, and draw it using CGContextDrawImage, passing the rect you were passed. (You may be passed the entire rect.)
It may be a little more flexible to do this on the CALayer instead of the UIView, but I doubt you'll see a great difference in performance. The view passes drawing to its layer.
The number of times a second this updates isn't really that important. drawRect: will not be called more often than the frame rate (max of 60 fps), no matter how often you call setNeedsDisplayInRect:. So you won't be creating images hundreds or thousands of times a second; just at the time that you need to draw something.
Are you seeing particular performance problems, or are you just concerned that you may in the future encounter performance problems? Do you have any sample code that shows the issue? I'm not saying it can't be slow (it might be if you're trying to do this full screen with retina). But you want to start with the simple solution and then optimize. Apple does a lot of graphics optimizations behind the scenes. Don't try to second guess them too much. They generate and draw images really well.
I've accepted another answer, but I'm including my own answer to show the alternative I used for testing.
-(void)viewWillLayoutSubviews {
self.pixelCount = 0;
self.seconds = 0;
CGRect frame = self.testImageView.bounds;
UIGraphicsBeginImageContextWithOptions(frame.size, YES, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.0);
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextMoveToPoint(context, 0.0, 0.0);
CGContextAddRect(context, frame);
CGContextFillRect(context, frame);
CGContextStrokePath(context);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextStrokePath(context);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
self.context = context;
self.testImageView.image = blank;
// This timer has no delay, so it will draw squares as fast as possible.
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(drawRandomRectangle) userInfo:nil repeats:NO];
// This timer is used to control the frequency at which the imageViews image is updated.
[NSTimer scheduledTimerWithTimeInterval:1/20.f target:self selector:#selector(updateImage) userInfo:nil repeats:YES];
// This timer just outputs the counter once per second so I can record statistics.
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(displayCounter) userInfo:nil repeats:YES];
}
-(void)updateImage
{
self.testImageView.image = UIGraphicsGetImageFromCurrentImageContext();
}
-(void)displayCounter
{
self.seconds++;
NSLog(#"%d",self.pixelCount/self.seconds);
}
-(void)drawRandomRectangle
{
int x1 = arc4random_uniform(self.view.bounds.size.width);
int y1 = arc4random_uniform(self.view.bounds.size.height);
int xdif = 20;
int ydif = 20;
x1 -= xdif/2;
y1 -= ydif/2;
CGFloat red = (arc4random() % 256) / 255.0f;
CGFloat green = (arc4random() % 256) / 255.0f;
CGFloat blue = (arc4random() % 256) / 255.0f;
UIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
CGRect frame = CGRectMake(x1*1.0f, y1*1.0f, xdif*1.0f, ydif*1.0f);
CGContextSetStrokeColorWithColor(self.context, [UIColor blackColor].CGColor);
CGContextSetFillColorWithColor(self.context, randomColor.CGColor);
CGContextSetLineWidth(self.context, 1.0);
CGContextAddRect(self.context, frame);
CGContextStrokePath(self.context);
CGContextFillRect(self.context, frame);
CGContextStrokePath(self.context);
if (self.pixelCount < 100000) {
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(drawRandomRectangle) userInfo:nil repeats:NO];
}
self.pixelCount ++;
}
The graph shows image updates per second on the x-axis and number of 20x20 squares drawn to the context per second in the y-axis.
I see a sharp increase in memory usage (from 39 MB to 186 MB on iPad) with "CGContextFillRect" statement execution in my below code. Is there something wrong here.
My application eventually crashes.
PS: Surprisingly the memory spike is seen on 3rd and 4th gen iPads and not on 2nd Gen iPad.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
}
return self;
}
- (id)initWithFrame:(CGRect)iFrame andHollowCircles:(NSArray *)iCircles {
self = [super initWithFrame:iFrame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
self.circleViews = iCircles;
}
return self;
}
- (void)drawHollowPoint:(CGPoint)iHollowPoint withRadius:(NSNumber *)iRadius {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(currentContext, self.circleRadius.floatValue);
[[UIColor whiteColor] setFill];
CGContextAddArc(currentContext, iHollowPoint.x, iHollowPoint.y, iRadius.floatValue, 0, M_PI * 2, YES);
CGContextFillPath(currentContext);
}
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGRect aRect = [self.superview bounds];
[[UIColor whiteColor]setFill];
CGContextFillRect(currentContext, aRect);
CGContextSaveGState(currentContext);
[[UIColor blackColor]setFill];
CGContextFillRect(currentContext, aRect);
CGContextRestoreGState(currentContext);
for (MyCircleView *circleView in self.circleViews) {
[self drawHollowPoint:circleView.center withRadius:circleView.circleRadius];
}
CGContextTranslateCTM(currentContext, 0, self.bounds.size.height);
CGContextScaleCTM(currentContext, 1.0, -1.0);
CGContextSaveGState(currentContext);
}
This code doesn't quite make sense; I assume you've removed parts of it? You create a blank alpha mask and then throw it away.
If the above code is really what you're doing, you don't really need to draw anything. You could just create a 12MB memory area and fill it with repeating 1 0 0 0 (opaque black in ARGB) and then create an image off of that. But I assume you're actually doing more than that.
Likely you have this view configured with contentScaleFactor set to match the scale from UIScreen, and this view is very large. 3rd and 4th gen iPads have a Retina display, so the scale is 2, and the memory required to draw a view is 4x as large.
That said, you should only expect about 12MB to hold a full screen image (2048*1536*4). The fact that you're seeing 10x that suggests something more is going on, but I suspect that it's still related to perhaps drawing too many copies.
If possible, you can step the scale down to 1 to make retina and non-retina behave the same.
EDIT:
Your edited code is very different from your original code. There's no attempt to make an image in this code. I've tested it out as best I can, and I don't see any surprising memory spike. But there are several oddities:
You're not correctly balancing CGContextSaveGState with CGContextRestoreGState. That actually might cause a memory problem.
Why are you drawing the rect all in white and then all in black?
Your rect is [self.superview bounds]. That's in the wrong coordinate space. You should almost certainly mean [self bounds].
Why do you flip the context right before returning from drawRect and then save the context? This doesn't make sense at all.
I would assume your drawRect: would look like this:
- (void)drawRect:(CGRect)rect {
[[UIColor blackColor] setFill];
UIRectFill(rect); // You're only responsible for drawing the area given in `rect`
for (CircleView *circleView in self.circleViews) {
[self drawHollowPoint:circleView.center withRadius:circleView.circleRadius];
}
}
I am trying to make a custom animated bar graph for an iPad application (i.e., bar height increases to set level when activated). I am quite new to iOS development and I just want to get feedback on how to approach this task.
I am trying to play around with the answer in this entry and I want to know if it's right that I'm starting from this point.
If you just want a solid bar, you can create a UIView of the size and placement that you need, set its background color, and add it to your view. This is decent coding, no shame in using a UIView to draw solid rectangles. :]
For more complicated graphics, you might want to create a custom subclass of UIView and override its drawRect message to do some custom drawing. For example:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 4.0);
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 0, 1.0); // opaque yellow
CGContextMoveToPoint(context, x1, y1); // for suitable definition of x1,y1, etc
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);
}
or whatever other CGContext* sort of drawing you might want to do (e.g. pie charts, line charts, etc).
To animate a bar that you create by adding a UIView with a background color, stick the following whenever the animation starts:
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
self.startTime = [NSDate date];
and then add the following message (note: the bar will grow upwards).
- (void) onTimer:(NSTimer*)firedTimer
{
float time = [self.startTime timeIntervalSinceNow] * -1;
if (time>kMaxTime)
{
[timer invalidate];
timer = nil;
time = kMaxTime;
}
int size = time * kPixelsPerSecond;
myBar.frame = CGRectMake(x, y - size, width, size);
}
idk about that link, but you can generate them from here http://preloaders.net/ that should give you a good base to make your own