I have subclassed an UILabel and i use drawrect method to draw the string on the screen. This is my code:
- (void)drawRect:(CGRect)rect
{
CGRect aRect = self.frame;
NSMutableArray *Text=[globaltextarray Text];
[[NSString stringWithCString:[[Text objectAtIndex:labelindexpath.row] cStringUsingEncoding:NSISOLatin1StringEncoding] encoding:NSUTF8StringEncoding] drawInRect:aRect
withFont:[UIFont fontWithName:#"Times New Roman" size:15.0] lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
[super drawRect:rect];
}
It is working fine but it is slow for my purpose. I need to draw the string without using [Label setNeedDisplay]; because setNeedsDisplay needs to be called from the main thread. The rendering work needs to be done in a background queue. I would be grateful if you could provide me with a small example. Any help appreciated.
Thanks.
You can create a new CGContext to draw the string into CGImage/UIImage using CGBitmapContextCreateImage on separate thread, then draw the bitmap(generated image) into view on the main thread.
EDIT:
Check this post for example.
Related
I am implementing a UIview to contain a number of layers (about 9) to draw different elements of a graph in real time. I had previously implemented these as 9 different UIViews, and drew on them using the drawRect() function and it worked fine... but was very slow. From what I've been able to find online, it seems as if CALayers will be much faster. To make this change, I've subclassed CALayer, and only overriden the drawinContext: function. Here is the entirety of my CALayer.m file
#import "FFTLayer.h"
#implementation FFTLayer
#synthesize strokeColor,points,numPoints;
-(void)display{
[self drawInContext:UIGraphicsGetCurrentContext()];
NSLog(#"displaying!!");
[super display];
}
-(void)drawInContext:(CGContextRef)ctx
{
NSLog(#"drawing in context!");
CGContextSetStrokeColorWithColor(ctx, strokeColor);
CGContextStrokeLineSegments(ctx, points, numPoints*2);
}
#end
I have tried a bunch of different things. but so far, the only way that I've been able to get drawInContext: to be called is by calling it in the display() method, which seems wrong. Even when I do get drawInContext to be called, Xcode tells me that my drawing context is invalid. Here is the code that I'm using to try and tell FFTlayer to call itself.
context = UIGraphicsGetCurrentContext();
[self.layer addSublayer:fftLayer];
[fftLayer setPoints:fft_points];
[fftLayer drawInContext:context];
[fftLayer setNeedsDisplay];
[self performSelectorOnMainThread:#selector(setNeedsDisplay) withObject:nil waitUntilDone:NO];
Even after spending a few days reading about CALAyers online, I am still pretty confused about how to properly use them, and if I'm drawing this the most efficient/correct way. In case you can't tell from the "FFTLayer" class name, I'm an audio guy, not a graphics guy :)
Any help would be greatly appreciated!!
EDIT:
I set up all of my layers in the following way
//FFT Graph Layer
fftLayer = [[FFTLayer alloc] init];
fftLayer.backgroundColor = [UIColor clearColor].CGColor;
fftLayer.frame = self.layer.bounds;
[fftLayer setNumPoints:nP];
[fftLayer setStrokeColor:fft_strokeColor];
Did you import "FFTlayer.h" in your view subclass?
Try to add after layer init:
fftLayer.delegate = fftLayer;
My UIView structure:
I have a "master" UIView (actually UIScrollView, but it's only purpose scrolling per pagination).
As a subView on this "master" I have my "pageView" (subclass of UIScrollView). This UIScrollView can have any content (e.g. a UIImageView).
The "master" has another subView: PaintView (subclass of UIView). With this view, I track the finger movements and draw it.
The structure looks like this:
[master.view addSubview: pageView];
[master.view addSubview: paintView];
I know when the user zooms in (pageView is responsible for this) and over delegate/method calls I change the frame of paintView according to the zoom change, during the zoom action.
After zooming (scrollViewDidEndZooming:withView:atScale) I call a custom redraw method of paintView.
Redraw method and drawRect:
-(void)redrawForScale:(float)scale {
for (UIBezierPath *path in _pathArray) {
//TransformMakeScale...
}
[self setNeedsDisplay];
}
-(void)drawRect:(CGRect)rect {
for (UIBezierPath *path in _pathArray) {
[path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
The problem:
When I zoom-in, I receive a memory warning and the app crashes.
In the Allocations profiler I can see that the app own a lot of memory, but I can't see why.
When I don't call 'setNeedDisplay' after my 'redrawForScale' method the app isn't crashing.
When I log the rect in drawRect I see values like this: {{0, 0.299316}, {4688, 6630}}.
The problem is (re)drawing such a huge CGRect (correct me if this is wrong).
The solution for me is to avoid calling a custom drawRect method. In some stackoverflow answers (and I think somewhere in the Appls docs) it was mentioned that a custom drawRect: will reduce the performance (because iOS can't do some magic on a custom drawRect method).
My solution:
Using a CAShapeLayer. For every UIBezierPath I create a CAShapeLayer instance, add the UIBezierPath to the path attribute of the layer and add the layer as a sublayer to my view.
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] initWithLayer:self.layer];
shapeLayer.lineWidth = 10.0;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.path = myBezierPath.CGPath;
[self.layer addSublayer:shapeLayer];
With this solution I don't need to implement drawRect. So the App won't crash, even with a big maximumZoomScale.
After changing the UIBezierPath (translate, scale, change color) I need to set the changed bezierPath to the shapeLayer again (set the path attribute of shapeLayer again).
I am loading an image in a UIImageView which I then add to a UIScrollView.
The image is a local image and is about 5000 pixels in height.
The problem is that when I add the UIImageView to the UIScrollView the thread is blocked.
It is obvious because when I do this I can not scroll the UIScrollView till the image is displayed.
Here's the example.
UIScrollView *myscrollview = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 768, 1004)];
myscrollview.contentSize = CGSizeMake(7680, 1004);
myscrollview.pagingEnabled = TRUE;
[self.view addSubview:myscrollview];
NSString* str = [[NSBundle mainBundle] pathForResource:#"APPS.jpg" ofType:nil inDirectory:#""];
NSData *imageData = [NSData dataWithContentsOfFile:str];
UIImageView *singleImageView = [[UIImageView alloc] initWithImage:[UIImage imageWithData:imageData]];
//the line below is the blocking line
[scrollView addSubview:singleImageView];
It is the last line in the script that blocks the scroller. When I leave it out everything works perfect, except for the fact the image is not showing of course.
I seem to recall that using multithreading does not work on UIView operations so I guess that's out of the question ass well.
Thanks for your kind help.
If you're providing these large images, you should maybe check out CATiledLayer; there's a video of a good presentation on how to use this from WWDC 2010.
If these aren't your images, and you can't downsample them or break them into tiles, you can draw the image on a background thread. You may not draw to the screen graphics context on any main thread but the main thread, but that doesn't prevent you from drawing to a non-screen graphics context. On your background thread you can
create a drawing context with CGBitmapContextCreate
draw your image on it just as you would draw onto the screen in drawRect:
when you're done loading and drawing the image invoke your view's drawRect: method on the main thread using performSelectorOnMainThread:withObject:waitUntilDone:
In your view's drawRect: method, once you've fully drawn your image on the in-memory context, copy it to the screen using CGBitmapContextCreateImage and CGContextDrawImage.
This isn't trivial, you'll need to start your background thread at the right time, synchronize access to your images, etc. The CATiledLayer approach is almost certainly the better one if you can find a way to manipulate the images to make that work.
Why you want to load such huge image to memory at one time? split it into many small images and load/free it dynamically.
Try allocating the UIImageView without a UIImage, and add it as a sub-view to the UIScrollView.
Load the UIImage in a separate thread, and get the method running on the other thread to set the image property of the UIImageView when the image is loaded into memory. Also you'll probably encounter some memory problems as an image of this size loaded into a UIImage will probably be 30MB+
UIScrollView *myscrollview = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 768, 1004)];
myscrollview.contentSize = CGSizeMake(7680, 1004);
myscrollview.pagingEnabled = TRUE;
[self.view addSubview:myscrollview];
NSString* str = [[NSBundle mainBundle] pathForResource:#"APPS.jpg" ofType:nil inDirectory:#""];
UIImageView *singleImageView = [[UIImageView alloc] init];
[scrollView addSubview:singleImageView];
//Then fire off a method on another thread to load the UIImage and set the image
//property of the UIImageView.
Just keep an eye on memory and beware of using convenience constructors with UIImage (or any object that could end up being huge)
Also where are you currently running this code?
I know this seems like a simple task, which is why I don't understand why I can't get the image to render.
When I set up my UIView, I do the following:
myUiView.backgroundColor = [UIColor clearColor];
myUiView.opaque = NO;
I create and retain the UIImage in the init function of my UIView:
image = [[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"test" ofType:#"png"]] retain];
then my drawRect looks like this:
- (void) drawRect:(CGRect) rect
{
[image drawInRect:self.bounds];
}
Ultimately I'll be manipulating that UIImage via bitmap context, and then in drawRect create a CGImage out of the context, and render that, but for now I'm just trying to get it rendering a known image.
I've been digging through this site, as well as the documentation. I've gone down the CG path and tried drawing it with CGContextDrawImage by following the numerous examples other people have posted, but that didn't work either.
So I've come back to what seems to be the most straightforward way to draw an image, but it isn't working.
Any help would be greatly appreciated.
Thanks in advance.
First of all, verify that the size and position of self.bounds are what you want them to be. If the size is {0,0} nothing will display. Check using this function:
NSLog(#"%#", NSStringFromCGRect(self.bounds));
Also make sure that the image is not nil:
NSLog(#"%#", image);
I have a NSView where I draw thousands of NSBezierPaths. I would like to highlight (change fill color) the a selected one on mousemoved event. At the moment I use in mouseMoved function the following command:
[self setsetNeedsDisplay:YES];
that force a call to drawRect to redraw every path. I would like to redraw only the selected one.
I tried to use addClip in drawRect function:
NSBezierPath * path = ... //builds the path here
[path addClip];
[path fill];
but it seems that drawRect destroys all the other previously drawn paths and redraws only the one clipped.
Is it possible NOT to invalidate all the view when calling drawRect? I mean just to incrementally overwrite what was on the view before?
Thanks,
Luca
You should use [self setNeedsDisplayInRect:…]. Pass the NSRect you want invalidated, and that will be the area passed to the drawRect: call.
Inside drawRect:, check the area that was passed in and only perform the drawing necessary inside of that rectangle.
Also, you might want to look into using NSTrackingArea instead of mouseMoved: – these allow you to set up specific rectangles to trigger updates for.
I think I solved in a faster way, as I do not know a priori which paths are present in a rectangle I want to avoid a loop through all the paths. Fortunately my paths do not change often, so I can cache all the paths in a NSImage. On mouseMoved event I set:
RefreshAfterMouseMoved = YES;
and in drawRect function I put something like:
if (RefreshAfterMouseMoved) {
[cacheImage drawAtPoint:zero fromRect:viewRect operation:1
fraction:(CGFloat)1.0];
//redraw only the hilighted path
}
else{
if (cacheImage) [cacheImage release];
cacheImage = [[NSImage alloc] initWithSize: [self bounds].size ];
[cacheImage lockFocus];
// draw everything here
[cacheImage unlockFocus];
[cacheImage drawAtPoint:zero fromRect:viewRect operation:1
fraction:(CGFloat)1.0];
}
This method can be combined with the above setNeedsDisplayInRect method putting in mousedMoved function:
NSRect a, b, ab;
a = [oldpath bounds];
b = [newpath bounds];
ab = NSUnionRect(a,b);
RefreshAfterMouseMoved = YES;
[self setNeedsDisplayInRect:ab];