18

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.

  1. 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)
  2. On the subview use CALayers and draw on there
  3. Create a draw line method using CGContext functions
  4. Something else?

Cheers for the help

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
sregorcinimod
  • 277
  • 3
  • 12

2 Answers2

45

Conceptually all your propositions are similar. All of them would lead to the following steps (some of them done invisibly by UIKit):

  1. Setup a bitmap context in memory.
  2. Use Core Graphics to draw the line into the bitmap.
  3. Copy this bitmap to a GPU buffer (a texture).
  4. 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.

Performance comparison Core Graphics vs. Core Animation

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • +1 for the most efficient approach, but calculating the correct position and transformation is harder than with the simple drawing solution (either UIView or CALayer). Given two points *p1* and *p2*, how do you position/rotate the layer correctly ? I've got a few ideas but neither feels "right". – DarkDust May 01 '11 at 11:56
  • 2
    @DarkDust Added code to show how to set the layers properties. – Nikolai Ruhe May 01 '11 at 14:26
  • @Niklai Ruhe: Thanks, that's even simpler than I thought. – DarkDust May 01 '11 at 14:43
  • @Nikolai Ruhe: looks like a great approach - I will try it later (when I have finished my day job) but before I do 1 more question as I will be drawing multiple lines i would therefore need to add multiple layers to the UIView. But if I use [[NameOfView layer] addSublayer: layername]; how could i make the layername dynamic so i can loop through the same code to draw more than 1 line? – sregorcinimod May 01 '11 at 22:47
  • @NikolaiRuhe - I have attempted to implement your code but everytime I call it nothing happens - i even put in an NSLog just to check it was being called but nothing. I checked whether the object I was calling it on was Null but that wasn't the case. Is there anything special I have to do with the CALayer i'm passing in the argument before I call the code – sregorcinimod May 03 '11 at 01:09
  • @sregorcinimod I posted a link with a working demo project that shows how to implement the descibed approach. It also shows the difference between classic (Core Graphics) and GPU-only (Core Animation) drawing. – Nikolai Ruhe May 03 '11 at 08:38
  • +10 thank you Nikolai - that is perfect - implemented it and it works great. – sregorcinimod May 04 '11 at 08:48
  • lines are drawn good, but they are aliased. Is it possible to have anti-aliasing in a simple way? – pqnet Apr 08 '12 at 17:44
  • 2
    @pqnet: You can switch on anti aliasing for core animation layers by setting `UIViewEdgeAntialiasing` in you app's Info.plist. – Nikolai Ruhe Apr 09 '12 at 04:59
  • @NikolaiRuhe Cool, it works wonder. Did you do any performance test using antialiasing? How does it compare with other approaches? – pqnet Apr 09 '12 at 10:16
  • @NikolaiRuhe Amazing example but How to get intersection point between two lines ? Have you any idea ? – Chintan Feb 09 '13 at 08:22
  • 1
    @Maverick That question has been answered many times, for example here: http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect – Nikolai Ruhe Feb 09 '13 at 09:38
5

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 !

DarkDust
  • 90,870
  • 19
  • 190
  • 224