Transform (or copy) an object to a subclass instance in Objective-C
Asked Answered
A

2

3

I want to transform an instance of an object into an instance of a subclass of that object class, so that I can use the additional methods and properties of that subclass, in Objective-C.

How can I do this in a way that does not require me to hardcode the properties of that object class in a copy method?

Autry answered 6/12, 2014 at 20:52 Comment(1)
In general, no, there is not a way to do that. The scheme you propose below will work for some objects, but is far from foolproof and should only be used in "production" code if you have carefully verified that the source and destination classes "expose" all important settings as settable properties.Noellenoellyn
A
6

It is not possible to transform an object into an instance of a subclass in Objective-C. However, with the class below you can supply an instance of both the object and the subclass and have the values of all properties copied to the subclass instance. This implementation works with both Objective-C object types and C primitives. You do not have to specify (or indeed even determine) the properties that need to be copied, providing you know that the important variables are visible and can be set (i.e., there are no properties that are exposed as "read only" or not exposed at all, whose values cannot be recalculated by the class). This method is thus relatively robust for known classes and will not require updating to support future changes you make in your object class that fit these parameters. It is iOS 8 compatible.

This class provides four class methods:

+ (id) copyObject:(id)object toSubclassObject:(id)subclassObject

Copies all properties of object to subclassObject. If the subclassObject is not a subclass of object, nil is returned.

+ (NSDictionary *) propertiesOfObject:(id)object;

Returns a dictionary of all visible properties of an object, including those from all its superclasses (other than NSObject).

+ (NSDictionary *) propertiesOfClass:(Class)class;

Returns a dictionary of all visible properties of a class, including those from all its superclasses (other than NSObject).

+ (NSDictionary *) propertiesOfSubclass:(Class)class;

Returns a dictionary of all visible properties that are specific to a subclass. Properties for its superclasses are not included.

Header:

//  SYNUtilities.h

#import <Foundation/Foundation.h>

@interface SYNUtilities : NSObject
+ (id) copyObject:(id)object toSubclassObject:(id)subclassObject;
+ (NSDictionary *) propertiesOfObject:(id)object;
+ (NSDictionary *) propertiesOfClass:(Class)class;
+ (NSDictionary *) propertiesOfSubclass:(Class)class;
@end

Implementation:

#import "SYNUtilities.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation SYNUtilities
+ (id) copyObject:(id)object toSubclassObject:(id)subclassObject
{
    if (![[subclassObject class] isSubclassOfClass:[object class]]) {
        return nil;
    }

    NSDictionary * properties = [self propertiesOfObject:object];
    NSLog(@"Properties of %@:\n%@", [object class], properties);

    for (NSString * property in properties) {
        SEL selector = NSSelectorFromString(property);
        if (selector) {
            id value = [object valueForKey:property];
            [subclassObject setValue:value forKey:property];
        }
    }
    return subclassObject;
}

+ (NSDictionary *) propertiesOfObject:(id)object
{
    Class class = [object class];
    return [self propertiesOfClass:class];
}

+ (NSDictionary *) propertiesOfClass:(Class)class
{
     if (class == NULL) {
        return nil;
    }

   NSMutableDictionary * properties = [NSMutableDictionary dictionary];
    [self propertiesForHierarchyOfClass:class onDictionary:properties];
    return [NSDictionary dictionaryWithDictionary:properties];
}

+ (NSDictionary *) propertiesOfSubclass:(Class)class
{
    if (class == NULL) {
        return nil;
    }

    NSMutableDictionary *properties = [NSMutableDictionary dictionary];
    return [self propertiesForSubclass:class onDictionary:properties];
}

+ (NSMutableDictionary *)propertiesForHierarchyOfClass:(Class)class onDictionary:(NSMutableDictionary *)properties
{
    if (class == NULL) {
        return nil;
    }

    if (class == [NSObject class]) {
        // On reaching the NSObject base class, return all properties collected.
        return properties;
    }

    // Collect properties from the current class.
    [self propertiesForSubclass:class onDictionary:properties];

    // Collect properties from the superclass.
    return [self propertiesForHierarchyOfClass:[class superclass] onDictionary:properties];
}

+ (NSMutableDictionary *) propertiesForSubclass:(Class)class onDictionary:(NSMutableDictionary *)properties
{
    unsigned int outCount, i;
    objc_property_t *objcProperties = class_copyPropertyList(class, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = objcProperties[i];
        const char *propName = property_getName(property);
        if(propName) {
            const char *propType = getPropertyType(property);
            NSString *propertyName = [NSString stringWithUTF8String:propName];
            NSString *propertyType = [NSString stringWithUTF8String:propType];
            [properties setObject:propertyType forKey:propertyName];
        }
    }
    free(objcProperties);

    return properties;
}

static const char *getPropertyType(objc_property_t property) {
    const char *attributes = property_getAttributes(property);
    char buffer[1 + strlen(attributes)];
    strcpy(buffer, attributes);
    char *state = buffer, *attribute;
    while ((attribute = strsep(&state, ",")) != NULL) {
        if (attribute[0] == 'T' && attribute[1] != '@') {
            // A C primitive type:
            /*
             For example, int "i", long "l", unsigned "I", struct.
             Apple docs list plenty of examples of values returned. For a list
             of what will be returned for these primitives, search online for
             "Objective-c" "Property Attribute Description Examples"
             */
            NSString *name = [[NSString alloc] initWithBytes:attribute + 1 length:strlen(attribute) - 1 encoding:NSASCIIStringEncoding];
            return (const char *)[name cStringUsingEncoding:NSASCIIStringEncoding];
        }
        else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
            // An Objective C id type:
            return "id";
        }
        else if (attribute[0] == 'T' && attribute[1] == '@') {
            // Another Objective C id type:
            NSString *name = [[NSString alloc] initWithBytes:attribute + 3 length:strlen(attribute) - 4 encoding:NSASCIIStringEncoding];
            return (const char *)[name cStringUsingEncoding:NSASCIIStringEncoding];
        }
    }
    return "";
}

@end
Autry answered 6/12, 2014 at 20:52 Comment(0)
I
3

I needed to create a subclass of NSTextFieldCell, used in an NSTableView, and wanted to keep the properties intact that were set for the cell in Interface Builder.

I solved the task by using NSKeyedArchiver, which is made to store and restore an object's properties.

Since NSTextFieldCell implements initWithCoder, it supports the archiver functions, and therefore I could use this code to init my subclass from the other's properties:

- (id)initWithCell:(NSCell *)cell {
    // Use NSArchiver to copy the NSCell's properties into our subclass
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *arch = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [cell encodeWithCoder:arch];
    [arch finishEncoding];
    NSKeyedUnarchiver *ua = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    self = [self initWithCoder:ua];
    // Here I'd set up additional properties of my own class
    return self;
}
Ionone answered 20/3, 2017 at 17:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.