0

I've had this problem for a couple of days now, I've googled a lot but found out nothing useful so I thought to ask here. I've got a retention problem with an NSMutableArray, however I can't understand where the trouble is and how to fix it. I am drawing a a line chart using Quartz 2D, and in order to do so I have subclassed UIView with GraphView. The header looks like this:

@interface GraphView : UIView {
    NSMutableArray *data;
}

- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx;
- (void)drawLineGraphWithContext:(CGContextRef)ctx;
- (void)addValue:(float)value;

@property(nonatomic, retain) NSMutableArray *data;

@end

I use the "data" array to store data points to draw, within the drawLineGraphWithContext: method. The addValue: method, on the other hand, is used in the main controller to fill in the data array. In the viewDidLoad of the controller I do, to fill in the data array:

graphView = [[GraphView alloc] init];

float data[] = {0.7, 0.4, 0.9, 1.0, 0.2, 0.85, 0.11, 0.75, 0.53, 0.44, 0.88, 0.77, 0.99, 0.55};

for(int i = 0; i < 14; i++)
{
    [graphView addValue:data[i]];
}

Everything is good at this point, and in debug I can see the array is populated with all elements. The problem is when the drawLineGraphWithContext: method is invoked; for some reason, the array suddenly gets empty and its retain count is 0. I have tried changing the way I access the object, using dot notation or omitting the self, however the behaviour looks the same. Strangely enough, I have tried identifying zombies (with both GDB and Instruments), however none seems to be found. As a matter of fact, then the main loop within the drawLineGraphWithContext: below is started the data array has zero elements, and nothing is drawn. Below complete files (I am omitting irrelevant parts).

I am using Xcode 4.2 with Automatic Reference Counting.

Thanks in advance to anyone helping!

GraphView.h:

@interface GraphView : UIView {
    NSMutableArray *data;
}

- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx;
- (void)drawLineGraphWithContext:(CGContextRef)ctx;
- (void)addValue:(float)value;

@property(nonatomic, retain) NSMutableArray *data;

@end

GraphView.m:

#import "GraphView.h"

@implementation GraphView
@synthesize data;

- (id) init
{
    self = [super init];
    if(self) {
        data = [NSMutableArray array];
        NSLog(@"%s", __FUNCTION__);
    }
    return self;
}

- (void)addValue:(float)value
{
    NSMutableArray *tempArr = [self data];
    NSNumber *val = [NSNumber numberWithFloat:value];
    [tempArr addObject:val];
    [self setData:tempArr];
}

- (void)drawLineGraphWithContext:(CGContextRef)ctx
{
    CGContextSetLineWidth(ctx, 2.0);
    CGContextSetStrokeColorWithColor(ctx, [[UIColor colorWithRed:1.0 green:0.5 blue:0 alpha:1] CGColor]);

    int maxGraphHeight = kGraphHeight - kOffsetY;

    CGContextBeginPath(ctx);

    CGContextMoveToPoint(ctx, kOffsetX, kGraphHeight - maxGraphHeight * [[[self data] objectAtIndex:0] floatValue]);

    int i = 0;
    NSMutableArray *arr = [self data];

    for(NSNumber *elem in arr)
    {
        float val = [elem floatValue];
        CGContextAddLineToPoint(ctx, kOffsetX + i * kStepX, kGraphHeight - maxGraphHeight * val);
       i++;
    }

    CGContextDrawPath(ctx, kCGPathStroke);    

}

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetLineWidth(context, 0.6);
    CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] CGColor]);

    CGFloat dash[] = {2.0, 2.0};
    CGContextSetLineDash(context, 0.0, dash, 2);

    int lines = (kDefaultGraphWidth - kOffsetX)/kStepX;
    for(int i = 0; i < lines; i++)
    {
        CGContextMoveToPoint(context, kOffsetX + i * kStepX, kGraphTop);
        CGContextAddLineToPoint(context, kOffsetX + i * kStepX, kGraphBottom);
    }

    int horizontalLines = (kDefaultGraphWidth-kOffsetY)/kStepY;
    for(int i = 0; i < horizontalLines; i++)
    {
        CGContextMoveToPoint(context, kOffsetX, kGraphBottom - kOffsetY - i * kStepY);
        CGContextAddLineToPoint(context, kDefaultGraphWidth, kGraphBottom - kOffsetY -i * kStepY);
    }
    CGContextStrokePath(context);
    CGContextSetLineDash(context, 0, NULL, 0);

    [self drawLineGraphWithContext:context];
}

@end

GRViewController.m:

#import "GRViewController.h"

@implementation GRViewController
@synthesize scroller;
@synthesize graphView;

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
scroller.contentSize = CGSizeMake(kDefaultGraphWidth, kGraphHeight);
    graphView = [[GraphView alloc] init];

    float data[] = {0.7, 0.4, 0.9, 1.0, 0.2, 0.85, 0.11, 0.75, 0.53, 0.44, 0.88, 0.77, 0.99, 0.55};

    for(int i = 0; i < 14; i++)
    {
        [graphView addValue:data[i]];
    }
}

@end

GRViewController.h:

#import <UIKit/UIKit.h>
#import "GraphView.h"

@interface GRViewController : UIViewController
@property (retain, nonatomic) IBOutlet UIScrollView *scroller;
@property (retain, nonatomic) IBOutlet GraphView *graphView;

@end
Diferdin
  • 1,252
  • 1
  • 14
  • 30

3 Answers3

2

It sounds like you have two different instances of GraphView — one which you are creating in that method with [[GraphView alloc] init], and another instance (probably in a nib) that is actually being displayed. The GraphView that gets the data isn't displayed, and the GraphView that is displayed never gets any data. You can confirm that this is the case by doing NSLog(@"I am %@", self) in the relevant GraphView methods. You'll see two different objects reporting.

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • Thanks. I have tried inserting that line in the drawLineGraphWithContext: method, however the line appears once only in the GDB console. – Diferdin Nov 04 '11 at 17:47
  • @Antonio: That's the method that we suspect is being called on the other instance, so it makes sense. Try logging in `addValue:` and see if `self` has the same address as in `drawLineGraphWithContext:`. – Chuck Nov 04 '11 at 18:06
  • I have found a way to get rid of the problem, but I would be very much interested in understanding why this solution works and whether this is the correct solution or not. I managed to retain the array by simply removing its declaration within GraphView.h and declaring it locally within GraphView.m. Didn't really do anything else, so same body for addValue: and the other methods. Mow, my question is: why does that work? Or better, why does declaring the array in the header causes the problem? Any of you gurus out there willing to get this? :-) THanks anyway all of you who replied. – Diferdin Nov 04 '11 at 18:23
  • @Antonio: I assume you don't actually mean "declare it locally," as in a local variable inside a method. I suspect what you actually did was declare it *globally* inside GraphView.m (that is, outside of any method). The reason that "works" is because then all instances of the class share the same variable, whereas before each instance had its own array variable. This kind of solution is generally frowned on, because it is a much more brittle design — and it is **definitely wrong** in this case, because it doesn't address the underlying problem, which is that you have two different objects. – Chuck Nov 04 '11 at 19:03
  • I understand, but what would you suggest then? – Diferdin Nov 04 '11 at 22:26
  • @Antonio: Only have one GraphView per controller. You can probably just get rid of that `graphView = [[GraphView alloc] init]`. – Chuck Nov 04 '11 at 23:03
  • Thanks Chuck, but dose that line cause a duplicate graph view? Also, if I don't allocate it and initial it, how do I init the array? Thanks. – Diferdin Nov 05 '11 at 09:41
  • I have tried removing the line you mention, and the result is that I cannot even fill in the elements in the array. I would say that was predictable... – Diferdin Nov 05 '11 at 10:25
  • @Antonio: Like I said, you appear to have two instances hanging around. Find the original instance that's actually being displayed and use that one instead of creating a new instance. That's the idea. Without seeing your app, I have no way of knowing where the two instances are, but I strongly suspect one is in a nib, so I figured it might be hooked up to File's Owner's `graphView` outlet. If you do have a GraphView in a nib, try hooking up that outlet. – Chuck Nov 05 '11 at 19:37
1

In your init method use either...

self.data = [NSMutableArray array];

or...

data = [[NSMutableArray alloc] init];

You are not going via the property without using 'self.' and hence not retaining the array.

Simon Lee
  • 22,304
  • 4
  • 41
  • 45
  • Thanks for you reply, however this seems not to be the problem. I have tried both (actually, the second was my first choice) but none helps with that – Diferdin Nov 04 '11 at 17:10
  • you may have other problems but @simon is correct. unless your project is using ARC but that's another topic. – XJones Nov 04 '11 at 17:24
  • 1
    My project is using ARC, and I mention this in my original post. Thanks. – Diferdin Nov 04 '11 at 17:34
  • 2
    `data = [NSMutableArray array];` is perfectly OK in ARC. This answer is misleading. – Nikolai Ruhe Nov 04 '11 at 17:43
  • I have found a way to get rid of the problem, but I would be very much interested in understanding why this solution works and whether this is the correct solution or not. I managed to retain the array by simply removing its declaration within GraphView.h and declaring it locally within GraphView.m. Didn't really do anything else, so same body for addValue: and the other methods. Mow, my question is: why does that work? Or better, why does declaring the array in the header causes the problem? Any of you gurus out there willing to get this? :-) THanks anyway all of you who replied. – Diferdin Nov 04 '11 at 18:23
1

Try this.

- (void)addValue:(float)value
{
    NSNumber *val = [NSNumber numberWithFloat:value];
    [data addObject:val];
}

You can just use the data ivar, and not worry about getting or setting the property. This might fix it, since it won't be using your getter and setter every time addValue is called.

Ryan
  • 518
  • 3
  • 12
  • Thanks, but unfortunately that is not the case. – Diferdin Nov 04 '11 at 17:44
  • I have found a way to get rid of the problem, but I would be very much interested in understanding why this solution works and whether this is the correct solution or not. I managed to retain the array by simply removing its declaration within GraphView.h and declaring it locally within GraphView.m. Didn't really do anything else, so same body for addValue: and the other methods. Mow, my question is: why does that work? Or better, why does declaring the array in the header causes the problem? Any of you gurus out there willing to get this? :-) THanks anyway all of you who replied. – Diferdin Nov 04 '11 at 18:23