Question

I want to be able to have two classes that are responsible to respond to selectors differently depending if the platform is iOS or OSX.

However, I want to have code that uses only one class, and I want to avoid repeating #ifdefs.

Ideally I would want to have 3 classes:

  • UniversalClass
  • iOSSpecificClass
  • OSXSpecificClass

iOSSpecificClass and OSXSpecificClass both extend UniversalClass. All calls would be done to UniversalClass, and that class is responsible to call the respective methods of iOSSpecificClass and OSXSpecificClass.

There are two solutions that I came up with:

@interface UniversalClass : NSObject

+ (void) universalMethod;

@end

@implementation UniversalClass

+(id)forwardingTargetForSelector:(SEL)aSelector {
    #if TARGET_OS_IPHONE
        return [iOSSpecificClass class];
    #else
        return [OSXSpecificClass class];
    #endif
}

@end

The problem with this approach is that UniversalClass promises something in the .h that can or cannot deliver. The warnings also tell us that. Grr. Warnings.

The second approach would be like this:

@implementation UniversalClass

+ (Class)correctClass {
    Class aClass = Nil;

    #if TARGET_OS_IPHONE
        aClass = [iOSSpecificClass class];
    #else
        aClass = [OSXSpecificClass class];
    #endif

    return aClass;
}

+ (void)universalMethod {
    Class masterClass = [UniversalClass correctClass];
    [masterClass universalMethod];
}
@end

The problem with this approach is that I have to perform changes for every method I add and I feel that I am kinda repeating myself without needing.

What are the edge cases I have to pay attention to in both solutions? Is there any better solution than those?

Was it helpful?

Solution

One option is to have a common header file and two different implementations for two targets (one for OSX and another for iOS) that both import and implement the header methods.

Something like this:

enter image description here

OTHER TIPS

Another alternative is to examine if you really need two classes. One @interface and two @implementations (potentially in separate files) is a pattern that I've seen.

Something like (this from CodeRunner where I did my test):

#import <Foundation/Foundation.h>

// #define iPHONE 1

@interface MyClass : NSObject

- (NSString*) someString;

- (BOOL) aMethod: (NSString*) inString;

@end



// common implementations here
@interface MyClass (common)
- (NSString*) commonString;
@end

@implementation MyClass (common)

- (NSString*) commonString
{
    return @"same";
}
@end

#ifdef iPHONE

// iPhone specific implementations
@implementation MyClass

- (BOOL) aMethod: (NSString*) inString
{
    return [inString isEqualToString: @"iPhone Impl"];
}

- (NSString*) someString
{
return @"iPhone Impl";
}

@end

#else

@implementation MyClass

- (BOOL) aMethod: (NSString*) inString
{
    return [inString isEqualToString: @"iPhone Impl"];
}

- (NSString*) someString
{
return @"OS X Impl";
}

@end

#endif

// test

int main(int argc, char *argv[]) {
    @autoreleasepool {
    MyClass * obj = [[MyClass alloc] init];

    NSLog(@"is iPhone? %@", [obj aMethod: [obj someString]] ? @"YES" : @"NO");
    NSLog( @"string: %@", [obj someString] );
}
}

You could obviously do this more elegantly by having two .m files and putting one implementation in each (iPhone in one, OS X in the other); or three if you are going to have common routines that are shared by both.

Anyway, just an alternative way to get the same / similar effect - single interface to differing functionality.

You could go with something like this:

@implementation UniversalClass

static Class class;

+ (void)load
{
  class = [UniversalClass correctClass];
}

+ (Class)correctClass {
    Class aClass = Nil;

    #if TARGET_OS_IPHONE
        aClass = [iOSSpecificClass class];
    #else
        aClass = [OSXSpecificClass class];
    #endif

    return aClass;
}

+ (void)universalMethod {
    [class universalMethod];
}

This will keep the promise you made on the .h by implementing the corresponding method (no warnings) and get the right class only once.

How about just ignoring the warning for the specific case of your forwardingTargetForSelector: version? It's like saying “hey, I know what I'm doing!” :-)

Add something like these #pragma calls around your @implementation line:

...

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation UniversalClass
#pragma clang diagnostic pop

...

See this answer here on Stack Overflow.

The solution that you are proposing is the Class Cluster pattern, which is quite common in Cocoa (e.g. it is used in NSArray, NSValue, etc). Class clusters are classes that return a private subclass from their constructor instead of an instance of the class that was requested. Here is how you might implement that in this case:

MyClass.h

@interface MyClass : NSObject

- (void)someMethod;

@end

MyClass.m

@implementation MyClass

+ (id)alloc
{
    if (self == [MyClass class])
    {
        #if TARGET_OS_IPHONE
            return [MyClass_iOS alloc];      
        #else
            return [MyClass_Mac alloc];
        #endif
    }
    else
    {
        return [super alloc];
    }
}

- (void)someMethod
{
    //abstract, will be overridden
}  

@end

MyClass_iOS and MyClass_Mac would be declared in separate files and privately imported in the McClass.m file.

This seems like a pretty elegant solution at first, but it's not really appropriate for this situation. Class clusters are great for swapping class implementation at runtime when you don't know which implementation you want at compile time (good examples would be supporting different iOS versions, or universal apps that behave differently on iPad and iPhone) but for Mac/iOS we know at compile time which code we need, so introducing a cluster of 3 separate classes is redundant.

This solution doesn't really offer any benefit over the ones suggested by https://stackoverflow.com/users/145108/dad or https://stackoverflow.com/users/3365314/miguel-ferreira because we still have to branch the import code:

#if TARGET_OS_IPHONE
    #import "MyClass_iOS.h"
#else
    #import "MyClass_Mac.h"
#endif

We could solve that by having a single header for both MyClass_iOS and MyClass_Mac (which was Miguel's solution) or by having both implementations in the same file (which was Dad's solution) but then we've just built a layer on top of one of the solutions you already rejected.

Personally, I would just use a single .m file with three clearly delineated sections:

@interface MyClass

#pragma mark -
#pragma mark Common code

- (void)someMethod1
{

}

#pragma mark -
#pragma mark iOS code
#if TARGET_OS_IPHONE

- (void)someMethod2
{

}

#pragma mark -
#pragma mark Mac code
#else

- (void)someMethod2
{

}

#endif

@end

This avoids creating unnecessary classes and gives you freedom to easily have shared methods or separate implementations for each platform without exposing any of that in the interface.

If the classes for the two platforms definitely won't have any code in common, I'd probably opt for Miguel's solution, which is very clean.

I don't accept the "user confusion" explanation. You'd basically have these three files:

MyClass.h
MyClass_iOS.m
MyClass_Mac.m

I think if someone is confused by what that means, they shouldn't be working on your code base ;-)

You could also combine this with the class cluster approach if you did want to inherit shared code between the two platforms, in which case your MyClass.m file would contain both the shared implementation and the private interface:

@interface MyClass_Private : MyClass

- (void)somePlatformSpecificMethod;

@end

@implementation MyClass

+ (id)alloc
{
    if (self == [MyClass class])
    {
        return [MyClass_Private alloc];      
    }
    else
    {
        return [super alloc];
    }
}

- (void)someSharedMethod
{
    //concrete implementation
}

@end

And your project structure would look more like this:

MyClass.h
MyClass.m
MyClass_Private_iOS.m
MyClass_Private_Mac.m

Hope that helps!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top