Inspecting Objective-C Properties

Sun, 14 Jun 2020 10:04:00 +0200

Using the Objective-C runtime to inspect properties of a class, and creating a convenience wrapper to work with them.

In this article we’re going to use the Objective-C runtime for some basic introspection: getting the properties of a class, including the attributes of each property (like readonly or weak). Then we’ll create a wrapper class so we can work with property attributes more easily, without needing to use the C functions. Finally we will look at how this all works with Swift.

Getting property names

Take an example Person class:

@interface Person: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSDate *dateOfBirth;
@property (nonatomic, readonly) int age;    // computed from dateOfBirth
@end

In a running app, each property of a class is represented by the opaque type objc_property_t. We can’t use it directly, but it gets returned from and passed into the property-related runtime functions.

The runtime lets us get the details of a single property or all the properties of a class at once. To use any of these functions, we’ll need to first @import ObjectiveC.runtime.

Getting the name of a single property

We’ll get the name of the dateOfBirth property first, using the class_getProperty and property_getName functions.

Note that runtime functions deal with C strings, not NSString. This means that they’re declared as const char *, don’t have a leading @, and are printed with %s instead of %@.

objc_property_t prop = class_getProperty(Person.class, "dateOfBirth");
const char *propName = property_getName(prop);
NSLog(@"%s", propName);

// Prints: dateOfBirth

Not very exciting so far, since we already had the property name 😁

But what if we could get all the properties of the Person class without knowing the names? We can with class_copyPropertyList.

Getting the names of all the properties

When we call class_copyPropertyList, we need to pass in a pointer to an unsigned int, which it will set to the number of properties found. It will return a pointer to the first item in an array of objc_property_t. Since the method name contains “copy”, we’re taking ownership of the array and need to free it when we’re done.

uint propCount;
objc_property_t *props = class_copyPropertyList(Person.class, &propCount);
for (uint n = 0; n < propCount; n++) {
    const char *propName = property_getName(props[n]);
    NSLog(@"%d: %s", n, propName);
}
free(props);

// Prints:
//  0: name
//  1: dateOfBirth
//  2: age

Now this is getting a bit more interesting, but we really want to get all the details of each property, not just the names.

Getting property attributes

We’ll expand on the last example by adding another function call, to property_getAttributes:

uint propCount;
objc_property_t *props = class_copyPropertyList(Person.class, &propCount);
for (uint n = 0; n < propCount; n++) {
    const char *propName = property_getName(props[n]);
    const char *propAttribs = property_getAttributes(props[n]);
    NSLog(@"%d: %s - %s", n, propName, propAttribs);
}
free(props);

// Prints:
//  0: name - T@"NSString",C,N,V_name
//  1: dateOfBirth - T@"NSDate",&,N,V_dateOfBirth
//  2: age - Ti,R,N

You might be thinking, what the heck are those attribute strings? 🤔

They’re a compact encoding which contain all of the details of a property, and are included in the compiled binary. Apple’s docs on Declared Properties have a full description of the format, but the short version is:

They always start with a T plus the encoded type of the property:

@ for an object, followed by the quoted name of the class if available

i for int

d for double

the full list can be found in Type Encodings

The rest is a comma-separated list of flags:

N for nonatomic

R for readonly

C for copy

& for strong

and a few others, listed in Property Type String

The final flag is V with the name of the backing ivar, if it has one

Unavailable property attributes

Not all of the details of a property can be accessed at run time. The missing ones are:

1. Nullability. There is no way to ask the runtime whether a property is nullable or nonnull

2. Generics. If you have a property of type NSArray<NSString *> *, the attributes string will only say that you have an NSArray, but not which type of objects it contains

The reason that they are unavailable is that in Objective-C, nullability and generics are lightweight, meaning they are only used at build time and are not encoded or used in the compiled binary.

Creating a property metadata wrapper

It would be useful for doing further reflection to build an Objective-C class to give us easy access to property metadata, instead of having to deal with runtime API directly or parse those attribute strings each time. The interface could look something like this:

/// These are mutually exclusive, so use an enum
typedef NS_ENUM(char, SetterType) {
    Assign, Strong, Weak, Copy
};

@interface ClassProperty : NSObject

@property (nonatomic, readonly) NSString *name;

/// This will be the raw type encoding for now
@property (nonatomic, readonly) NSString *encodeType;

@property (nonatomic, readonly) BOOL isReadOnly;
@property (nonatomic, readonly) BOOL isNonAtomic;
@property (nonatomic, readonly) BOOL isDynamic;
@property (nonatomic, readonly) SetterType setterType;

/// Custom getter or the default getter
@property (nonatomic, readonly) SEL getter;

/// Custom or default setter, NULL if the property is readonly
@property (nonatomic, readonly, nullable) SEL setter;

/// Will be nil if the property is computed or dynamic
@property (nonatomic, readonly, nullable) NSString *ivarName;

- (instancetype)initWithProperty:(objc_property_t)property;

@end

Let’s get started on the implementation. For simplicity, we’ll just put the parsing of the property attributes string right in the init method:

@implementation ClassProperty

@synthesize getter = _getter;
@synthesize setter = _setter;

- (instancetype)initWithProperty:(objc_property_t)property {
    self = [super init];
    if (self) {
        _name = @(property_getName(property));
        _setterType = Assign;  // this is the default

Notice with the _name assignment that you can “box” a C string into an NSString the same way you box a number into an NSNumber, by wrapping it in @().

To parse the attributes, let’s just split the string by comma, and iterate over each part. We can just look at the first character in each attribute to see what it is. We’re not going to worry too much about performance here, but switching on a single char is not a bad way to go.

        NSString *attribStr = @(property_getAttributes(property));
        NSArray *attribs = [attribStr componentsSeparatedByString:@","];

        for (NSString *attrib in attribs) {
            unichar attribChar = [attrib characterAtIndex:0];
            NSString *attribDetail = [attrib substringFromIndex:1];

            switch (attribChar) {
                case 'T':
                    _encodeType = attribDetail;
                    break;
                case 'R':
                    _isReadOnly = YES;
                    break;
                case 'N':
                    _isNonAtomic = YES;
                    break;
                case 'D':
                    _isDynamic = YES;
                    break;
                case '&':
                    _setterType = Strong;
                    break;
                case 'W':
                    _setterType = Weak;
                    break;
                case 'C':
                    _setterType = Copy;
                    break;
                case 'G':
                    _getter = NSSelectorFromString(attribDetail);
                    break;
                case 'S':
                    _setter = NSSelectorFromString(attribDetail);
                    break;
                case 'V':
                    _ivarName = attribDetail;
                    break;
                default:
                    NSAssert(NO, @"Unknown attribute: %@", attrib);
            }
        }

There’s one final touch needed: if the property doesn’t have a custom getter or setter, we want to return the default selectors.

For a getter, the default is simply the name of the property. Setters are slightly trickier, we need to turn a property name like myProperty into a selector setMyProperty:, and then only if it isn’t readonly:

        if (_getter == NULL) {
            _getter = NSSelectorFromString(_name);
        }

        if (_setter == NULL && _isReadOnly == NO) {
            NSString *name = [NSString stringWithFormat:@"set%@%@:",
                              [_name substringToIndex:1].uppercaseString,
                              [_name substringFromIndex:1]];
            _setter = NSSelectorFromString(name);
        }
    }
    return self;
}

@end

Getting the properties of any class

This is already quite useful for examining a single property, but it would be really handy to get a list of these for any class. We can achieve this by moving our class_copyPropertyList example above into a category on NSObject:

@interface NSObject (Introspection)
@property (class, readonly) NSArray<ClassProperty *> *classProperties;
@end

@implementation NSObject (Introspection)

+ (NSArray<ClassProperty *> *)classProperties {
    NSMutableArray *clsProps = [NSMutableArray array];
    uint propCount;
    objc_property_t *props = class_copyPropertyList(self.class, &propCount);
    for (uint n = 0; n < propCount; n++) {
        [clsProps addObject:[[ClassProperty alloc] initWithProperty:props[n]]];
    }
    free(props);
    return clsProps;
}

@end

Now we can get metadata for all the properties of any class!

Assuming we’d added a custom description to ClassProperty, it’s as easy as:

NSLog(@"%@", Person.classProperties);

// Prints:
//  (
//    name: nonatomic, copy, type=@\"NSString\", getter=name, setter=setName:, ivar=_name
//    dateOfBirth: nonatomic, strong, type=@\"NSDate\", getter=dateOfBirth, setter=setDateOfBirth:, ivar=_dateOfBirth
//    age: readonly, nonatomic, assign, type=i, getter=age
//  )

Dumping the properties of any class

We can go one step further by introspecting not just the properties themselves, but the property values of an object by using Key-Value Coding:

Person *person = [Person new];
person.name = @"Homer";
person.dateOfBirth = [NSDate dateWithTimeIntervalSince1970:0];

for (ClassProperty *prop in Person.classProperties) {
    NSLog(@"%@ = %@", prop.name, [person valueForKey:prop.name]);
}

// Prints:
//  name = Homer
//  dateOfBirth = Thu Jan  1 01:00:00 1970
//  age = 50

This is starting to look really useful now... we could use this to do automatic serialisation and deserialisation of data model classes! But that’s a topic for another post 😉

Swift compatibility

While this series is not really about Swift, it’s worth looking into how it interacts with the Objective-C runtime.

The wrapper class we’ve created here will work on Swift classes, but only for properties marked with @objc, since they are the only ones visible to the runtime.

For the equivalent of our Person class in Swift, it would print these property details:

@objc class Person: NSObject {
    @objc var name: String
    @objc var dateOfBirth: Date
    @objc var age: Int { ... }
    var swiftOnly: Bool  // not visible to ObjC
}

print(Person.classProperties)

// Prints:
//  (
//    name: nonatomic, copy, type=@\"NSString\", getter=name, setter=setName:
//    dateOfBirth: nonatomic, copy, type=@\"NSDate\", getter=dateOfBirth, setter=setDateOfBirth:
//    age: readonly, nonatomic, assign, type=q, getter=age
//  )

You should be able to spot a few differences to the Objective-C class: properties are always nonatomic, there are no backing ivars, and object properties are automatically copy instead of strong if the class conforms to NSCopying.

Introspecting with Swift

While the Objective-C runtime functions don’t work on pure Swift classes, Swift has its own introspection tool called Mirror:

struct Person {
    let name = "Homer"
    let dateOfBirth = Date(timeIntervalSince1970: 0)
    var age: Int { ... }
}

let p = Person()
let m = Mirror(reflecting: p)
m.children.forEach { p in
    print("\(p.label!) = \(p.value) (\(type(of: p.value)))")
}

// Prints:
//  name = Homer (String)
//  dateOfBirth = 1970-01-01 00:00:00 +0000 (Date)

This works quite differently to our Objective-C introspection though:

We need to use an instance of the class instead of just being able to examine the class itself

The computed property age doesn’t show up

While there is no type information included, the actual values are, so we can use type(of:) on those

There is less detail about the properties, but some of those concepts just don’t exist in Swift, like ivars and custom getters and setters

On the plus side, Mirror can be used for any Swift type, not just classes. You’ll notice that we actually introspected a Swift struct above.

Unfortunately, it looks like Mirror is not designed to work at all on Objective-C classes, so we can’t use a single technique for both languages.

Wrapping up

That’s about it for introspecting Objective-C properties.
I’ve personally found the property functions to be among the most useful tools for runtime programming, and I’ve built several interesting projects using them. I plan to delve into some of these in future posts.

Useful links

Apple’s documentation on:

Declared Properties

Type Encodings