0

I have a set of someProtocol objects, I'd like to make a set from a NSString nested property on this object:

// Protocol SomeProtocol
@protocol SomeProtocol<NSObject>

@property(nonatomic, readonly) id<SomeSubProtocol> someSubProtocolObject;

@end

// Protocol SomeSubProtocol
@protocol SomeSubProtocol<NSObject>

@property(nonatomic, readonly) NSString *Id;

@end

I have a set of SomeProtocols:

NSSet<id<SomeProtocol>> *setOfSomeSubProtocols;

I'd like to get a NSSet of the Id properties:

NSSet<NSString *> *idSet = ?; // Calculate from arrayOfSomethings.

I've tried:

idSet = [setOfSomeSubProtocols valueForKeyPath @"someSubProtrocolObject.id"];

But I'd prefer something that throws a compiler error if the properties change...

Rik
  • 1,870
  • 3
  • 22
  • 35
  • https://stackoverflow.com/questions/6127638/nsarray-equivalent-of-map ? One of the implementation of `map()` in Objective-C might be able throws an compiler error if not possible because the property has changed... – Larme Aug 16 '21 at 08:00
  • Also, there is nothing wrong with a manual loop on `setOfSomeSubProtocols`. – Larme Aug 16 '21 at 13:56

2 Answers2

1

Define the following somewhere:

@protocol KeyPath <NSObject> @end
typedef NSString<KeyPath> KeyPath;

NS_INLINE KeyPath *GetKeyPath(__unused unsigned long val, NSString *str) { return (KeyPath *)str; }

#define PRIVATE_KEYPATH(obj, key) GetKeyPath(sizeof(obj.key), @#key)
#define PROTOCOL_KEYPATH(proto, key) PRIVATE_KEYPATH(((id<proto>)nil), key)

then use it like this:

[setOfSomeSubProtocols valueForKeyPath:PROTOCOL_KEYPATH(SomeSubProtocol, Id)];

This will give you a compile error if the type name of SomeSubProtocol or the property name Id changes.

P.S.

id is a bad property name, because it's also a built-in type name. Consider using identifier property name instead.

Eugene Dudnyk
  • 5,553
  • 1
  • 23
  • 48
1

In Swift, I would have suggested to use map(), but it doesn't exists in Objective-C. You can search or implement yourself a version of map in Objective-C.

But, what you can do, is a manual loop, since it's a normal approach of the issue.

__block NSMutableSet *ids = [[NSMutableSet alloc] init];
[setOfSomeSubProtocols enumerateObjectsUsingBlock:^(id<SomeProtocol>  _Nonnull obj, BOOL * _Nonnull stop) {
    [ids addObject:[[obj someSubProtocolObject] identifier]];
}];

It's quite simple and does the job.

You could as said before, implement a map on NSSet, something like this:

@implementation NSSet(Map)

-(NSSet *)mapWithBlock:(id (^)(id))block {
    __block NSMutableSet *set = [[NSMutableSet alloc] init];
    [self enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        id mappedObject = block(obj);
        [set addObject:mappedObject];
    }];
    return set;
}

With call:

NSSet *ids = [setOfSomeSubProtocols mapWithBlock:^id (id<SomeProtocol> object) {
    return [[object someSubProtocolObject] identifier];
}];
NSLog(@"ids: %@", ids);
Larme
  • 24,190
  • 6
  • 51
  • 81