Sunday, October 17, 2010

Handling Initialization Failure

A few months ago a friend of mine asked me the following question: "What should I do if I want to return nil in one of my initializers?" Returning nil in an initializer is nothing fancy. I do it all the time. However, there are a few rules you should be aware of.
  • Only return nil (in an initializer) if you can't perform the initialization. If a caller passes your initializer a path to a file that does not exist and the object can't work without a file it would make sense to return nil.
  • Don't return nil if you can find a nice "workaround" for a problem during initialization such as replacing a missing argument with a sensible default value.
  • If you want to return nil and you haven't sent a initialization message to super yet perform the following steps:
    • cleanup any resources you may have created,
    • call [self release]; and then
    • return nil
  • If you want to return nil and already have sent a initialization message to super (which returned nil) perform the following steps:
    • cleanup any resources you may have created,
    • return nil
If you return nil you must have a good reason. It is a good idea to let the caller know what went wrong during the initialization process. In Objective-C/Foundation we can simply use NSError to pass the cause of a problem back to the caller. Imagine a class called Person. A Person has a first and a last name. One can create a Person by using -initWithFirstName:lastName:error:. If the passed first and/or last name is nil -initWithFirstName:lastName:error: creates an error object, sends release to self and returns nil.

#import "Person.h"

enum {
   PersonErrorCodeFirstLastNameNotValid = 0
};
typedef NSInteger PersonErrorCodes;

@implementation Person

@synthesize firstName, lastName;

- (id)initWithFirstName:(NSString *)initFirstName 
               lastName:(NSString *)initLastName 
                  error:(NSError **)error {
   if(initFirstName == nil || initLastName == nil) {
      if(error != NULL) {
         NSMutableDictionary *u = [NSMutableDictionary dictionary];
         [u setValue:@"first/last name not valid." 
              forKey:NSLocalizedDescriptionKey];
         PersonErrorCodes c = PersonErrorCodeFirstLastNameNotValid;
         *error = [NSErrorerrorWithDomain:@"com.example.unique" 
                                     code:c 
                                 userInfo:u];
      }
      [self release];
      return nil;
   }
   self = [super init];
   if(self != nil) {
      self.firstName = initFirstName;
      self.lastName = initLastName;
   }
   returnself;   
}

- (id)initWithFirstName:(NSString *)initFirstName 
               lastName:(NSString *)initLastName {
   return [self initWithFirstName:initFirstName 
                         lastName:lastName 
                            error:NULL];
}

- (id)init {
   return [self initWithFirstName:[NSString string
                         lastName:[NSString string]];
}

- (void)dealloc {
   self.firstName = nil;
   self.lastName = nil;
   [super dealloc];
}

@end

As you can see, this class has three initializers: -init, -initWithFirstName:lastName: and -initWithFirstName:lastName:error:. The designated initializer is -initWithFirstName:lastName:error:, which is used by a caller which is interested in the cause of a failed initialization. -initWithFirstName:lastName: can be used by a caller which only wants to create a Person and see if it worked or not.

Tuesday, August 17, 2010

Core Data Editor 3.0 released

Yesterday I finally released Core Data Editor 3.0. It took me about ten months to get from a beta version of Core Data Editor 3.0 to the final version. What is new:

  • Core Data Editor automatically observes the file system and tries to figure out when a persistent store file is changed by another application. If Core Data Editor detects a change it automatically reloads the file and refreshes its UI. Many thanks to Stuart Connolly, the developer of SCEvent, which makes observing the file system very easy.
  • Better support for iOS applications: In Core Data Editor 2 you could only specify the location of the iPhone Simulator directory, globally. This makes no sense anymore. Apple changed the locations where the Simulator places application data which broke Core Data Editor. Now you have to specify the location of the applications folder in each configuration. This allows you to use Core Data Editor for different versions of the SDK.
  • Updates are now delivered via Sparkle.
  • Help is now available and integrated in the app and will be constantly improved.

Core Data Editor Website

The following screen cast shows Core Data Editor and a third party app. As I add objects in the third party app Core Data Editor refreshes it's UI to reflect the changes. Neat - isn't it?

SyncThumbnail.png

(Click on the image above to watch the screen cast.)

Starting with version 3.0 Core Data Editor costs $20. Everyone who already donated $20 will receive a free license for Core Data Editor 3.0. Just drop me an e-mail if I forgot you. I hope you like the latest version of Core Data Editor.

Core Data Editor Website

Sunday, July 18, 2010

Drawing Patterns

Imagine you have a nice wooden texture that you want to use in some kind of navigation bar. You want the wooden texture to be repeatedly drawn along the x-axis. Your first approach might look something like this.

#import "PatternView.h"

@implementation PatternView

- (void)drawRect:(NSRect)dirtyRect {
   [superdrawRect:dirtyRect];

   NSImage *bg = [NSImageimageNamed:@"bg"];
   NSColor *backgroundColor = [NSColorcolorWithPatternImage:bg];
[backgroundColor set];
   NSRectFill([selfbounds]);
}

@end

This might not be what you have expected. Let me show you how the above code behaves in action.

Pattern1-Web.jpg

(Click on the image above to see the video)

As you can see when resizing the window the displayed texture is changing as well. This is because of the fact that patterns are always drawn relative to the containing window. Of course this works fine for patterns that should repeat along both axes. In this case however, we don't want that. Let me show you how to fix that.

#import "PatternView.h"

@implementation PatternView

- (void)drawRect:(NSRect)dirtyRect {
   [superdrawRect:dirtyRect];
   NSSize size = [selfbounds].size;

   // Create a new image and make it as big as the view
   NSImage *bigImage = [[[NSImagealloc] initWithSize:size] autorelease];

   // Prepare for drawing onto the new image
   [bigImage lockFocus];

   NSImage *bg = [NSImageimageNamed:@"bg"];
   NSColor *backgroundColor = [NSColorcolorWithPatternImage:bg];
   [backgroundColor set];
   NSRectFill([selfbounds]);

   [bigImage unlockFocus];

   // Now draw the image to the view
   [bigImage drawInRect:[selfbounds
               fromRect:NSZeroRect 
              operation:NSCompositeSourceOver 
               fraction:1.0f];
}

@end


The trick is to create a new empty image and size it correctly. Then you simply have to draw as shown in the first example. This results in exactly what we want.

Pattern2-Web.jpg
(Click on the above image to see the video)

Feel free to download the examples.

 

Sunday, May 16, 2010

Cocoa Tip: Detecting Arrow Down Key Events

We all know the Spotlight Search item in the top right corner of the screen. You can enter something and Spotlight instantly finds all matching files. The results view below the search field can be accessed by pressing the down arrow key. Usually pressing the down arrow key has the effect that the cursor jumps to the end of the text field's content. How can we override the default behavior? One solution would be to subclass NSTextView and provide an instance of this text view as the field editor of the cell. Luckily we have an alternative solution for this:

   [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *event) {
      NSString *characters = [event characters];
      unichar character = [characters characterAtIndex:0];
      if(character == NSDownArrowFunctionKey) {
         NSLog(@"key down");
      } 
      return event;
   }];

This solution looks pretty straight forward. We could also return an alternative event. I don't want to say that this is my final solution but I am happy to see that common things are easy with Cocoa.

Edit: You can remove a monitor by calling +removeMonitor: (NSEvent).