The Seventh Principle: An idiom should stick to language features which make code obvious.

This is a continuation of the blog post 12 Principles of Good Software Design.

7. An idiom should stick to language features which make code obvious.

The Objective C language has a number of interesting features which allow us to add new functionality to our code. Features such as KVO allow us to trigger an event when a field changes. Categories allow us to extend an existing class. Method swizzling allows us to replace the functionality of a method with another for all objects of a given class.

Java has a number of interesting features as well. The ability to customize our class loader allows us to modify the way we load executable code. Java has a rich set of introspection primitives as well as reflection support to allow us to dynamically examine classes and code. Java even includes an annotation mechanism that can help us guide our introspection to associate semantic meaning to methods and fields.

Do not use them unless you absolutely must.

When you use these features you can make the functionality of your code impossible to follow for someone new to a project. These features can make it impossible for someone to discover if functionality is built in or added, impossible for someone to follow the flow of execution, impossible to discover how to fix bugs or extend functionality.

And some of the tools at your disposal are downright dangerous, in the sense that if you screw something up you can make the application unstable. Certain techniques, such as method swizzling, can introduce bugs that are nearly impossible to debug.

So many times we use features such as this, when it is not necessary; similar functionality could be achieved more simply through easier to understand and easier to maintain techniques, such as class inheritance.


Suppose you have the following application which displays a table of items in iOS. The model declaration is simple: given a start row and a number of rows, this loads an array of the objects and asynchronously returns the loaded list of items.

#import <Foundation/Foundation.h>

@interface Model : NSObject

+ (Model *)shared;

- (void)loadStarting:(NSInteger)startRow length:(NSInteger)length callback:(void (^)(NSArray *))callback;

@end

The view controller and table cell should be familiar; they are from our previous example.

Our view controller:

#import "ViewController.h"
#import "TableViewCell.h"
#import "Model.h"

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) NSMutableArray *data;
@end

static const void *ImageNameKey = &ImageNameKey;

@implementation ViewController

- (void)viewDidLoad
{
	[super viewDidLoad];

	/*
	 *	Create and bind our view model
	 */

	self.data = [[NSMutableArray alloc] init];
	[[Model shared] loadStarting:0 length:20 callback:^(NSArray *a) {
		[self.data addObjectsFromArray:a];
		[self.tableView reloadData];
	}];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return self.data.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

	NSDictionary *d = self.data[indexPath.row];
	[cell setData:d];

	return cell;
}

@end

Our table cell simply takes the contents and displays them; our view controller only needs to know the number of rows loaded.

Our assignment: add endless scrolling. That is, as we scroll in our table view, as we reach the bottom we should dynamically load more items until we reach the end.


For our example we will create a new category on UITableView which sends us a message when scrolling is close to the bottom, so we can then trigger loading more rows.

Our final category looks like the following:

@protocol UITableViewEndlessDelegate <UITableViewDelegate>
- (BOOL)appendMoreDataToTableView:(UITableView *)tableView;
@end

@interface UITableView (EndlessScroll)

- (void)setEndlessScrollingEnabled:(BOOL)flag;
- (BOOL)endlessScrollingEnabled;

- (void)finishedAppendingData;

@end

The essence of our endless scrolling code involves looking at the current offset of the content, and if it is close enough to the bottom, trigger a callback to ask our view controller to load more rows. If we have no more rows to load, then we disable endless scrolling for that table view. We also keep track if the table view is in the loading state; if it is, we don’t trigger multiple callbacks to load more data. (We can use that state to extend our table view to maintain some loading UI, such as a spinner at the bottom of our content area.)

The code which handles that is below:

/*
 *	Endless scrolling logic. This is simple; all we do is test if the
 *	y position is at max, and if it is, trigger our event.
 */

- (CGFloat)es_triggerOffset
{
	CGFloat h = self.contentSize.height;
	UIEdgeInsets insets = self.contentInset;
	h -= self.bounds.size.height - insets.top - insets.bottom;
	if (h < 0) h = 0;
	return h;
}

- (void)es_testBounds
{
	if (self.es_state.loading) return;	// we're loading

	CGFloat ypos = self.contentOffset.y;
	if (ypos >= self.es_triggerOffset - 44) {	// We're close to bottom
		self.es_state.loading = YES;
		BOOL flag = [(id<UITableViewEndlessDelegate>)self.delegate appendMoreDataToTableView:self];
		if (!flag) self.endlessScrollingEnabled = NO;
	}
}

It’s fairly straight forward. The first method calculates the offset which would be the “maximum scroll”; that is, the maximum amount our content can scroll. The second method determines if we are loading, and if we are not, then determines if we are at the bottom of the scroll area. If so, we note that we’re loading, and call our callback.

Because this is a category we cannot simply extend our class. We must instead use an association to store our data:

- (InternalState *)es_state
{
	return objc_getAssociatedObject(self, InternalStateKey);
}

Our internal state simply tracks if endless scrolling is initialized, and if we are in the loading state. Additional items, such as spinners, would also be tacked on here.

/*
 *	Declare internal state record that we track for our endless scrolling
 *	mechanism
 */

@interface InternalState: NSObject
@property (assign) BOOL initialized;
@property (assign) BOOL loading;
@end

@implementation InternalState
@end

Note that we also extend the UITableViewDelegate with an additional method. From our header declaration:

@protocol UITableViewEndlessDelegate <UITableViewDelegate>
- (BOOL)appendMoreDataToTableView:(UITableView *)tableView;
@end

This additional callback in our delegate allows our view controller to detect if we need to load more items and take the appropriate action. Once we’re done loading (as loading is an asynchronous process), we call the -finishedAppendingData; category method. If there are no more rows we disable endless scrolling by calling -setEndlessScrollingEnabled: with NO.

Now at the heart of our endless scrolling, we require callbacks as the contents move. Currently on iOS, when the contents move, the method setContentOffset: is invoked. As the contents resize, setContentSize: is invoked. In order to intercept calls to these methods we must use method swizzling. We basically replace the pointer to the function that is invoked by the selectors setContentOffset and setContentSize with pointers to our own functions. Our functions then invoke the previous selectors, then detect if we hit the bottom by calling our -es_TestBounds methods.

/*
 *	Swizzle swap
 */

static void SwizzleMethods(Class c, SEL original, SEL alternate)
{
	Method origMethod = class_getInstanceMethod(c, original);
	Method altMethod = class_getInstanceMethod(c, alternate);

	if (class_addMethod(c, original, method_getImplementation(altMethod), method_getTypeEncoding(altMethod))) {
		class_replaceMethod(c, alternate, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
	} else {
		method_exchangeImplementations(origMethod, altMethod);
	}
}

+ (void)load
{
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		SwizzleMethods(self, @selector(setContentOffset:), @selector(es_SetContentOffset:));
		SwizzleMethods(self, @selector(setContentSize:), @selector(es_SetContentSize:));
	});
}

The load method is special; it is invoked as our category is loaded. This means our methods are swizzled the moment any code refers to our category.

And our swizzled methods are:

/*
 *	The swizzled methods
 */

- (void)es_SetContentOffset:(CGPoint)contentOffset
{
	[self es_SetContentOffset:contentOffset];

	if (self.es_state.initialized) {
		[self es_testBounds];
	}
}

- (void)es_SetContentSize:(CGSize)contentSize
{
	[self es_SetContentSize:contentSize];

	if (self.es_state.initialized) {
		[self es_testBounds];
	}
}

Notice the oddity that the methods look like they’re calling themselves. They’re not. As we swizzle the methods, what’s old becomes new again–and what appears to be a method calling itself in fact is the new method calling the original method.

Our complete category declaration is thus:

#import "UITableView+EndlessScroll.h"
#import <objc/runtime.h>

static const void *InternalStateKey = &InternalStateKey;

/*
 *	Declare internal state record that we track for our endless scrolling
 *	mechanism
 */

@interface InternalState: NSObject
@property (assign) BOOL initialized;
@property (assign) BOOL loading;
@end

@implementation InternalState
@end

/*
 *	Swizzle swap
 */

static void SwizzleMethods(Class c, SEL original, SEL alternate)
{
	Method origMethod = class_getInstanceMethod(c, original);
	Method altMethod = class_getInstanceMethod(c, alternate);

	if (class_addMethod(c, original, method_getImplementation(altMethod), method_getTypeEncoding(altMethod))) {
		class_replaceMethod(c, alternate, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
	} else {
		method_exchangeImplementations(origMethod, altMethod);
	}
}

@implementation UITableView (EndlessScroll)

+ (void)load
{
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		SwizzleMethods(self, @selector(setContentOffset:), @selector(es_SetContentOffset:));
		SwizzleMethods(self, @selector(setContentSize:), @selector(es_SetContentSize:));
	});
}

- (InternalState *)es_state
{
	return objc_getAssociatedObject(self, InternalStateKey);
}

- (BOOL)endlessScrollingEnabled
{
	InternalState *state = self.es_state;
	if (state == nil) return NO;
	return state.initialized;
}

- (void)setEndlessScrollingEnabled:(BOOL)flag
{
	if (!self.endlessScrollingEnabled && flag) {
		// Internal state not set, so set one up and enable
		InternalState *state = [[InternalState alloc] init];
		state.initialized = YES;
		objc_setAssociatedObject(self, InternalStateKey, state, OBJC_ASSOCIATION_RETAIN);
	} else {
		InternalState *state = objc_getAssociatedObject(self, InternalStateKey);
		state.initialized = flag;
	}
}

- (void)finishedAppendingData
{
	self.es_state.loading = NO;
	[self es_testBounds];
}

/*
 *	The swizzled methods
 */

- (void)es_SetContentOffset:(CGPoint)contentOffset
{
	[self es_SetContentOffset:contentOffset];

	if (self.es_state.initialized) {
		[self es_testBounds];
	}
}

- (void)es_SetContentSize:(CGSize)contentSize
{
	[self es_SetContentSize:contentSize];

	if (self.es_state.initialized) {
		[self es_testBounds];
	}
}

/*
 *	Endless scrolling logic. This is simple; all we do is test if the
 *	y position is at max, and if it is, trigger our event.
 */

- (CGFloat)es_triggerOffset
{
	CGFloat h = self.contentSize.height;
	UIEdgeInsets insets = self.contentInset;
	h -= self.bounds.size.height - insets.top - insets.bottom;
	if (h < 0) h = 0;
	return h;
}

- (void)es_testBounds
{
	if (self.es_state.loading) return;	// we're loading

	CGFloat ypos = self.contentOffset.y;
	if (ypos >= self.es_triggerOffset - 44) {	// We're close to bottom
		self.es_state.loading = YES;
		BOOL flag = [(id<UITableViewEndlessDelegate>)self.delegate appendMoreDataToTableView:self];
		if (!flag) self.endlessScrollingEnabled = NO;
	}
}

@end

And with all this machinery in place, we can finally update our View Controller to handle append events.

#import "ViewController.h"
#import "TableViewCell.h"
#import "Model.h"
#import "UITableView+EndlessScroll.h"

@interface ViewController () <UITableViewEndlessDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) NSMutableArray *data;
@end

static const void *ImageNameKey = &ImageNameKey;

@implementation ViewController

- (void)viewDidLoad
{
	[super viewDidLoad];

	/*
	 *	Create and bind our view model
	 */

	self.data = [[NSMutableArray alloc] init];
	[[Model shared] loadStarting:0 length:20 callback:^(NSArray *a) {
		[self.data addObjectsFromArray:a];
		[self.tableView reloadData];

		self.tableView.endlessScrollingEnabled = YES;
	}];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return self.data.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

	NSDictionary *d = self.data[indexPath.row];
	[cell setData:d];

	return cell;
}

- (BOOL)appendMoreDataToTableView:(UITableView *)tableView
{
	[[Model shared] loadStarting:self.data.count length:20 callback:^(NSArray *a) {
		NSInteger count = a.count;
		if (count == 0) {
			// No more data. Turn off endless scrolling
			self.tableView.endlessScrollingEnabled = NO;

		} else {
			// Create array of indexes we're inserting
			NSMutableArray *indexes = [[NSMutableArray alloc] init];
			NSInteger last = self.data.count;
			for (NSInteger i = 0; i < count; ++i) {
				[indexes addObject:[NSIndexPath indexPathForRow:i+last inSection:0]];
			}

			// Insert the data
			[self.data addObjectsFromArray:a];

			// Update the table
			[self.tableView beginUpdates];
			[self.tableView insertRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationAutomatic];
			[self.tableView endUpdates];
		}
		[self.tableView finishedAppendingData];
	}];
	return YES;
}

@end

The notable elements are in blue. Our delegate method -appendMoreDataToTableView: handles the operation of appending more data; if more data is not available, endless scrolling ends.


Problem solved, right? And in such a clever way.

Well, not quite.

First, because we used a category and method swizzling, a casual user who has never seen this code before would not know how endless scrolling was hooked into our system. It’s not readily apparent, for example, that our category has added endless scrolling to all UITableViews and not just the one we’ve enabled, by virtue of the +load method. Worse, if you have an application with two or three dozen table views, only one of which implements endless scrolling, it may be nearly impossible for a new user to realize your category even exists.

This is important, because second: our method swizzling takes place on startup, and because all tables are affected (and not just the ones with endless scrolling) any bugs in our endless scrolling code will break all table views (and not just the ones we’ve added our functionality to). This means that a bug in a chunk of code that is entirely invisible to the user who is maintaining your code (because the <UITableView+EndlessScroll> category still affects a table view even though the header wasn’t loaded) will wind up with an exceedingly hard to understand crash, thanks to swizzling. What should be an easy to understand issue may wind up looking like an internal iOS bug.

Third, supporting the swizzling operation makes our code considerably longer and more complicated than it needs to be.


Suppose instead of adding a category we created a child class of our UITableView. This would have the nice property that only tables with endless scrolling would be affected by any bugs in our code. And it eliminates our need for swizzling and for tracking internal state using an association.

Our declaration for our child class is very similar to the category:

#import <UIKit/UIKit.h>

@protocol UITableViewEndlessDelegate <UITableViewDelegate>
- (BOOL)appendMoreDataToTableView:(UITableView *)tableView;
@end

@interface ESTableView : UITableView

- (void)setEndlessScrollingEnabled:(BOOL)flag;
- (BOOL)endlessScrollingEnabled;

- (void)finishedAppendingData;

@end

However, our class becomes considerably more concise, and more easily understood.

Instead of maintaining a property we can use class properties:

@interface ESTableView ()
@property (assign) BOOL initialized;
@property (assign) BOOL loading;
@end

Our methods for starting and stopping endless scrolling becomes considerably simpler as well:

- (BOOL)endlessScrollingEnabled
{
	return self.initialized;
}

- (void)setEndlessScrollingEnabled:(BOOL)flag
{
	self.initialized = flag;
}

Instead of swizzling we do a method override:

- (void)setContentOffset:(CGPoint)contentOffset
{
	[super setContentOffset:contentOffset];

	if (self.initialized) {
		[self es_testBounds];
	}
}

- (void)setContentSize:(CGSize)contentSize
{
	[super setContentSize:contentSize];

	if (self.initialized) {
		[self es_testBounds];
	}
}

And the rest remains more or less the same. Our complete ESTableView class becomes:

#import "ESTableView.h"

@interface ESTableView ()
@property (assign) BOOL initialized;
@property (assign) BOOL loading;
@end

@implementation ESTableView

- (void)setContentOffset:(CGPoint)contentOffset
{
	[super setContentOffset:contentOffset];

	if (self.initialized) {
		[self es_testBounds];
	}
}

- (void)setContentSize:(CGSize)contentSize
{
	[super setContentSize:contentSize];

	if (self.initialized) {
		[self es_testBounds];
	}
}

- (BOOL)endlessScrollingEnabled
{
	return self.initialized;
}

- (void)setEndlessScrollingEnabled:(BOOL)flag
{
	self.initialized = flag;
}

- (void)finishedAppendingData
{
	self.loading = NO;
	[self es_testBounds];
}

/*
 *	Endless scrolling logic. This is simple; all we do is test if the
 *	y position is at max, and if it is, trigger our event.
 */

- (CGFloat)es_triggerOffset
{
	CGFloat h = self.contentSize.height;
	UIEdgeInsets insets = self.contentInset;
	h -= self.bounds.size.height - insets.top - insets.bottom;
	if (h < 0) h = 0;
	return h;
}

- (void)es_testBounds
{
	if (self.loading) return;	// we're loading

	CGFloat ypos = self.contentOffset.y;
	if (ypos >= self.es_triggerOffset - 44) {	// We're close to bottom
		self.loading = YES;
		BOOL flag = [(id<UITableViewEndlessDelegate>)self.delegate appendMoreDataToTableView:self];
		if (!flag) self.endlessScrollingEnabled = NO;
	}
}

@end

Of course this requires a minor change in our view controller: we need to replace our UITableView with an ESTableView. But the rest of the View Controller logic is absolutely identical.


Now we’ve replaced our Objective-C specific category, swizzle and property-based solution with a more traditional child object and object inheritance solution, we’ve now done a few things:

First, we have isolated any bugs in our endless scrolling code to just the table views which have endless scrolling. This is great because it means if there is a defect in your endless scrolling code, it won’t present itself in some odd location elsewhere in the application as if it were a bug in the iOS operating system.

Second, and more importantly, because we now have to switch the UITableView to an ESTableView for endless scrolling, we signal in the code those places were we are using the enhanced functionality. This allows someone new to the code to understand that new functionality was added, and to allow them some insight as to where to find the new functionality (in the ESTableView class).

In other words, it has become exceedingly obvious that a class with new functionality has been added, while avoiding potentially dangerous constructs such as swizzling.


Of course there are times when using swizzling and attaching properties dynamically to a table helps solve a deep problem. However, most of the time there is no reason why creating a child class wouldn’t give the same results, and in the process isolate any problems while making it blinding obvious to a new developer what is going on.

And if it is blindingly obvious you have extended the functionality by creating a child class, then you will have to answer fewer questions from any new developers who take over your code.

Which means you can get home to your life sooner, while maintaining your level of productivity.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s