0

I found this StackOverflow question on friend classes in Objective-C which was similar to the issue I'm trying to solve, but not quite the same. So I'd like to extend their example.

Suppose I want the ability to hand a Monkey multiple Bananas and he'll keep an array of all of his own Bananas. When I tell him to eat, he eats from his array. I can then ask him to give me a Banana at random:

Banana.h

@interface Banana : NSObject
- (BOOL)isPeeled;
@end

Banana.m

#import "Banana.h"

@implementation Banana
{
    @private
    BOOL peeled;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        peeled = NO;
    }
    return self;
}

- (void)peel // PRIVATE METHOD
{
    peeled = YES;
}

- (BOOL)isPeeled
{
    return peeled;
}
@end

Monkey.h

#import "Banana.h"

@interface Monkey : NSObject
- (void)give:(Banana *)aBanana;
- (void)eat;
- (Banana *)getBanana;
@end

Monkey.m

#import "Monkey.h"

@implementation Monkey
{
    @private
    NSMutableArray *bananas;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        bananas = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)give:(Banana *)aBanana
{
    [bananas addObject:aBanana];
}

- (void)eat
{
    BOOL hungry = YES;
    for (int i = 0; i < bananas.count; i++)
    {
        if (!banana.isPeeled)
        {
            hungry = NO;
            [banana peel];
            break;
        }
    }
    if (hungry)
    {
        NSLog(@"Monkey ANGRY!");
    }
}

- (Banana *)getBanana
{
    int r = arc4random_uniform(bananas.count);
    return [bananas objectAtIndex:r];
}
@end

In this case, Banana.h and Monkey.h would be public headers in a library I'm distributing. The intended use of my library would look something like so:

ClientApp.m

#import "MonkeyLib/MonkeyLib.h"

@implementation SomeClass

- (void)someMethod
{
    Banana *oneBanana = [[Banana alloc] init];
    Monkey *oneMonkey = [[Monkey alloc] init];
    [oneMonkey give:oneBanana];
    [oneMonkey eat];
    Banana *twoBanana = [oneMonkey getBanana];
    if (twoBanana.isPeeled)
    {
        NSLog(@"No sad monkeys!");
    }
}

@end

The immediate issue with the code above is that my library won't compile because no interface for the class Banana defines a peel method (as utilized by Monkey.m)

The StackOverflow question I linked at the beginning of this question suggests creating a third header file:

BananaPrivate.h

@interface Banana (PrivateMethods)
- (void)peel;
@end

Then in Monkey.m I can import BananaPrivate.h and I can suddenly call the peel method. However this fails because when I attempt to import BananaPrivate.h I create a collision -- I've already defined Banana when I imported Banana.h in Monkey.h

I can't remove Banana.h from Monkey.h, because one of the methods on Monkey returns an instance of type Banana!

I found a quirky work-around by sub-classing Banana like so:

InternalBanana.h

@interface InternalBanana : Banana
- (void)peel;
@end

Then within Monkey.m I can safely #import "InternalBanana.h". However this has two issues:

  1. It only works for Bananas instantiated by my Monkey (if a user of my library instantiates a Banana and passes it to my Monkey, it will be a Banana and not an InternalBanana)
  2. I have to create an InternalBanana.m file, but within this file I cannot define peel. Attempting to define peel within InternalBanana.m will throw an error that peeled is not defined, since peeled is a private variable of the super class. If I define peel within InternalBanana.m and it only calls [super peel], that will also fail since Banana does not define a peel method. While this works, it does cause Xcode to throw a warning saying that there is no method definition for peel (but it builds and runs despite the warning)

What is the proper solution to this monkey business?

stevendesu
  • 15,753
  • 22
  • 105
  • 182
  • Objective-C is a dynamic language. You can achieve this for compiling, but there’s no way you can prevent an existing selector to be called at runtime. So probably not worth wasting your time... – Macmade Oct 01 '19 at 21:33
  • 2
    The `Banana (PrivateMethods)` category adds method `peel` to the existing `Banana` class. It doesn't define another `Banana` class. I tried your code and don't see a collision. – Willeke Oct 01 '19 at 21:58
  • @Willeke Apparently my issue was that I removed the `(PrivateMethods)` bit. Turns out that's required, even though Xcode didn't syntax highlight it, color it, or seem to know what it was. I added it back in and it worked. Removed it, error. – stevendesu Oct 01 '19 at 22:03
  • Beside this: `@private` is meaningless. It only restricts the direct access to a *var*. But this is already restricted to the actual module, because you define it in the class implementation. Moreover, unrelated to your Q, `-eat` works strangely on `hungry`. It is always true. – Amin Negm-Awad Oct 02 '19 at 10:33
  • @AminNegm-Awad Good catch, simple typo on my part. After eating the monkey should no longer be hungry :) – stevendesu Oct 03 '19 at 13:52

0 Answers0