BS
BleepingSwift
Published on

> The NSObject Class Hierarchy Explained

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

Open any Objective-C file and you'll see it: @interface MyClass : NSObject. That inheritance isn't optional ceremony—it's what makes your class work with the entire Cocoa ecosystem. Let's explore what NSObject actually provides.

The Root of Everything

NSObject is Objective-C's root class. Almost every class you interact with inherits from it, forming a single inheritance tree:

NSObject
├── NSString
├── NSArray
├── UIView
│   ├── UILabel
│   ├── UIButton
│   └── UITableView
├── UIViewController
│   ├── UITableViewController
│   └── UINavigationController
└── YourCustomClass

When you create a class inheriting from NSObject, you get a remarkable amount of functionality for free.

Identity and Comparison

NSObject provides the fundamental notion of object identity:

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

The default isEqual: returns YES only when comparing the same instance. Override it for value semantics:

@implementation Person

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;
    if (![object isKindOfClass:[Person class]]) return NO;

    Person *other = (Person *)object;
    return [self.name isEqualToString:other.name] &&
           self.age == other.age;
}

- (NSUInteger)hash {
    return self.name.hash ^ @(self.age).hash;
}

@end

Always override both or neither. Collections use hash for quick lookups and isEqual for confirmation.

Introspection

NSObject lets objects examine themselves at runtime:

NSString *str = @"Hello";

[str class];                           // NSString (or a private subclass)
[str superclass];                      // NSObject
[str isKindOfClass:[NSString class]];  // YES
[str isMemberOfClass:[NSString class]]; // Maybe NO (class clusters)
[str respondsToSelector:@selector(length)]; // YES
[str conformsToProtocol:@protocol(NSCopying)]; // YES

These methods enable dynamic behavior. You can write code that adapts to objects it knows nothing about at compile time.

Memory Management

The retain/release methods live on NSObject:

- (instancetype)retain;
- (oneway void)release;
- (instancetype)autorelease;
- (NSUInteger)retainCount;  // Never use this

Under ARC you don't call these directly, but they still execute. Your object exists because NSObject's infrastructure tracks references and deallocates when counts reach zero.

Description for Debugging

When you log an object, NSObject's description methods provide the output:

- (NSString *)description;
- (NSString *)debugDescription;

Override these for useful logging:

- (NSString *)description {
    return [NSString stringWithFormat:@"<Person: %@, age %ld>",
            self.name, (long)self.age];
}

Now NSLog(@"%@", person) shows something meaningful.

Key-Value Coding

NSObject provides the KVC infrastructure:

id value = [person valueForKey:@"name"];
[person setValue:@"Alice" forKey:@"name"];

// Key paths work too
id city = [person valueForKeyPath:@"address.city"];

This powers Interface Builder bindings, Core Data, and numerous frameworks that access properties by name.

Key-Value Observing

Built on KVC, KVO lets you observe property changes:

[person addObserver:self
         forKeyPath:@"name"
            options:NSKeyValueObservingOptionNew
            context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    NSLog(@"Name changed to: %@", change[NSKeyValueChangeNewKey]);
}

Remove observers before deallocation—this is a common source of crashes.

Initialization

NSObject defines the initialization pattern:

- (instancetype)init {
    self = [super init];
    if (self) {
        // Initialize instance variables
    }
    return self;
}

The self = [super init] pattern exists because initializers can return different objects (class clusters) or nil (failure). Always check before proceeding.

Copying

The copy methods are declared in NSObject:

- (id)copy;
- (id)mutableCopy;

These call copyWithZone: and mutableCopyWithZone: from NSCopying and NSMutableCopying protocols. Implement those protocols to make your class copyable.

The NSObject Protocol

Interestingly, NSObject the class conforms to NSObject the protocol. The protocol declares the basic methods every object should have:

@protocol NSObject

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
- (Class)superclass;
- (Class)class;
- (instancetype)self;
- (BOOL)respondsToSelector:(SEL)aSelector;
// ... more methods

@end

This means NSProxy (the only other root class) can also provide these methods despite not inheriting from NSObject.

NSProxy: The Other Root

NSProxy exists for objects that forward messages elsewhere:

@interface NSProxy <NSObject>  // Conforms to protocol, doesn't inherit

It's used for lazy loading, distributed objects, and test mocks. You rarely encounter it directly.

Objects Without NSObject

Technically, you can create classes that don't inherit from NSObject:

@interface Standalone {
    Class isa;
}
@end

But you'd need to implement retain/release yourself, couldn't use KVC or KVO, and would break most frameworks. There's no practical reason to do this.

The Foundation Connection

NSObject lives in Foundation, not the Objective-C runtime itself. This means:

  • Pure C programs using the runtime don't need Foundation
  • The runtime provides primitives; NSObject provides conveniences
  • Other frameworks (like CoreFoundation) can provide alternative base classes

In practice, everything uses Foundation and everything inherits from NSObject. It's the invisible foundation beneath all your code.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.