Objective Clean

Like shuffling at the club, there's a powerful backlash underway against OO languages. Every new language is functional. But like crustaceans Objective-C was here before us and will surely survive the 21st century's attempts to retire the C family. Like GN-z11, it may no longer hold dominion over tech, but it's happy in retirement as a niche language - powering millions of apps quietly now.

I wrote this as a living document for me, but maintain it both for memory, and as a historical document of ObjC's brief time as the hottest software technology on Earth.

The Objective-Clean themes:
  • Make things clear - it's already as verbose as popular languages get - a bit more with enums, templates etc. won't hurt and improve safety.
  • Adopt some Swiftiness - custom initialization, categories, and delegates dovetail a bit with things like POP in Swift.
  • Keep things loosely coupled
  • Don't fight the runloop

A sorting view for a game


The sorting view that doubles as my ObjC reference is a control useful for games, pickers, settings, menus and to help us literally sort through thirty years of Objective-C implementation options that have grown quadratically since 1984.
Download here to follow along.
Make it clear
Use templates for all your collections. Also, note that properties are preferred over i-vars for performance.

@property (nonatomic, strong) NSMutableArray<UIView *> *dropAreas;
@property (nonatomic, strong) NSMutableDictionary<NSNumber*, UIView*> *dictionary;
Make public properties read only and use a setter to pass data. This prevents people from assigning things willy nilly.

//  MELSortingView.h
- (void)setLabels:(NSArray *)labels;

//  MELSortingView.m
- (void)setLabels:(NSArray *)labels{
	...
}
Use enums instead of integers to differentiate cases

typedef enum : NSUInteger {
    kInitialView,
    kSecondaryView
} SortingViews;
Objective Swift
Initialize the control with an instanceType convenience constructor. This allows more flexibility during creation and guides API consumers to understand and use it as intended. Good method signatures are self-documenting.

//  MELSortingView.m
+ (instancetype)sortingViewWithFrame:(CGRect)frame forView:(UIView *)superView numberOfViews:(NSInteger)numberOfViews{
    return [[self alloc] initWithFrame:frame forView:superView numberOfViews:numberOfViews];
}
Categories are like Swift extensions - here we'll use one to center our labels.

//centeredOriginForChildFrame is from a UIView category
CGRect frame = [label frame];
frame.origin = CGPointMake([dragView centeredOriginForChildFrame:frame].x, [dragView centeredOriginForChildFrame:frame].y);
[label setFrame:frame];
For data sharing, the control uses a delegate chain causing messages to bubble up to the view controller that owns the sorting view. Why is this preferable to having both the sorting view and the controller subscribe to NSNotifications?
  1. Delegate methods are easier to reason about as notification data must often be hidden inside the userInfo dictionary. For example, if you don't want a certain listener to respond to the notification, you must put this conditional in the notification dictionary. Spaghetti.
  2. You prevent the case where a notification is sent to a nil object.
  3. Notifs are dispatched synchronously to all subscribers simultaneously. So, using delegation ensures that our view controller only receives info after the UI has finished updating.
Stay loose
Separate your panning and swapping business logic with a panManager class. Then, if you wanted to add pinching or tapping, you would create separate managers for them. This keeps code modular, extendable, easier to test, and easier to use across projects. panManager also uses a custom initializer for self-documentation and also exposes only one method handlePan so that it can be the target of our view's gestures. This is cleaner than creating a new recognizer for each subview.
Inside handlePan, use a bit of CG math to see which view is being dragged and where it's being dropped. Delegate methods are fired when these conditions are met.

//MELSortingView.m
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.panManager action:@selector(handlePan:)];
            [self addGestureRecognizer:pan];
            
//panManager.h
- (void)handlePan:(UIPanGestureRecognizer *)recognizer;
So, our loosely coupled delegate pattern has first the sorting view and then the view controller each receiving event-driven messages.

//panManger.h
@protocol PanManagerDelegate <NSObject>
 
@optional
- (void)viewWasMovedWithView:(UIView *)view;
- (void)view:(UIView *)view didAlternateWithView:(UIView *)destinationView fromOriginalRect:(CGRect)originalRect;
@end
 
//MELSortingView.h
@protocol MELSortingViewDelegate <NSObject>
 
@optional
- (void)view:(MELSortingView *)sortingView wasMovedWithView:(UIView *)aView;
- (void)view:(MELSortingView *)sortingView didAlternateView:(UIView *)departureView withView:(UIView *)destinationView;
@end
Don't fight the runloop
layoutSubviews gets called on the next tick of the runloop, when an event has occurred that requires a redraw.
Use bounds instead of frame - better for animations.
Use lazy initiation for private views. This ensures that memory is only allocated when needed (usually in layoutSubviews) but ensures that it IS allocated anytime it is called on self.

//  MELSortingView.m
- (void)layoutSubviews{
    [super layoutSubviews];
    
    CGRect canvasFrame = [[self canvasView] bounds];
    canvasFrame.size = CGSizeMake(self.bounds.size.width, self.bounds.size.height);
    [[self canvasView] setFrame:canvasFrame];
}
...

- (UIView *)canvasView{
    if (!_canvasView){
        _canvasView = [UIView new];
        [self addSubview:_canvasView];
    }
    return _canvasView;
}
Misc.
For looping, fast enumeration is a bit slower than standard C iteration but is thread safe.

[self.dragSubjects enumerateObjectsUsingBlock:^(UIView *dragView, NSUInteger idx, BOOL *stop) {
	...
}];
The delegate pattern is the easiest way to ship a memory leak. Prevent them by declaring the property weak, since the thing it's referencing is the only strong pointer we need.

@property (weak, nonatomic) id<MELSortingViewDelegate> delegate;
Speaking of not fighting the system, use sizeToFit with labels - this is both easier and better for localization (don't forget to do it after setting the string).

See you in ≈ 2 years
The two things I miss most about The OC is its open invitation to hack and its atavistic stability. These will ensure its place in my heart as I watch the new generation learn app development with Safe Swift.

It's two years from now and I've been called to assist on another xcodeproj that won't compile. NSCopying has blown up again. Things have been allocated as dictionaries but cast to arrays with nary a warning. Never change old man.
Copyright © 2011–2018 Mike Leveton