How do I prevent duplicate code inside 2 drawRect: methods? - cocoa

I have a few lines of drawing code that are duplicated in two different subclasses. When I move this drawing code to its own class and then call it from within drawRect: it is called but it is never drawn to the screen. What is the right way prevent duplicating code in two different drawRect: methods?
Details: I'm making a custom control by subclassing NSTableView and NSTableCellView. My drawing code needs to be in drawRect: in both of these subclasses.
I created a subclass of NSObject that declares one method. Here is the implementation:
#implementation TNLChartDrawingExtras
- (void)drawDividersInRect:(NSRect)rect startingAtDate:(NSDate *)startDate withZoomFactor:(NSNumber *)zoomFactor {
float pos = 0;
NSDate *currentDate = [startDate copy];
while (pos < rect.size.width) {
//draw the vertical divider
NSBezierPath *linePath = [NSBezierPath bezierPathWithRect:NSMakeRect(pos, 0.0, 1.0, rect.size.height)];
[[NSColor colorWithCalibratedWhite:0.85 alpha:0.5] set];
[linePath fill];
//increment the values for the next day
currentDate = [NSDate dateWithTimeInterval:86400 sinceDate:currentDate]; // add one day to the current date
pos = pos + (86400.0/ [zoomFactor floatValue]);
}
}
In my NSTableView subclass I define a property for this object. Then in awakeFromNib I create an instance of this class:
- (void)awakeFromNib {
self.extras = [[TNLChartDrawingExtras alloc] init];
}
In drawRect: I send this message:
- (void)drawRect:(NSRect)dirtyRect {
// more code here...
[self.extras drawDividersInRect:viewBounds startingAtDate:chart.startDate withZoomFactor:self.zoomFactor];
}
The code is executed but the lines it is supposed to draw don't appear. If I put the code from drawDividersInRect:... in the drawRect: method, it works fine.

My original solution (described in the question) may have worked if I had continued to debug it. However, I think the more important question is what is the right way to approach this problem. Here I solve it by adding category on NSView to the project:
I'm trying to add custom drawing code to both NSTableView and NSTableCellView. Both are subclasses of NSView so I created a category of NSView and added my custom drawing method there. Now I can call my drawing method from both subclasses.

Without see any of your code, it sounds like you are in need of a protocol, which is that same thing as an interface in the java language. Protocols are a series of methods that a group of a few unrelated classes may need to used. For example, in a drawing program like PhotoShop, Rects, Ovals, and Images are all valid objects that can be stored as layers in a .psd document, however, they all share traits like the ability to change object properties in a particular way. An example would be adjusting an objects' opacity or rescale an objects size, etc. Methods that access the objects properties for scaling or functions that can be shared between unrelated objects types call for protocols.
They are essentially .h files that list out the methods. The .m file that defines implementation of the code can store a tag in it's .h file.
// example of a class that acts as a protocol implementor
#interface LayerObject: NSObject <Resizable>
The tag says, "I am a member of the protocol named X, you can find one/some of the methods of protocol X in my .m file." All you would have to do is import the protocol to the desired classes using the following syntax:
// Declare protocol
#protocol Resizable;
// List methods wanted from protocol
- id resizeRect: id layerObject;
to gain the methods defined in the protocol.
Here is a website that describes protocols through an example:
http://agilewarrior.wordpress.com/2012/03/19/simple-objective-c-protocol-example/
Another solution would be to create a class hierarchy that uses an abstract class to put the given drawRect method you are working in. From here you could define the two subclass you are working on as a subclass of the abstract class in which they would inherit the drawRect method code, keeping you from repeating the code in two separate classes.

Related

MacOS: Cell-based NSTableView custom row height breaks rendering

I am attempting to update some very old, formerly functional MacOS code (Objective C, pre-10.7) to modern MacOS and XCode 13. I have an NSCell-based NSTableView with custom row heights which when compiled pre-10.7 works fine and which more recently has stopped working. Recall 10.7 was the era in which auto-layout was introduced and NSTableView revised to accomodate NSView-based cells, so it makes sense that something problematic in this formerly functional code was flushed at that point.
The symptom of "stopped working" is that my custom NSCell's drawing method (drawWithFrame:inView) is never called, and the NSTableView renders each row as an opaque rectangle two pixels high. 8 years ago someone reported a similar symptom in similar global circumstances, but this issue remained unresolved, and they are no longer active.
In Interface Builder, I have "Row Size Style: Custom" and "Content Mode: Cell Based" set. I believe auto-layout is not relevant, since NSCells do not have constraints.
Now for the bizarre part. If I remove my NSTableViewDelegate's heightOfRow method, the table render works perfectly, and custom rows render though all at a constant height.
However if I re-add an implementation as straightforward as:
- (float)tableView:(NSTableView *)tableView heightOfRow:(int)row
{
return 30.0;
}
then I can see this method being called, once per row, to accumulate their sizes but after that NSCell's draw method is never called.
On the off chance I am completely misunderstand the documentation for heightOfRow, I have tried returning numbers much larger and much smaller than 30.0 as well. No luck. The fact that the table renders perfectly with heightOfRow unimplemented makes me believe my dataSource architecture and even my custom NSCell architecture is all functional, and that the issue somehow relates specifically to how NSTableView is interpreting my table's rows.
Here's the table's config in Interface Builder:
and here's source of a (toyed-down) ViewController for the panel (a modal dialog box) that both contains the tableView and acts as its delegate:
/* A customCell, installed in our nib, allows us to do our custom rendering */
#interface CustomCell : NSCell
{
}
#end
#implementation CustomCell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
CGContextRef quartz = [NSGraphicsContext currentContext].CGContext;
CGContextSaveGState(quartz);
CGContextSetRGBFillColor(quartz, 1.0, 0.0, 0.0, 1.0);
CGContextFillEllipseInRect(quartz, cellFrame);
CGContextRestoreGState(quartz);
}
#end
#implementation GSPRDBBrowseDialogController
#undef SHOW_THE_BUG
#ifdef SHOW_THE_BUG
/*
SHOW THE BUG: Explicitly including any definition of the
following delegate method, even one that returns a constant row
height of 30, causes our [cell drawWithFrame] method to never be called,
and all table rows appear as black rectangles 2 pixels high.
*/
- (float)tableView:(NSTableView *)tableView heightOfRow:(int)row
{
return 30;
}
#else
/* with no implementation of heightOfRow, Cocoa calls our renderer using
some the constant row height value (24 pixels) specified in the nib.
*/
#endif
- (IBAction) openButtonPressed: (id) sender
{
[NSApp stopModal];
}
- (id) initWithContext: (const RDB_Context*) iContext
helpContext: (int) iHelpContext
{
rdbContext = *iContext;
return( self = [super initWithNibNamed: #"rdbbrowse_dlg"
dialogData: NULL
changeProc: NULL
backTrack: false
helpContext: iHelpContext]);
}
- (int) numberOfRowsInTableView: (NSTableView*) iTableView
{
return 20;
}
- (id) tableView: (NSTableView*) iTableView
objectValueForTableColumn: (NSTableColumn*) iColumn
row: (int) iRow
{
return nil; // doesn't matter. real code returns more here.
}
#end
Running this code with SHOW_THE_BUG #UNDEFined produces the following correct (but undesired) result: 20 rows of red circles, each 24 pixels high:
But changing to #DEFINE SHOW_THE_BUG, thereby including a toy-implementation of heightOfRow that should set all rows to 30 pixels high, and instead we get this...thin rectangles (look closely at the top of the table), and the actual cell renderer (drawWithFrame) is never called:
Any thoughts about what crazy contortion my code may be in, where defining a custom row height that is effectively constant breaks rendering completely but leaving row height undefined produces almost-perfect results?
Thanks,
Nick
This wound up being my own fault. The object I was instantiating as NSTableViewDelegate, my dialog box NSWindowController, turned out to inherit from a long-forgotten base class that provides some general utilities for other dialog boxes in this app. That base class, in turn, implemented some of the NSTableViewDelegate contract, and those parts of one delegate instantiation were thus by inheritance intermingled with the various parts of the delegate contract I'm implementing here, leading to a broken contract from NSTableView's perspective. Since the base class is out of my control solution here was to firewall this table's delegate implementation into its own object, owned by but not descending from this NSWindowController, so it completely controls how much of the (optional) NSTableViewDelegate instantiation gets presented to the table.
I'm suitably sheepish and grateful to #Willeke for insisting the local implementation was itself coherent, which is what led me to wonder if the effective delegate implementation was somehow "more than" the local one...

how to reference NSTableView inside NSView Class (Cocoa)?

I m trying to implement a mac application, basically I have a NSTableView inside a NSView.
I implemented a drag & drop of multiple files feature on the NSView, now I want to update NSTableView with the files names just dropped.
So I need to reference NSTableView in my NSView and try reloadData to reload tableview.
I have tried initialised tableview controller in NSView and it seems not working?
MyTableController *testController = [[MyTableController alloc] init];
testController.nsMutaryOfDataObject = [[NSMutableArray alloc]init];
int i;
for (i = 0; i < 20; i ++) {
NSString * zStr1 = [[NSString alloc]initWithFormat:#"%d",(i + 1) * 10];
NSString * zStr2 = [[NSString alloc]initWithFormat:#"%d",((i + 1) * 100) + 2];
NSString * zStr3 = [[NSString alloc]initWithFormat:#"%d",((i + 1) * 1000) + 3];
MyDataObject * zDataObject = [[MyDataObject alloc]initWithString1:zStr1
andString2:zStr2
andString3:zStr3];
[testController.nsMutaryOfDataObject addObject:zDataObject];
} // end for
[testController.idTableView setHeaderView:nil];
[testController.idTableView reloadData];
Xib file as below:
Anyone can help me out?
A tableview has TWO different aspects: the view (presentation of data; drag and drop) and the datasource (an array combined with an array controller).
Usually a tableview leads a life on its own, independent from the view it is positioned in. So concentrate yourself first on the datasource-aspect and second on the drag-and-drop-aspect.
"TableView reload" simply means load the assigned array again.
For the data structure in your array a model is strongly recommended.
The names of the different parts in your model are bound to the columns in your table.
The array is the datasource to your tableview.
Use an array controller.
Your fileNames must be in the array, organized in the manner of the model...
Because a tableview is always part (subview) of an NSScrollView, assure that your really have selected the tableview itself.
In the documentation are fairly good samples for working with tableviews, with and without bindings, also "TableViewLinks" handling URLs...
Your code is missing the basic methods for a tableview to work, so start from scratch with a sample.
Sounds, as if the TV is working correctly
and
you have the fileNames available in the NSView class
and
don't want to implement the drag and drop functionality also in the class for the TV.
Then you could use a notification that is sent from the NSView-class to the TV-class with the fileNames-data attached. After receiving in the tv-class the fileNames could be used/added to the array. I once have created such a solution, it works perfect.
It also assures encapsulation of the classes.

When to retain and release CGLayerRef?

I have a question similar to this one:
CGLayerRef in NSValue - when to call retain() or release()?
I am drawing 24 circles as radial gradients in a view. To speed it up I am drawing the gradient into a layer and then drawing the layer 24 times. This worked really well to speed up the rendering. On subsequent drawRect calls some of the circles may need to be redrawn with a different hue, while others remain the same.
Every time through drawRect I recalculate a new gradient with the new hue and draw it into a new layer. I then loop through the circles, drawing them with the original layer/gradient or new layer/gradient as appropriate. I have a 24 element NSMutableArray that stores a CGLayerRef for each circle.
I think this is the answer provided in the question I linked above, however it is not working for me. The second time through drawRect, any circle that is drawn using the CGLayerRef that was stored in the array causes the program to crash when calling CGContextDrawLayerAtPoint. In the debugger I have verified that the actual hex value of the original CGLayerRef is stored properly into the array, and in the second time through drawRect that the same hex value is passed to CGContextDrawLayerAtPoint.
Further, I find that if I don't CGLayerRelease the layer then the program doesn't crash, it works fine. This tells me that something is going wrong with the memory management of the layer. It's my understanding that storing an object into an NSArray will increment it's reference count, and it won't be deallocated until the array releases it.
Anyway, here is the relevant code from drawRect. Down at the bottom you can see that I commented out CGLayerRelease. In this configuration the app doesn't crash although I think this is a resource leak. If I uncomment that release then the app crashes the second time though drawRect (between the first and second calls one of the circles has it's led_info.selected property cleared, indicating that it should use the saved layer rather than the new layer:
NSLog(#"ledView drawing hue=%4f sat=%4f num=%d size=%d",hue_slider_value,sat_slider_value,self.num_leds,self.led_size);
rgb_color = [UIColor colorWithHue:1.0 saturation:1.0 brightness:1.0 alpha:1.0];
end_color = [UIColor colorWithHue:1.0 saturation:1.0 brightness:1.0 alpha:0.0];
NSArray *colors = [NSArray arrayWithObjects:
(id)rgb_color.CGColor, (id)end_color.CGColor, nil];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace,(__bridge CFArrayRef) colors, NULL);
CGLayerRef layer = CGLayerCreateWithContext(context, (CGSize){self.led_size,self.led_size}, /*auxiliaryInfo*/ NULL);
if (layer) {
CGContextRef layer_context = CGLayerGetContext(layer);
CGContextDrawRadialGradient(layer_context, gradient, led_ctr,self.led_size/8,led_ctr, self.led_size/2,kCGGradientDrawsBeforeStartLocation);
} else {
NSLog(#"didn't get a layer");
}
for (int led=0;led<[self.led_info_array count];led++) {
led_info=[self.led_info_array objectAtIndex:led];
// the first time through selected=1 and led_info.cg_layer=nil for all circles,
// so this branch is taken.
if (led_info.selected || led_info.cg_layer==nil) {
CGPoint startPoint=led_info.rect.origin;
CGContextDrawLayerAtPoint(context, startPoint, layer);
CGContextAddRect(context, led_info.rect);
led_info.cg_layer=layer;
// the second time through drawRect one or more circles have been deselected.
// They take this path through the if/else
} else {
CGPoint startPoint=led_info.rect.origin;
// app crashes on this call to CGContextDrawLayerAtPoint
CGContextDrawLayerAtPoint(context, startPoint, led_info.cg_layer);
}
}
// with this commented out the app doesn't crash.
//CGLayerRelease(layer);
Here is the declaration of led_info:
#interface ledInfo : NSObject
#property CGFloat hue;
#property CGFloat saturation;
#property CGFloat brightness;
#property int selected;
#property CGRect rect;
#property CGPoint center;
#property unsigned index;
#property CGLayerRef cg_layer;
- (NSString *)description;
#end
led_info_array is the NSMutableArray of ledInfo objects, the array itself is a property of the view:
#interface ledView : UIView
#property float hue_slider_value;
#property float sat_slider_value;
#property unsigned num_leds;
#property unsigned led_size;
#property unsigned init_has_been_done;
#property NSMutableArray *led_info_array;
//#property layerPool *layer_pool;
#end
The array is initialized like this:
self.led_info_array = [[NSMutableArray alloc] init];
Edit: since I posted I have found that if I put retain/release around the assignemt into the NSMutableArray then I can also leave in the original CGLayerRelease and the app works. So I guess this is how it is supposed to work, although I'd like to know why the retain/release is necessary. In the objective C book I am reading (and the answer to the question linked above) I thought assigning into NSArray implicitly did retain/release. The new working code looks like this:
if (led_info.selected || led_info.cg_layer==nil) {
CGPoint startPoint=led_info.rect.origin;
CGContextDrawLayerAtPoint(context, startPoint, layer);
CGContextAddRect(context, led_info.rect);
if (led_info.cg_layer) CGLayerRelease(led_info.cg_layer);
led_info.cg_layer=layer;
CGLayerRetain(layer);
} else {
CGPoint startPoint=led_info.rect.origin;
CGContextDrawLayerAtPoint(context, startPoint, led_info.cg_layer);
}
You can probably tell that I'm brand new to Objective C and iOS programming, and I realize that I'm not really sticking to convention regarding case and probably other things. I'll clean that up but right now I want to solve this memory management problem.
Rob, thanks for the help. I could use a little further clarification. I think from what you are saying that there are two problems:
1) Reference counting doesn't work with CGLayerRef. OK, but it would be nice to know that while writing code rather than after debugging. What is my indication that when using "things" in Objective C/cocoa that resource counting doesn't work?
2) You say that I'm storing to a property, not an NSArray. True, but the destination of the store is the NSArray via the property, which is a pointer. The value does make it into the array and back out. Does resource counting not work like this? ie instead of CGLayerRef, if I were storing some NSObject into NSArray using the code above would resource counting work? If not, then would getting rid of the intermediate led_info property and accessing the array directly from within the loop work?
You're not storing the layer directly in an NSArray. You're storing it in a property of your ledInfo object.
The problem is that a CGLayer is not really an Objective-C object, so neither ARC nor the compiler-generated (“synthesized”) property setter will take care of retaining and releasing it for you. Suppose you do this:
CGLayerRef layer = CGLayerCreateWithContext(...);
led_info.cg_layer = layer;
CGLayerRelease(layer);
The cg_layer setter method generated by the compiler just stores the pointer in an instance variable and nothing else, because CGLayerRef isn't an Objective-C object reference. So when you then release the layer, its reference count goes to zero and it's deallocated. Now you have a dangling pointer in your cg_layer property, and when you use it later you crash.
The fix is to write the setter manually, like this:
- (void)setCg_layer:(CGLayerRef)layer {
CGLayerRetain(layer);
CGLayerRelease(_cg_layer);
_cg_layer = layer;
}
Note that it's important to retain the new value before releasing the old one. If you release the old one before retaining the new one, and the new one happens to be the same as the old one, you might deallocate the layer right in the middle!
UPDATE
In response to your edits:
Reference counting works with CGLayerRef. Automatic reference counting (ARC) doesn't. ARC only works with things that it thinks are Objective-C objects ARC does not automatically retain and release a CGLayerRef, because ARC doesn't think a CGLayerRef is a reference to an Objective-C object. An Objective-C object is (generally speaking) an instance of a class declared with #interface, or a block.
The CGLayer Reference says that CGLayer is derived from CFType, the basic type for all Core Foundation objects. (As far as ARC is concerned, a Core Foundation object is not an Objective-C object.) You need to read about “Ownership Policy” and “ Core Foundation Object Lifecycle Management” in the Memory Management Programming Guide for Core Foundation.
The “destination of the store” is an instance variable in your ledInfo object. It's not “the NSArray via the property”. The value doesn't ”make it into the array and back out.” The array gets a pointer to your ledInfo object. The array retains and releases the ledInfo object. The array never sees or does anything with the CGLayerRef. Your ledInfo object is responsible for retaining and releasing any Core Foundation objects it wants to own, like the layer in its cg_layer property.
As I mentioned, if ledInfo doesn't retain the layer (with CFRetain or CGLayerRetain) in its cg_layer setter, it risks the layer being deallocated, leaving the ledInfo with a dangling pointer. Do you understand what a dangling pointer is?

Can't get subview animation to appear even after alloc :)

I have a subview loaded into an UIView. In the subview's .m file I have the following:
- (void)startAnimation {
// Array to hold png images
imageArray = [[NSMutableArray alloc] initWithCapacity:22];
animatedImages = [[UIImageView alloc] initWithImage:viewForImage];
// Build array of images, cycling through image names
for (int i = 1; i < 22; i++){
[imageArray addObject:[UIImage imageNamed:[NSString stringWithFormat:#"image%d.png", i]]];
}
animatedImages.animationImages = [NSArray arrayWithArray:imageArray];
// One cycle through all the images takes 1 seconds
animatedImages.animationDuration = 2.0;
// Repeat forever
animatedImages.animationRepeatCount = 0;
// Add subview and make window visible
[viewForMovie addSubview:animatedImages];
// Start it up
animatedImages.startAnimating;
NSLog(#"Executed");
}
Please be noted that I have in the .h file:
UIImageView *animatedImages;
NSMutableArray *imageArray;
UIView *viewForMovie;
#property(nonatomic,retain)IBOutlet UIView *viewForMovie;
and in the .m file:
#synthesize viewForMovie;
and I have connected viewForMovie to a UIView in IB. I've been on this for several hours now and have tried many variations I've found on the web but cannot get it to work. There are no errors and the other GUI graphics in the subview appear very nicely....but the animation just doesn't appear over top where it should. Also the NSlog reports that the method has in fact been called from the parent. Can anyone see any blaring issues? Thx.
PS: I'm pretty new at this.
Based on the code shown and the behavior you see so far, here are my suggestions:
Make sure the viewForMovie IBOutlet is connected properly in Interface Builder. If it's not connected properly (and so nil), nothing will appear. If you didn't mean to make it an IBOutlet in the first place, then you'll need to manually create it and add it as a subview to self before using it.
Not sure why you have the viewForMovie UIView in the first place. Is this subview's class (let's call it MySubview) a subclass of UIView? You can just show the animation in self instead of adding another subview inside it. Are you going to add more uiviews to this subview besides the viewForMovie?
To get rid of the "may not respond to" warning, declare the startAnimation method in the MySubview.h file (under the #property line):
-(void)startAnimation;
The fact that the warning says "UIView may not respond" also tells you that the parent view has declared newView as a UIView instead of MySubview (or whatever you've named the subview class). Change the declaration in the parent from UIView *newView; to MySubview *newView;.
In the initWithImage, what is "viewForImage"? Is it a UIImage variable or something else?
If all of the images are the same size and fit in the subview as-is, you don't need to set the frame--the initWithImage will automatically size the UIImageView using the init-image dimensions.
Double check that the images you are referencing in the for-loop are named exactly as they are in the code and that they have actually been added to the project.
Finally, you should release the objects you alloc in startAnimation. At the end of the method, add:
[imageArray release];
[animatedImages release];
The only item, however, that I think is actually preventing the animation from appearing right now is item 1.

OCMock with Core Data dynamic properties problem

I'm using OCMock to mock some Core Data objects. Previously, I had the properties implemented with Objective-C 1.0 style explicit accessors:
// -- Old Core Data object header
#interface MyItem : NSManagedObject {}
- (NSString *) PDFName;
- (void) setPDFName:(NSString *)pdfName;
#end
// -- implementation provides generated implementations for both getter and setter
Now I've moved the code to Objective-C 2.0 and want to take advantage of the new #property syntax, and the dynamically-generated method implementations for Core Data objects:
// -- New Core Data object header
#interface MyItem : NSManagedObject {}
#property (nonatomic, retain) NSString *PDFName;
#end
// -- Core Data object implementation
#implementation MyItem
#dynamic PDFName;
#end
However, now when I create a mock item, it doesn't seem to handle the dynamic properties:
// -- creating the mock item
id mockItem = [OCMockObject mockForClass:[MyItem class]];
[[[mockItem stub] andReturn:#"fakepath.pdf"] PDFName]; // <-- throws exception here
The error looks like this:
Test Case '-[MyItem_Test testMyItem]' started.
2009-12-09 11:47:39.044 MyApp[82120:903] NSExceptionHandler has recorded the following exception:
NSInvalidArgumentException -- *** -[NSProxy doesNotRecognizeSelector:PDFName] called!
Stack trace: 0x916a4d24 0x92115509 0x97879138 0x978790aa 0x9090cb09 0x97820db6 0x97820982 0x10d97ff 0x10d9834 0x9782005d 0x9781ffc8 0x20103d66 0x20103e8c 0x20103642 0x20107024 0x20103642 0x20107024 0x20103642 0x20105bfe 0x907fead9 0x977e4edb 0x977e2864 0x977e2691 0x90877ad9 0xbf565 0xbf154 0x107715 0x1076c3 0x1082e4 0x89d9b 0x8a1e5 0x894eb 0x907e81c7 0x978019a9 0x978013da 0x907dd094 0x907ea471 0x9478c7bd 0x9478c1b9 0x94784535 0x5ede 0x326a 0x5
Unknown.m:0: error: -[MyItem_Test testMyItem] : *** -[NSProxy doesNotRecognizeSelector:PDFName] called!
Am doing something wrong? Is there another way to mock a Core Data / object with #dynamic prooperties?
Also responded to your cross-post on the OCMock Forum
Check out http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html.
Basically he suggests abstracting out your Core Data object's interface to a protocol, and using that protocol instead of the class where you pass instances of your core data object around.
I do this for my core data objects. Then you can use mockForProtocol:
id mockItem = [OCMockObject mockForProtocol:#protocol(MyItemInterface)];
[[[mockItem expect] andReturn:#"fakepath.pdf"] PDFName];
Works great! He also suggests creating a non-core data mock implementation of the interface which just synthesizes the properties:
#implementation MockMyItem
#synthesize PDFName;
#end
...
id <MyItemInterface> myItemStub = [[MockMyItem alloc] init] autorelease];
[myItem setPDFName:#"fakepath.pdf"];
I've used this as well, but I'm not sure it adds anything over the mockForProtocol:/stub: approach, and it's one more thing to maintain.
The above answer didn't satisfy me, because I didn't like to create a protocol for that. So I found out that there is an easier way to do that.
Instead of
[[[mockItem stub] andReturn:#"fakepath.pdf"] PDFName]; // <-- throws exception here
Just write
[[[mockItem stub] andReturn:#"fakepath.pdf"] valueForKey:#"PDFName"];
One of solutions is using a protocol, which is intended to substitute it's original interface, but it could be a bit heavy and leads to significant amount of code you should duplicate.
Personally, I found a way to make it lightweight:
Create a simple category, for instance, inside your unit testing file, just before your unit testing class:
#implementation MyItem(UnitTesing)
- (NSString *)PDFName{return nil;};
#end
Also, you can keep it in separate file, but make sure, that this file is not a part of your production target. That is why I prefer to keep it in the same test file, where I want to use it.
The huge advantage of this method, is that you should not copy methods, that are created by XCode to support relationships. Also you can put to this category only methods you are going to call inside your tests.
There are some caveats, though, for example, you should add another methods inside the category, to support setters, when you are going to check, how correct your code changes the properties of your managed object:
- (void)setPDFName:(NSString *)name{};

Resources