I’m writing some library code at the moment that needs to work on both Mac OS X and iOS. The APIs I need to use on each platform are different, so I need different code on each platform. I also happen to think that putting both versions of the code in the same implementation file is icky.
Here’s what I’m doing. I define an abstract class FZAPlatformStrategy, that defines the interface and knows how to choose an appropriate subclass depending on the platform.
FZAPlatformStrategy.h
@interface FZAPlatformStrategy : NSObject { @private } - (void)doThing; + (FZAPlatformStrategy *)newPlatformStrategy; @end
FZAPlatformStrategy.m
@implementation FZAPlatformStrategy - (void)doThing { [[NSException exceptionWithName: @"FZAPlatformStrategyAbstractClassException" reason: @"Use +[FZAPlatformStrategy newPlatformStrategy] to get an appropriate subclass" userInfo: nil] raise]; } + (FZAPlatformStrategy *)newPlatformStrategy { id strategy = nil; #if TARGET_OS_IPHONE strategy = [[FZAPlatformStrategyiPhone alloc] init]; #else strategy = [[FZAPlatformStrategyMac alloc] init]; #endif return platformStrategy; } @end
OK, so now as you can imagine there is a class FZAPlatformStrategyiPhone that uses the iOS APIs, and another FZAPlatformStrategyMac that uses the OS X APIs. Each of these overrides the -doThing method to provide the appropriate platform-specific implementation. Of course, to get these to compile I conditionally define the whole @implementation contents of each based on the target platform.
The library code that needs to interface with this code just calls [FZAPlatformStrategy newPlatformStrategy]; to get an instance, and then because we’ve encapsulated the platform-specific behaviour it can use whatever it gets in a consistent way. Need to add another platform, say GNUStep/Linux? Define a new FZAPlatformStrategy subclass, and change +newPlatformStrategy to know when to return an instance of that subclass.
The remaining bit of ugly is the preprocessor conditional in +newPlatformStrategy. I decided to leave this as-is: it is at least highly localised ugly. The other approach I considered and discarded was to look for a platform-specific class like UIDevice being non-Nil, but then I realised that someone would probably implement that on the other platform and mess up the test.
Any other solutions for doing the “which platform am I on” test gratefully received.
Another possible way of doing this would be to implement a registration class method on FZAPlatformStrategy which would be called by the appropriate subclass to set which class should be used when allocing new objects. So, you’d have something like:
FZAPlatformStrategy.m:
@implementation FZAPlatformStrategy
static Class platformStrategyClass = nil;
+ (void)registerPlatformStrategyClass:(Class)inClass {
platformStrategyClass = inClass;
}
+ (FZAPlatformStrategy *)newPlatformStrategy {
NSAssert(platformStrategyClass != nil, @”No platform strategy subclass was registered”);
return [[[platformStrategyClass alloc] init] autorelease];
}
@end
Then, each subclass would register itself in its own +load method:
FZAPlatformStrategyiPhone.m:
#if TARGET_OS_IPHONE
@implementation FZAPlatformStrategyiPhone
+ (void)load {
[FZAPlatformStrategyClass registerPlatformStrategyClass:self];
}
@end
#endif
FZAPlatformStrategyMac.m:
#if TARGET_OS_MAC
@implementation FZAPlatformStrategyMac
+ (void)load {
[FZAPlatformStrategyClass registerPlatformStrategyClass:self];
}
@end
#endif
The advantages are that a) FZAPlatformStrategy doesn’t need to directly know about any of the possible subclasses, and b) you only need to ifdef out the implementation of each class according to the platform it’s being compiled for.
Nice solution!