Objective-C best practices - practical examples

Forget the youth and beauty of Swift for a minute. Study this open source project for some concrete examples of good iOS engineering practices that you hear about all the time but may not understand.

  • Self-documenting code
  • Hiding implementation details with encapsulation
  • Making code modular, loosely coupled, and easier to debug
  • Using delegates to communicate

MELSortingView in the flesh


MELSortingView is a dynamic control useful for games, pickers, settings, and menus.
The view subclasses UIView and can be initialized with a instanceType convenience constructor. Initializing like this allows more flexibility in your constructor while helping to guide consumers of your API to understand and not use the control in unintended ways. The method signature acts as self-documentation. Other benefits of using instanceType can be found here.
Inside the constructor, we limit how small the sorting view’s width can be with an NSAssert. This keeps us in tune with UIKit’s self-imposed limitations for the size of controls i.e. switches and sliders. We also ensure that at least two subviews are requested (sorting is the control's raison d'etre, so give it something to sort). Apple justifies these rules with its famous HIG.

+ (instancetype)sortingViewWithViews:(NSInteger)views XOffset:(CGFloat)XOffset YOffset:(CGFloat)YOffset andWidth:(CGFloat)width
{
	return [[self alloc] initWithViews:views XOffset:XOffset YOffset:YOffset andWidth:width];
}

- (id)initWithViews:(NSInteger)views XOffset:(CGFloat)XOffset YOffset:(CGFloat)YOffset andWidth:(CGFloat)width
{
	NSAssert(views > 2 && views < 8, NSLocalizedString(@"You must have at least 2 and at most 6 views", nil));
	NSAssert(width >= 100, NSLocalizedString(@"View width must be at least 100", nil));

	CGFloat height = width * 1.775;
	CGFloat subViewHeight = height/views;
	CGRect viewFrame = CGRectMake(XOffset, YOffset, width, height);

	self = [super initWithFrame:viewFrame];

	if (self)
	{
	self.dragSubjects = [NSMutableArray array];
	self.dropAreas = [NSMutableArray array];
	self.dictionary = [NSMutableDictionary dictionary];


	for (NSInteger i = 0; i < views; i++)
	{
	UIView *dragView = [[UIView alloc]initWithFrame:CGRectMake(0, i*subViewHeight, width, subViewHeight)];
	dragView.tag = i;
	dragView.userInteractionEnabled = YES;
	[dragView setBackgroundColor:[UIColor colorWithRed:(77-(i*10))/255.f green:(77-(i*10))/255.f blue:(255-(i*20))/255.f alpha:255/255.f]];
	[self addSubview:dragView];
	[self.dragSubjects addObject:dragView];

	UIView *dropView = [[UIView alloc]initWithFrame:CGRectMake(0, i*subViewHeight, width, subViewHeight)];
	dropView.tag = i;
	dropView.userInteractionEnabled = NO;
	[self addSubview:dropView];
	[self.dropAreas addObject:dropView];

	[self.dictionary setObject:dragView forKey:[NSNumber numberWithInteger:dropView.tag]];

	self.panManager = [[PanManager alloc] initWithDragSubjects:self.dragSubjects 
	andDropAreas:self.dropAreas];

	self.panManager.delegate = self;

	UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.panManager 
	action:@selector(handlePan:)];

	[self addGestureRecognizer:pan];

 }
}

return self;
}

After the checks, create the number of subviews requested in a loop, dynamically determining their height and color.
For each subview create an almost-identical 'dropView' subview that will facilitate the swapping of views later on. The only difference being that userInteractionEnabled is false for these constant views.
Expose one method addLabels but use the initialization data from the constructor to set the labels with the proper dimensions. You could extend this method to include font type and other decoration parameters but you should leave the label's size and positioning up to the implementation.

//MELSViewController.m
- (void)viewDidLoad
{
  ...
  [sortView addLabels];
  
}
 
//MELSortingView.h
@interface MELSortingView : UIView
  ...
  - (void)addLabels;
 
@end
 
//MELSortingView.m
- (void)addLabels
{
  //implementation details based on the view's constructor
}
Separate (loosely couple) the panning and swapping business logic of our view by creating a separate 'panManager' class. Then, if you wanted pinching or tapping functionality, you would create separate managers for them. This keeps code modular, easier to extend, more testable, and easier to use across projects. panMananger also uses a custom initializer for self-documentation and also exposes only one method handlePan so that it can be the target of our sorting view's UIPanGestureRecognizer. This is much cleaner than creating a new gesture recognizer for each subview.

//MELSortingView.m

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.panManager action:@selector(handlePan:)];
            [self addGestureRecognizer:pan];
            
//panManager.h
- (void)handlePan:(UIPanGestureRecognizer *)recognizer;
Inside handlePan, monitor the recognizer's began state, changed state and ended state. Use Core Graphics geometry to see which view is being dragged and where it's being dropped. Delegate methods are fired when these conditions are met.
How the panning, snapping, and swapping actually works is for another post. Email me with questions.
For communication, MELSortingView uses a delegate chain so that messages bubble up to the view controller that owns the sorting view. Why is this preferable to having both the sorting view and the view controller subscribe to NSNotifications? Standard arguments include:
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 - a good recipe for spaghetti and/or a long debug session for maintainers of your code.
2) You only have one observer for your event (one-to-one) while notifications are system-wide with multiple observers (one-to-many).
3) You prevent the case where a notification is sent to a nil object.
4) Most importantly, NSNotifications are dispatched synchronously to all subscribers simultaneously. So by using delegation, we ensure that our view controller only receives the info after the UI has finished updating.
Therefore, our loosely coupled delegate pattern has first the sorting view, and then the view controller (if it conforms to the MELSortingViewDelegate), receiving messages for when a subview is panned, or dropped-and-swapped.

//panManger.h
@protocol PanManagerDelegate 
 
@optional
 
- (void)viewWasMovedWithView:(UIView *)view;
- (void)view:(UIView *)view didAlternateWithView:(UIView *)destinationView fromOriginalRect:(CGRect)originalRect;
 
@end
 
//MELSortingView.h
@protocol MELSortingViewDelegate 
 
@optional
 
- (void)view:(MELSortingView *)sortingView wasMovedWithView:(UIView *)aView;
- (void)view:(MELSortingView *)sortingView didAlternateView:(UIView *)departureView withView:(UIView *)destinationView;
 
@end
 
//MELSViewController.m
#pragma mark - MELSortingViewDelegate
 
- (void)view:(MELSortingView *)sortingView wasMovedWithView:(UIView *)aView
{
    NSLog(@"a view at position %ld was moved", aView.tag);
}
 
- (void)view:(MELSortingView *)sortingView didAlternateView:(UIView *)departureView withView:(UIView *)destinationView
{
    NSLog(@"a view at position %ld was swapped with a view at position %ld", (long)departureView.tag, (long)destinationView.tag);
}

So that's a bit about how I think about design. Hope you enjoyed. You can view, download, and use MELSortingView here.
Copyright © 2011–2015 Mike Leveton