Supporting Leopard while developing in Snow Leopard

Apparently Apple’s Cocoa method availability documentation is not enough to determine whether something will work on an earlier version of Mac OS X. That is, avoiding 10.6-only methods doesn’t ensure that your application will also work flawlessly in 10.5. As a first-time Mac developer programming an application on Snow Leopard that is also targeted for Leopard, I was initially overwhelmed with the issues I encountered when I sent out my first few beta releases. Initially it didn’t run, which turns out because I lapse and used a 10.6 only method. After that issue was fixed, then a number of functionalities doesn’t work — some dialog windows simply won’t show and the log files only showed “failed to load NIB file” error without anything more actionable like an unknown method exception. It turned out that I had to set the NSDebugEnabled global variable (defined in NSDebug.h) to YES so that the seemingly arbitrary error becomes more informative and thus able to diagnose the root cause better.

Why do I want to still support Leopard? Primarily because Snow Leopard is only about 48% of the installed base (Adium update statistics, week 12 year 2010). Additionally Leopard is the last version of the OS that still supports the PowerPC processor and it its just a compiler option to also support PowerPC. Why not support Tiger as well? The initial reason was because I want to take advantage of the Core Data automatic schema migration feature that is missing in Tiger. In the progress of developing the app, it turns out that a number of open-source frameworks that I rely on (and one of which is critical to the app’s functionality) are also Leopard-only.

PastedGraphic.zM8UTJF17qoq.jpg

These were the issues that I found and fixed during the first few rounds of Leopard testing:

  • Method [NSObject awakeFromNib] is not defined in Leopard but defined in Snow Leopard.
  • Core Data’s “IN” predicate only supports NSArray as the parameter in Leopard and not NSSet.
  • QCView is not CoreAnimation friendly in Leopard.

Method [NSObject awakeFromNib] a.k.a the shadow initializer.

You’ll probably define this method when you have a top-level object in your Interface Builder file that isn’t derived from NSWindowController. According to the method documentation, you’ll need to call the superclass’ implementation as the first thing in this method. This is all fine in Snow Leopard but in plain Leopard (Mac OS X 10.5), NSObject doesn’t provide any implementation of this method — not even an empty method body — and thus the program will simply crash on Leopard when the NIB file is loaded and the object is initialized because it tries to call the undefined [NSObject awakeFromNib] method.

To solve this you’ll need to check whether NSObject (or any other framework classes that you inherit from) defines awakeFromNib or not. Furthermore you can’t use [super respondsToSelector:] method to check this because it has the exact same effect as [self respondsToSelector:] and will return true. Instead you’ll need to call [NSObject instancesRespondToSelector:] method, like the example below:

-(void) awakeFromNib {
if ([NSObject instancesRespondToSelector:@selector(awakeFromNib)]) {
[
super awakeFromNib];
}
// continue on your merry way…
}

Yes, it becomes tricky since if you’ll need to change your base class then you will also need to search for these kind of method calls and update the class name accordingly. According to the docs, calling [[self superclass] instancesRespondToSelector:] also won’t work, which doesn’t make things any better. I suggest to add the above boilerplate code to all of your “root” classes (base classes that are directly derived from Apple’s classes), that way you don’t need to worry checking for awakeFromNib implementation further down the inheritance hierarchy and yet future-proofing your code. Please do take care to replace NSObject in the template code above with the proper superclass of your class.

Core Data’s “IN” predicate and NSSet

Another potential problem is that Leopard’s version of Core Data (when the underlying persistent storage is SQLite), doesn’t support set objects as a parameter. That is when you have a predicate such as attributeName IN (%@), you can’t pass an NSSet object to fill the parameter, which otherwise works fine in Snow Leopard. This issue also reveals itself if you have a fetched attribute that uses a predicate that compares a relationship attribute with another relationship attribute since relationship attributes are expressed as set objects. For example a fetched property with a predicate similar to “myAttribute IN $FETCH_SOURCE.allowedAttributes” will work in Snow Leopard but crashes in Leopard.

I came into this conclusion due to a similar issue with iPhone’s implementation of CoreData when used with SQLite backing store. In the iPhone (I’m not really sure which OS version discussed in that Stack Overflow question), IN predicates will work only if the list of items is provided in an NSArray instance and not NSSet. Apparently Leopard’s implementation suffers the same bug.

The workaround? Implement your own method for the fetch property and detect whether you run on Leopard or Snow Leopard. When your application is run in Snow Leopard or greater, just use the set instance as a parameter but when your application is run in Snowless Leopard (otherwise known as Mac OS X 10.5), convert the NSSet object into an NSArray. Be sure to leave out the deleted objects in the resulting array. Sample code below.

-(NSArray*) myFetchedProperty {
// … (note that some code parts that are not relevant to the topic are omitted) …
// … get the context etc …NSArray* result = nil;
SInt32 version = 0;
Gestalt( gestaltSystemVersion, &version );
if (version >= 0x1060) {
// for Mac OS X 10.6+
predicateString = @“SELF IN %@.allowedEntities”;
fetchRequest.predicate = [NSPredicate predicateWithFormat:predicateString,self];
} else {
// compatibility for Mac OS X 10.5
// otherwise will result in error “unimplemented SQL generation for predicate”
// looks like Mac OS X 10.5 SQLite requires “IN” clauses to have an array as a parameter
// instead of a set
// refer to: http://stackoverflow.com/questions/1436032/does-coredata-on-iphone-support-in-predicatesNSSet* allowedEntities = self.allowedEntities;
NSMutableArray* allowedEntitiesArray = [NSMutableArray arrayWithCapacity:allowedEntities.count];
for(id obj in allowedEntities) {
if (![obj isDeleted]) {
[allowedEntitiesArray addObject:obj];
}
}
predicateString = @“SELF IN %@“;
fetchRequest.predicate = [NSPredicate predicateWithFormat:predicateString, allowedEntitiesArray];
}

// … fetch the request and initialize the ‘result’ variable with the result…
return result;
}

 

Quartz Composer is not Core Animation friendly in Leopard

The last take away item from this round is do not embed a QCView instance inside a Core Animation view. What I mean by a Core Animation view is any view that has the Wants Core Animation Layer option enabled in Interface Builder (refer to the screenshot below). You’ll have to leave that option unchecked for the QCView instance and all the enclosing superviews (including the window). In Snow Leopard checking this seems to be okay but in plain Leopard, your quartz composition will not render and only show a blank gray box in place of the quartz composer view.
WantsCoreAnimationLayer.LYWOYv6p5nn0.jpg

Leopard Testing

Initially I intended to just ask my girlfriend to test my app on Leopard (she has a MacBook Air that still runs Leopard) and intended her machine to be the primary source of Leopard-based bug reports. I didn’t anticipate that there would be major issues in Leopard. Alas it turns out that I need a better solution than this due the round-trip time between a fix and subsequent bug report. Not to mention bugging her all the time for minor issues and changes.

Two articles that helped me on Leopard testing are on virtualizing Leopard client and making good use of your Leopard upgrade DVD. I’m not recommending this route if you already have a spare Mac that you can use, but buying another machine just for occasional testing doesn’t really make business sense for me at this point of time — in terms of both money needed and space taken by the new machine. That said, if you have a working PowerPC-based Mac that can still run Leopard well and you want to get rid of it at a really, really low price, please do contact me. I’ve almost got my hands on a used Mac Mini that was on sale for about US$ 300 (seller said prime condition, 1GB RAM, PowerPC, and accessories), but I was too late to reply to the ad. Oh well, by now I guess you already know which route I took for Leopard testing ;-)

Thanks and have fun!



Avoid App Review rules by distributing outside the Mac App Store!


Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

Avoid Delays and Rejections when Submitting Your App to The Store!


Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

0 thoughts on “Supporting Leopard while developing in Snow Leopard

Leave a Reply