Inspecting Objective-C Properties
June, 2020
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 asconst 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
- N for
- 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:
- Nullability. There is no way to ask the runtime whether a property is
nullable
ornonnull
- Generics. If you have a property of type
NSArray<NSString *> *
, the attributes string will only say that you have anNSArray
, 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 anNSString
the same way you box a number into anNSNumber
, 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:
The source code from this post can be found on Github. If you spot a bug, please create an issue on there.
Any comments or questions about this post? ✉️ nick @ this domain.
— Nick Randall