BS
BleepingSwift
Published on

> Categories vs Class Extensions in Objective-C

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

Objective-C offers two ways to add methods to a class without subclassing: categories and class extensions. They look similar but have fundamentally different use cases. Understanding the distinction will clean up your code organization.

Categories: Adding Methods to Any Class

A category adds methods to an existing class—even one you don't have the source code for:

// NSString+Validation.h
@interface NSString (Validation)
- (BOOL)isValidEmail;
- (BOOL)isNumeric;
@end

// NSString+Validation.m
@implementation NSString (Validation)

- (BOOL)isValidEmail {
    NSString *pattern = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
    return [predicate evaluateWithObject:self];
}

- (BOOL)isNumeric {
    NSScanner *scanner = [NSScanner scannerWithString:self];
    [scanner scanDouble:nil];
    return [scanner isAtEnd];
}

@end

Now every NSString in your app responds to isValidEmail and isNumeric.

Category Limitations

Categories can't add instance variables. This fails:

@interface NSString (Validation)
@property (nonatomic) BOOL wasValidated;  // Won't work
@end

You can declare the property, but there's no backing storage. The runtime doesn't reserve space in existing NSString instances for your new variable.

The workaround uses associated objects:

#import <objc/runtime.h>

static const void *WasValidatedKey = &WasValidatedKey;

@implementation NSString (Validation)

- (BOOL)wasValidated {
    return [objc_getAssociatedObject(self, WasValidatedKey) boolValue];
}

- (void)setWasValidated:(BOOL)validated {
    objc_setAssociatedObject(self, WasValidatedKey, @(validated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

It works, but it's clunky. If you need instance variables, consider other approaches.

Class Extensions: Private Interface

A class extension looks like a category with no name:

// Person.m (not the header)
@interface Person ()
@property (nonatomic, copy) NSString *socialSecurityNumber;
- (void)recalculateTaxBracket;
@end

Unlike categories, extensions:

  • Must be in the same compilation unit as the class implementation
  • Can add instance variables
  • Declare private interface

The compiler knows about extension properties at compile time, so it synthesizes ivars normally.

When to Use Each

Use categories when:

  • Adding methods to classes you don't control (Foundation, UIKit, third-party)
  • Organizing a large class into logical groups
  • Making methods available across your entire project

Use class extensions when:

  • Declaring private properties with real storage
  • Hiding internal methods from your public header
  • Extending your own classes with implementation details

Organizing With Categories

Large classes benefit from splitting implementation across files:

// Person.h - main interface
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

// Person+Networking.h
@interface Person (Networking)
- (void)fetchProfileFromServer;
- (void)uploadChanges;
@end

// Person+Formatting.h
@interface Person (Formatting)
- (NSString *)formattedAddress;
- (NSAttributedString *)displayName;
@end

Each category gets its own .h and .m file. The class stays manageable even as it grows.

The Conflict Risk

If two categories define the same method, behavior is undefined:

// StringHelpers.m
@implementation NSString (Helpers)
- (NSString *)trimmed {
    return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
@end

// StringUtils.m (different file, different category)
@implementation NSString (Utils)
- (NSString *)trimmed {  // Collision!
    return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
@end

One implementation wins; which one is unpredictable. Prefix your category method names:

- (NSString *)acm_trimmed;

Extensions for Protocol Conformance

A common pattern declares private protocol conformance in an extension:

// ViewController.m
@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSArray *items;
@end

The header doesn't expose these protocols as public API, but the implementation conforms to them. Clean separation of public and private interface.

Testing Considerations

Private methods in extensions aren't visible to test files. You can either:

  1. Test through public interface (preferred)
  2. Declare a testing category that exposes private methods
  3. Put the extension in a private header and import it in tests
// Person+Private.h (shared with tests)
@interface Person ()
- (void)recalculateTaxBracket;
@end

The Practical Summary

Categories add behavior to classes you don't own. Extensions add private interface to classes you do own. Categories can't add storage; extensions can. Both are essential tools for organizing Objective-C code.

Use categories to extend Foundation and UIKit with project-specific conveniences. Use extensions to keep your headers clean and your implementation details hidden.

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.