custom drawing nsview inside nsscrollview - nsview

I'm having a problem with custom drawing NSView using as documentView of a NSScrollView.
Here is my drawRect: code:
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor lightGrayColor] set];
NSRectFill(self.frame); // Fill entire frame
[[NSColor grayColor] set];
[NSBezierPath setDefaultLineWidth:1];
float y = 0.0f;
while (y <= self.frame.size.height) {
[NSBezierPath strokeLineFromPoint:NSMakePoint(0.0f, y) toPoint:NSMakePoint(self.frame.size.width, y)];
y += 50.0f;
}
float x = 0.0f;
while (x <= self.frame.size.width) {
[NSBezierPath strokeLineFromPoint:NSMakePoint(x, 0.0f) toPoint:NSMakePoint(x, self.frame.size.height)];
x += 50.0f;
}
}
Everything is OK when I scroll the view rightward, but I see strange lines when I scroll the view leftward.
I think this is a cached image buffer or something but I couldn't get why this occurs because I fill a rect which covers entire frame.
What causes this problem? How can I solve it? Thanks.

I know this is an old question but I just encountered the same problem and I managed to fix it.
the drawRect method has one argument, a (CGRect) dirtyRect. My mistake was that I used the dirtyRect to check for the boundaries to draw on, just like on iOS. But I discovered that this CGRect is called 'dirty' for a reason. It specifies the region that the OS wants you to redraw. This sometimes isn't the entire frame of the NSView, but a part of it. This allows you to make really fast optimized code that does't draw more than it has to.
My solution was to ignore the dirtyRect and instead simply use self.bounds for drawing.

Basically, you're drawing on top of the old content.
So, essentially, you need to clear it somehow.
One way is
[[NSColor clearColor] set];
NSRectFill(_bounds);
You may want something more precise or not.

Related

How to create a border around circle which is created using drawrect method in iphone

Hi in one of my application i had created a circle using drawrect method on UIView object.Now my concern i want draw a highlight border around the circle for that actually i used
myView.layer.borderWidth =3.0;
myView.layer.borderColor=[UIColor colorWithRed:myView.patternRed green:myView.patternGreen blue:myView.patternBlue alpha:1.0].CGColor;
But due to this code what is happening is a border is creating around the view and it's looks a rectangle, but i want to create a border around the circle itself. So if anyone know how to implement this functionality please let me know. Thanks in advance.
Try this
myView.layer.cornerRadius = 80.0f;
It'll curve your view into a circle.
Thanks.
you have to set some radius for the corners of that view so add this line
myView.layer.cornerRadius=20;
play with the numeric value to match your requirements
I hope it helps
This Works for me:
UIView *myView =[[UIView alloc]initWithFrame:CGRectMake(0,0,100,100)];
[self createRoundUIView:myView:80];
-(void)createRoundUIView:(UIView *)inputView sizeDiameter:(float)diameterSize;
{
CGPoint saveCenter = inputView.center;
CGRect frame = CGRectMake(inputView.frame.origin.x, inputView.frame.origin.y, diameterSize, diameterSize);
roundedView.frame = frame;
roundedView.layer.cornerRadius = diameterSize / 2.0;
roundedView.center = saveCenter;
}

iOS: CGContextRef drawRect does not agree with input

Background : I would like to draw blocks when the user touch up somewhere. If the block is there, I want to erase it. I manage the blocks by using NSMutableArrayto keep track of points where the block should go. Every time user touches, it will determine if the touch place already contained a block or not and manage the array accordingly.
Problem : I got a very weird feedback from this. First of all, everything in the array works as I wanted. The problem comes when the user wanted to erase a block. While the array is maintained correctly, the drawing seems to ignore the change in the array. It will not remove anything but the last dot. And even that flashes toggles on and off when the user clicked elsewhere.
Here is the code :
- (void)drawRect:(CGRect)rect
{
NSLog(#"drawrect current array %#",pointArray);
for (NSValue *pointValue in pointArray){
CGPoint point = [pointValue CGPointValue];
[self drawSquareAt:point];
}
}
- (void) drawSquareAt:(CGPoint) point{
float x = point.x * scale;
float y = point.y * scale;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x+scale, y);
CGContextAddLineToPoint(context, x+scale, y+scale);
CGContextAddLineToPoint(context, x, y+scale);
CGContextAddLineToPoint(context, x, y);
CGContextSetFillColorWithColor(context, [UIColor darkGrayColor].CGColor);
CGContextFillPath(context);
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *aTouch = [touches anyObject];
CGPoint point = [aTouch locationInView:self];
point = CGPointMake( (int) (point.x/scale), (int) (point.y/scale));
NSLog(#"Touched at %#", [NSArray arrayWithObject: [NSValue valueWithCGPoint:point]]);
NSValue *pointValue = [NSValue valueWithCGPoint:point];
int i = [pointArray indexOfObject:pointValue];
NSLog(#"Index at %i",i);
if (i < [pointArray count]){
[pointArray removeObjectAtIndex:i];
NSLog(#"remove");
}else {
[pointArray addObject:pointValue];
NSLog(#"add");
}
NSLog(#"Current array : %#", pointArray);
[self setNeedsDisplay];
}
scale is defined as 16.
pointArray is a member variable of the view.
To Test : You can drop this into any UIView and add that to the viewController to see the effect.
Question : How do I get the drawing to agree with the array?
Update + Explanation: I am aware of the cost of this approach but it is only created for me to get a quick figure. It will not be used in the real application, thus, please do not get hung up about how expensive it is. I only created this capability to get a value in NSString (#"1,3,5,1,2,6,2,5,5,...") of a figure I draw. This will become more efficient when I am actually using it with no redrawing. please stick to the question asked. Thank you.
I don't see anywhere where you are actually clearing what you drew previously. Unless you explicitly clear (such as by filling with UIRectFill() - which, as an aside, is a more convenient way to draw rectangles than filling an explicit path), Quartz is going to just draw over your old content, which will cause unexpected behavior on attempts at erasure.
So... what happens if you put at the beginning of -drawRect::
[[UIColor whiteColor] setFill]; // Or whatever your background color is
UIRectFill([self bounds]);
(This is of course horrendously inefficient, but per your comment, I am disregarding that fact.)
(As a separate aside, you probably should wrap your drawing code in a CGContextSaveGState()/CGContextRestoreGState() pair to avoid tainting the graphics context of any calling code.)
EDIT: I always forget about this property since I usually want to draw more complex backgrounds anyway, but you can likely achieve similar results by setting clearsContextBeforeDrawing:YES on the UIView.
This approach seems a little weird to me because every time the touchesEnded method is called you need to redraw (which is an expensive operation) and also need keep track of the squares. I suggest you subclass an UIView and implement the drawRect: method, so the view knows how to draw itself and implement the touchesEnded method in your view controller, where you can check if you have touched a squareView then remove it from view controller's view otherwise create a squareView and add it as subview to the view controller's view.

What's the best approach to draw lines between views?

Background: I have a custom scrollview (subclassed) that has uiimageviews on it that are draggable, based on the drags I need to draw some lines dynamically in a subview of the uiscrollview. (Note I need them in a subview as at a later point i need to change the opacity of the view.)
So before I spend ages developing the code (i'm a newbie so it will take me a while) I looked into what i need to do and found some possible ways. Just wondering what the right way to do this.
Create a subclass of UIView and use the drawRect method to draw the line i need (but unsure how to make it dynamically read in the values)
On the subview use CALayers and draw on there
Create a draw line method using CGContext functions
Something else?
Cheers for the help
Conceptually all your propositions are similar. All of them would lead to the following steps (some of them done invisibly by UIKit):
Setup a bitmap context in memory.
Use Core Graphics to draw the line into the bitmap.
Copy this bitmap to a GPU buffer (a texture).
Compose the layer (view) hierarchy using the GPU.
The expensive part of the above steps are the first three points. They lead to repeated memory allocation, memory copying, and CPU/GPU communication. On the other hand, what you really want to do is lightweight: Draw a line, probably animating start/end points, width, color, alpha, ...
There's an easy way to do this, completely avoiding the described overhead: Use a CALayer for your line, but instead of redrawing the contents on the CPU just fill it completely with the line's color (setting its backgroundColor property to the line's color. Then modify the layer's properties for position, bounds, transform, to make the CALayer cover the exact area of your line.
Of course, this approach can only draw straight lines. But it can also be modified to draw complex visual effects by setting the contents property to an image. You could, for example have fuzzy edges of a glow effect on the line, using this technique.
Though this technique has its limitations, I used it quite often in different apps on the iPhone as well as on the Mac. It always had dramatically superior performance than the core graphics based drawing.
Edit: Code to calculate layer properties:
void setLayerToLineFromAToB(CALayer *layer, CGPoint a, CGPoint b, CGFloat lineWidth)
{
CGPoint center = { 0.5 * (a.x + b.x), 0.5 * (a.y + b.y) };
CGFloat length = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
CGFloat angle = atan2(a.y - b.y, a.x - b.x);
layer.position = center;
layer.bounds = (CGRect) { {0, 0}, { length + lineWidth, lineWidth } };
layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1);
}
2nd Edit: Here's a simple test project which shows the dramatical difference in performance between Core Graphics and Core Animation based rendering.
3rd Edit: The results are quite impressive: Rendering 30 draggable views, each connected to each other (resulting in 435 lines) renders smoothly at 60Hz on an iPad 2 using Core Animation. When using the classic approach, the framerate drops to 5 Hz and memory warnings eventually appear.
First, for drawing on iOS you need a context and when drawing on the screen you cannot get the context outside of drawRect: (UIView) or drawLayer:inContext: (CALayer). This means option 3 is out (if you meant to do it outside a drawRect: method).
You could go for a CALayer, but I'd go for a UIView here. As far as I have understood your setup, you have this:
UIScrollView
| | |
ViewA ViewB LineView
So LineView is a sibling of ViewA and ViewB, would need be big enough to cover both ViewA and ViewB and is arranged to be in front of both (and has setOpaque:NO set).
The implementation of LineView would be pretty straight forward: give it two properties point1 and point2 of type CGPoint. Optionally, implement the setPoint1:/setPoint2: methods yourself so it always calls [self setNeedsDisplay]; so it redraws itself once a point has been changed.
In LineView's drawRect:, all you need to is draw the line either with CoreGraphics or with UIBezierPath. Which one to use is more or less a matter of taste. When you like to use CoreGraphics, you do it like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Set up color, line width, etc. first.
CGContextMoveToPoint(context, point1);
CGContextAddLineToPoint(context, point2);
CGContextStrokePath(context);
}
Using NSBezierPath, it'd look quite similar:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
// Set up color, line width, etc. first.
[path moveToPoint:point1];
[path addLineToPoint:point2];
[path stroke];
}
The magic is now getting the correct coordinates for point1 and point2. I assume you have a controller that can see all the views. UIView has two nice utility methods, convertPoint:toView: and convertPoint:fromView: that you'll need here. Here's dummy code for the controller that would cause the LineView to draw a line between the centers of ViewA and ViewB:
- (void)connectTheViews
{
CGPoint p1, p2;
CGRect frame;
frame = [viewA frame];
p1 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
frame = [viewB frame];
p2 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
// Convert them to coordinate system of the scrollview
p1 = [scrollView convertPoint:p1 fromView:viewA];
p2 = [scrollView convertPoint:p2 fromView:viewB];
// And now into coordinate system of target view.
p1 = [scrollView convertPoint:p1 toView:lineView];
p2 = [scrollView convertPoint:p2 toView:lineView];
// Set the points.
[lineView setPoint1:p1];
[lineView setPoint2:p2];
[lineView setNeedsDisplay]; // If the properties don't set it already
}
Since I don't know how you've implemented the dragging I can't tell you how to trigger calling this method on the controller. If it's done entirely encapsulated in your views and the controller is not involved, I'd go for a NSNotification that you post every time the view is dragged to a new coordinate. The controller would listen for the notification and call the aforementioned method to update the LineView.
One last note: you might want to call setUserInteractionEnabled:NO on your LineView in its initWithFrame: method so that a touch on the line will go through to the view under the line.
Happy coding !

how to draw new lines without old ones disappering?

I want to create a simple tool for drawing. The purpose is to draw a line that follows the accelerometer of the iPhone & iPad, so if the user tilts the device a line will be draw in the direction the device was moved.
I am able to register acceleration and drawing lines. My problem is that as soon as I draw a line the old one disappears. One possible solution would be to save to points already drawn and then re-draw everything, but I would think there are better solutions?
All help is appreciated!
My drawRect is at the moment like this:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 20.0);
CGContextSetStrokeColorWithColor(context, [UIColor yellowColor].CGColor);
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y);
CGContextAddLineToPoint(context, toPoint.x, toPoint.y);
CGContextStrokePath(context);
}
A different method is responsible for refreshing. This method is also called from the uiviewcontroller with certain intervals. Right now it shows a "trail" (or what I should call it) in the direction the device was moved. Not exactly what I am looking for:
- (void)drawNewLine:(CGPoint)to {
// calculate trail behind current point
float pointDifferenceX = ((toPoint.x - to.x) * 9);
float pointDifferenceY = ((toPoint.y - to.y) * 9);
fromPoint = CGPointMake(toPoint.x + pointDifferenceX, toPoint.y + pointDifferenceY);
toPoint = to;
[self setNeedsDisplay];
}
I can think of two options:
Either save all points and redraw the lines whenever the screen needs to be refreshed (as you mentioned)
Draw the lines into an off-screen pixelmap and refresh the screen from there
In either case, respect the Hollywood principle: Don't call, you will be called. That means don't just draw to the screen but wait for until drawRect: of your UIView is called. (You can trigger this by calling setNeedsDisplay.)

Drawing in UIView stops working if view width becomes greater than about 16600

I am using following code to plot graph in a view (in the drawRect method):
CGContextBeginPath(context);
CGContextMoveToPoint(context, devicePoint.x, devicePoint.y);
for (index = 1; index < dataCount; index++) {
devicePoint = [[deviceDataArray objectAtIndex:index] CGPointValue];
CGContextAddLineToPoint(context, devicePoint.x, devicePoint.y);
}
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextStrokePath(context);
It works if the view.bounds.size.width is less than about 16600. But above that size the plot stops appearing.
I resize the view depending on the range of the data to be plotted.
This is very bad idea to make such huge width for view, you must draw only what user can see in one time (or a little more) and use view of normal (screen) size for this. For controlling where user currently is use UIScrollView.
You are probably better off using CAShapeLayers to do the drawing, as they have no actual pixels, just a path that the hardware draws. Then you could have a UIScrollView which just exposed parts of the CAShapeLayers for drawing, otherwise you are making a huge image with the view as large as you have it currently.