"Convert to Modern Objective-C Syntax…"

July 28, 2024


A screenshot of Xcode 4. Courtesy of Dan Grassi

Now with Swift 6 around the corner it's a great time to talk about Objective-C. No particular reason for that really, other than me still enjoying the language – although mostly as a recreational activity.

Inspired by PSPDFKit's "Swifty Objective-C" and "Even Swiftier Objective-C" I'm sharing a few tricks that make the experience of programming in Objective-C more pleasant for me. But unlike PSPDFKit folks, I'm not going to rely on preprocessor or C++ for that because none of these bring me any joy.

__auto_type

This is my favorite keyword that brings some type inference we all love and appreciate in Swift to Objective-C. An option not to specify a type for every single variable is a nice-to-have feature on it's own, but there's a couple of special scenarios where not having to spell everything out is practically a lifesaver.

I mean:

void (^fetchProjects)(void (^completion)(NSArray <MyProject *> *_Nullable, NSError *_Nullable))
    = ^(void (^completion)(NSArray <MyProject *> *_Nullable, NSError *_Nullable)) {
    // ...
};

A single __auto_type may turn it into something way more tangible:

__auto_type fetchProjects = ^(void (^completion)(NSArray <MyProject *> *_Nullable, NSError *_Nullable)) {
    // ...
};

In terms of verbosity, block types have literally all of it1 so trimming the fat down from this declaration makes it arguably twice as easy to comprehend, versus the original one that requires double the cognitive effort to parse.

The thing with __auto_type of course is that it's painfully wordy and underscore-y (especially when compared to C++'s auto), but this seem to be perfectly in line with the overall verboseness of the language. It's also not as powerful as auto (e.g. it only works for variable declarations) but nevertheless a very handy tool to have.

({ })

I'd say the vast portion of all Objective-C code out there is designated to composing complex entities out of smaller ones which, in turn, may also be compositions of even smaller parts themselves.

And let's be honest, object composition code is usually pretty messy. Consider the following snippet:

NSString *selectedTeamName = self.teamNameField.stringValue;
if (self.options.sanitizeUserInput) {
    selectedTeamName = /* ... */;
}
MYTeamType *selectedTeamType = self.teamTypePopUp.selectedItem.representedObject;
MYTeam *targetTeam = [MYTeam teamWithName:selectedTeamName type:selectedTeamType];

NSArray <MYUser *> *selectedUsers = [self.users filteredArrayUsingPredicate: /* ... */];

MYUserAssignment *assignment = [MYUserAssignment assignmentWithUsers:selectedUsers targetTeam:targetTeam];

While there's nothing terribly wrong with this code, it's still quite annoying to read:

  1. You have to skim through the entire 10 lines of code before even realizing what this code is responsible for, which is essentially building up the assignment object from various smaller entities.
  2. We've polluted the current scope with 4 different interim one-off variables. Those variables names also have to be verbose to avoid conflicts with another variables from the same scope: e.g. it has to be selectedTeamName instead of just name for this very reason.

To address these two points, let me introduce you to "code block evaluation assignments" (or whatever they're called):

MYUserAssignment *assignment = ({
    MYTeam *team = ({
        // We could extract name-handling code into its own block,
        // but I prefer to limit nesting to 2 levels in here
        NSString *name = self.teamNameField.stringValue;
        if (self.options.sanitizeUserInput) {
            name = /* ... */;
        }
        MYTeamType *type = self.teamTypePopUp.selectedItem.representedObject;
        
        [MYTeam teamWithName:name type:type];
    });
    NSArray <MYUser *> *users = [self.users filteredArrayUsingPredicate: /* ... */];
    
    [MYUserAssignment assignmentWithUsers:users targetTeam:team];
});

By adding some artificial nesting to the code we've simultaneously got rid of scope pollution and switched to a somewhat declarative approach for configuring assignment, which gives a considerable boost to readability of this code by setting clear boundaries between object configuration and its actual usage later on2.

There's one issue with those code block expressions though: you can't put an arbitrary return statement in there – which means you're limited to only returning a single value defined on the very last line of the block. The good news is that we can easily define a callable C block in place and execute it immediately to enable multiple returns:

MYCompany *_Nullable *company = ^(void) {
   MYCompany *selectedCompany = self.companyPopUp.selectedItem.representedObject;
   if (selectedCompany.isPlaceholder) {
       return (MYCompany *)nil;
   }
   return selectedCompany;
}();

Injecting behavior with blocks

There're basically two mainstream approaches to dependency injection: we either inject entities hidden behind a bunch of tidy client-specific protocols – ultimately composing object hierarchies. Or we inject behavior-as-dependency via closures – ultimately defining and implementing a bunch of configurable logic breakpoints in our components.

Historically in Object-Oriented Programming world – of which Objective-C is an upstanding citizen – behavior injection is handled by some form of the Strategy pattern, or some other mix of object composition and delegation. But nothing really stops us from passing blocks as closures directly to a component, like we often do in Swift.

For instance, I usually follow this pattern in my view controllers. First I declare certain breakpoints as public properties of a controller:

typedef void (^MYCreateNewProjectFromIntent)(MYCreateProjectIntent *intent, void (^completion)(MYProject *_Nullable project, NSError *_Nullable error));

@interface MYViewController: NSViewController
@property (strong, nullable) MYCreateNewProjectFromIntent createNewProject;
@end

Now whenever I need to present this view controller, I just assign inline blocks to these properties during configuration:

- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender
{
    __auto_type vc = (MYViewController *)segue.destinationController;

    vc.createNewProject = ^(MYCreateProjectIntent *intent, void (^completion)(MYProject *_Nullable, NSError *_Nullable)) {
        // ...
    };
}

And finally the call-site featuring an injected createNewProject breakpoint:

@implementation MYViewController

- (void)createProject
{
    self.createNewProject(self.intent, ^(MYProject *newProject, NSError *error) {
        // ...
    };
}

@end

⚠️ Always make sure a block variable is not nil before attempting to call it, otherwise you'll end up with a crash.

So – amount of typing required aside – this is a powerful dependency injection technique that I've rarely seen in Objective-C projects. It's especially tempting if you consider an alternative approach where you'd need to define strict protocols or even new classes just to pass dependencies down to your components.

Oh and testing also becomes a breeze when every behavioral aspect of your component is configurable via plain easy-to-implement block breakpoints, so you don't have to come up with mocks, stubs, or whatever to unit test it.

Dispatch assertions

Structured Concurrency is definitely not coming to Objective-C (one may even argue whether it's actually coming to the majority of already established Swift projects out there), so we still have to be careful not to call things on wrong threads and queues:

[self.api fetchObject: ^(MYObject *_Nullable object, NSError *_Nullable error) {
    // ⚠️ Is it safe to call main thread-only APIs in this context? Who knows...
    self.textField.stringValue = object.title;
}];

Of course in most teams there's a set of rules and whatnot that dictate which queue a particular callback block must be run on. But, as with all unenforceable-at-compile-time rules, these are also easy to break in a language as tolerant as Objective-C.

So, as a precaution, whenever I write a method or a callback that assumes a specific execution context (say, main thread for UI manipulations), I always use Dispatch assertions to validate this assumption:

[self.api fetchObject:^(MYObject *_Nullable object, NSError *_Nullable error) {
    dispatch_assert_queue(dispatch_get_main_queue());
    
    // ✅ At this point we're 100% sure this is the main thread we're running on
    self.textField.stringValue = object.title;
}];

Each of these Dispatch assertions also has a _debug-suffixed version that is compiled out in Release builds, e.g. dispatch_assert_queue_debug(...).

Limiting protocols to certain types

It just never naturally occurred to me that you could apply a protocol specification to any type, not just id, practically simulating what Swift enables you to express with e.g.

protocol MyProtocol: NSViewController {}

So this declaration:

@property (weak) NSViewController <MyProtocol> *root;

is a perfectly valid definition of a root property that contains a weak reference to an NSViewController object that also conforms to MyProtocol.

Sum types

I believe that enums – and enums with associated values in particular – made Swift as a language. Having a first-class support for sum types in a programming language enables much more precise mapping of our mental models and theories to code.

One can categorize all enums usage in Swift into 2 primary groups:

  1. Enums as rich configuration options.

  2. Enums as state.

Let's talk about these two use cases and how they could be represented in Objective-C.

① Enums as configuration

If you don't require associated values in your enums, Objective-C's got you covered with a basic built-in integer-backed enum type:

typedef NS_CLOSED_ENUM(NSUInteger, MYOptions) {
	MYOptionsNo = 0,
	MYOptionsYes,
	MYOptionsMaybe
};

The usage is virtually the same as in Swift, and the compiler will even warn us if we forget to handle a certain case inside of a switch statement:

switch (options) {
    case MYOptionsNo:
        // ...
        break;
    case MYOptionsYes:
        // ...
        break;
    // ⚠️ Enumeration value 'MYOptionsMaybe' not handled in switch
}

If you do however need to associate your enum cases with a certain value other than a regular integer, this is where things will stop being as nice. Say, how do you approach converting this Swift enum to Objective-C?

enum GitOperation {
    case checkout(Reference, FilePath?)
    case stash ([FilePath])
    case push(Remote)

    var cliArguments: [String] { ... }
}

Option #1: a bunch of classes

One popular object-oriented approach to modelling some sort of a sum type where all of subtypes share (at least) the same public interface is a class cluster pattern that allows you to transparently create and work with different private sibling classes under the same interface.

A good example of a class cluster is an NSString class, which is actually a bunch of more specialized classes in a trench coat: there's a special constant string type (for string literals), a tagged pointer string type for very short strings, a regular string type, and a special type for storing paths – just to name a few.

In practice it means representing our enum cases as standalone classes that inherit from a single base class for a public interface, while keeping case-specific methods and properties to themselves:

// Access properties and methods common to all GitOperations
MYGitOperation *operation = /* ... */;
NSLog(@"CLI: %@", [operation.cliArguments componentsJoinedByString:@" "]);

if ([operation isKindOfClass:MYGitOperationCheckout.class]) {
    MYGitOperationCheckout *checkout = (MYGitOperationCheckout *)operation;
    // ... access git checkout-specific properties and methods here
}
else if ([operation isKindOfClass:MYGitOperationStash.class]) {
    MYGitOperationStash *stash = (MYGitOperationStash *)operation;
    // ... access git stash-specific properties and methods here
}
else {
    // etc
}

Not nearly as nice as a switch statement and we get zero compiler support for missing cases, but still a plausible option to have.

Option #2: also a bunch of classes

We can actually make the above example read more like a native switch statement and even gain basic compiler support with autocomplete. There's a few options to explore in that direction, for instance one covered in the "Artisanal Objective-C Sum Types" article by Heath Borders of Twitch. Here's what our example could look like should we employ the proposed technique:

MYGitOperation *operation = /* ... */;

[MYGitOperationSwitcher<void> switchOn:operation
    caseCheckout:^(MYGitOperationCheckout *checkout) {
        // ... access git checkout-specific properties and methods here
    }
    caseStash:^(MYGitOperationStash *stash) {
        // ... access git stash-specific properties and methods here
    }
    default:^(MYGitOperation *genericOperation) {
        // etc
    }
];

This finally brings type safety to our code, making it hard to accidentally miss switch cases or mess up class types in the corresponding blocks. The downside, of course, is that it does all that by requiring a pretty heavy boilerplate for every switch-able entity in your code (although you can probably generate most of it).

② Enums as state

While enums-as-configuration contribute a lot to my quality of life as a programmer, I often come across situations where I rely on enums to capture certain state, model changes to that state, and finally react to said changes. Designing these state enums in Objective-C could be a challenge, again with multiple approaches to consider. But this time I have a favorite one – and I'll walk you through it.

Step #1: just a state

Suppose we have a property in Swift that represents some resource that is not immediately available to use, but has to be downloaded or generated first. I tend to model such resources as some kind of a Loadable sum type:

enum Loadable<T, Error> {
    case pending
    case loading
    case loaded(Result<T, Error>)
}

var resource: Loadable<Resource, Resource.LoadingError> = .pending {
    didSet {
        switch resource {
            // ...
        }
    }
}

resource = .failure(.resourceDeleted)

This already allows me to specify all possible states for a given stateful property and also react to any state transitions that take place: e.g. I may display the resource in a view once it's available, or present an error in case of a loading failure, or animate a loading indicator in case it's still loading.

Of course one obvious drawback of this rather limited implementation is that, while the state itself is handled by one type (Loadable), all actual state transitions and observation become duties of some other parent type, which is now well aware of the internal assumptions, invariants and transition rules of our Loadable. Not a SOLID-friendly concept by any means, yet a good foundation to build upon.

Step #2: a state machine

Let's pick up where we left our Swift version of the Loadable enum and convert it to Objective-C almost as is:

@interface MYLoadable<T> : NSObject
@property (readonly) BOOL isPending;
@property (readonly) BOOL isLoading;
@property (strong, readonly) T _Nullable data;
@property (strong, readonly) NSError *_Nullable error;
@end

We could also introduce a Result<T> type to wrap these two awkward nullable data and error properties, but this won't gain us much in terms of type safety anyway. Let's just follow the best practices of NSURLSession API design here.

You may notice that I've annotated all properties of this class as readonly which implies that, well, these are not to be set from outside – but rather from within MYLoadable itself, unlike in the original Swift variant. This makes it a state machine in a way that users of this class are only going to request it to change its state based on the provided inputs, and not manipulate the state directly.

What kind of requests should MYLoadable class handle? Well, for one, we should be able to ask it to actually attempt to load something. And of course MYLoadable doesn't have a clue about the nature of our T or how one is supposed to be loaded, so we must provide it with some block of code that does:

@interface MYLoadable<T> : NSObject
// [...]
typedef void (^MYLoadableDataProvider)(void (^provide)(T _Nullable data, NSError *_Nullable error));
@property (strong, readonly) MYLoadableDataProvider dataProvider;

- (instancetype)initWithDataProvider:(MYLoadableDataProvider)provider NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end

Now let's define the actual data loading request in form of a method:

@interface MYLoadable<T> : NSObject
// [...]
- (void)loadWithCompletion:(void (^_Nullable)(MYLoadable<T> *result))completion;
@end

and its implementation:

@implementation MYLoadable
// [...]
- (void)loadWithCompletion:(void (^_Nullable)(MYLoadable <id> *result))completion
{
    self.isPending = NO;
    self.isLoading = YES;
    self.error = nil;
    self.data = nil;

    self.dataProvider(^void(id _Nullable data, NSError *_Nullable error) {
        self.isLoading = NO;
        self.error = error;
        self.data = data;

        if (completion != nil) {
            return completion(self);
        }
    });
}
@end

For greater dynamism, a data provider block might be passed as an argument to this method each time we call it, instead of being set once during initialization.

Since we need to observe the state transitions of our loadable somehow, we're forced to add this ominous completion handler. We'll talk about a more natural way of reacting to state changes in the next section, but for now this is the best Objective-C has to offer.

If you squeeze your eyes a little, you'll see that we've actually implemented a pretty normal OOP-obeying class that encapsulates both state and manipulations on that state done in reaction to some external requests. There's also notifying callers of any state changes that were direct result of these explicit requests:

MYLoadable<NSImage *> *preview = [[MYLoadable alloc] initWithDataProvider:^(void (^provide)(NSImage *_Nullable, NSError *_Nullable)) {
    // [...]
    return provide(anImage, anError);
}];

[preview loadWithCompletion:^(MYLoadable<NSImage *> *result) {
    if (result.error != nil) {
        return;
    }
    NSImage *image = result.data;
    // [...]
}];

Step #3: a state machine with full observation

Alright, I was very careful with my choice of words in that last sentence about only being able to observe "state changes that were direct result of these explicit requests" with request completion handlers. And of course not all state changes are like that, some may just happen with none of our input, as a mere result of someone else's actions and requests to the same state machine. How do we observe these?

3.1. KVO (+ Bindings)

KVO is probably not your first choice when it comes to observing object changes, mostly because there are way more suitable options in the Swift world, and even in Objective-C KVO is usually hard to fit into trendy reactive and declarative code architecture patterns. Anyway:

@property (strong) MYLoadable<NSImage *> *preview;
@property (strong) NSProgressIndicator *loadingIndicator;

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addObserver:self forKeyPath:@"preview.isLoading"
            options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self && [keyPath isEqualToString:@"preview.isLoading"]) {
        dispatch_assert_queue(dispatch_get_main_queue());
        self.loadingIndicator.isHidden = !self.preview.isLoading;
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

There's also Cocoa Bindings offering a nice declarative way to bind loadingIndicator.isHidden directly to preview.isLoading without all this KVO boilerplate, but Bindings are not available in UIKit (what a pity) so you are unlikely to ever use them:

[self.loadingIndicator bind:NSHiddenBinding toObject:self
                withKeyPath:@"preview.isLoading"
                    options:@{
    NSValueTransformerNameBindingOption: NSNegateBooleanTransformerName
}];
3.2. A reactive switch

Circling back to the idea of having a switch-like interface for case matching, why not just make this switch a part of our "enum" type itself akin to the data provider bit?

// 🗿 Let's assume that all `self` references here are weak for brevity

MYLoadable<NSImage *> *preview = [[MYLoadable alloc] initWithDataProvider:^(void (^provide)(NSImage *_Nullable, NSError *_Nullable)) {
    // [...]
    return provide(anImage, anError);
} onLoading:^(void) {
    self.loadingIndicator.isHidden = NO;
    // ...
} onLoaded:^(NSImage *image) {
    self.imageView.image = image;
    // ...
} onError:^(NSError *error) {
    [self presentModalError:error];
}];

[preview load];

Here we're declaring what needs to be done whenever our loadable preview transitions to a new state. It's important that we, as mere users of our loadable, don't need to be aware of neither who's initiated that state transition nor when exactly it happens.

While this is still years behind Swift's enum + didSet + switch combo in terms of ergonomics and readability, it's certainly a huge leap for Objective-C. Especially since it's entirely possible to keep upgrading MYLoadable even further by introducing e.g. built-in caching or an option to provide a temporary placeholder value until the actual one is loaded – so it starts resembling other more specific async data wrappers like SwiftUI's AsyncImage, for instance.

Footnotes

  1. I'm not going to dunk on the block syntax since there is sound logic behind every single bit of it, but as soon as you combine those bits into a single declaration – it somehow turns into an incomprehensible set of brackets impossible for a human brain to process. Go figure.

  2. Oh and now with all the pieces split into more or less standalone code blocks, you might even go ahead and extract those blocks into their own methods or functions without disturbing the overall code layout (if you ever feel the need for a new method or function, that is).

view raw how-to-objc.md hosted with ❤ by GitHub