Objective-C Blocks are very powerful but often under-used or badly abused! Continuing on from my previous post on how to declare a block, here is a quick-fire list of 5 experience-earned tips that every developer should know.
1. typedef a block
If you find yourself using a particular block syntax a lot or perhaps you are using it in your delegate methods too, define it using typedefs in the header.
typedef void (^RWDownloadHandler)(BOOL success, NSError * _Nullable error);
// Then you can use it like so in properties
@property (nonatomic, copy, nullable) RWDownloadHandler completion;
// And with methods like so
- (void)animateWithCompletion:(nullable RWDownloadHandler)completion;
// Or use it within instance variables
RWDownloadHandler handler = ^void(BOOL success, NSError * _Nullable error) {
// Code
};
2. Minimise asynchronous block calls in UI methods
This might seem obvious but many developers often throw around dispatch_async in their UI code a lot. A typical pattern involves downloading or calculating some value in the background using a block and then calling a completion handler block using dispatch_async on the main thread. This can be particularly useful pattern to embed in a cell or view controller.
- (void)downloadImageURL:(NSURL *)URL completion:(void (^_Nullable)(UIImage *image, NSError *error))completion
{
NSAssert([NSThread isMainThread], @"UI method, call from main thread");
// Check the cache
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
// Calculate data or load from cache
dispatch_async(dispatch_get_main_queue(), ^{
self.image = image;
completion();
});
});
}
If you are downloading an image and then caching it, the first time round this will work well however the second time you already have the image but are still calling the block in the background and then the main thread. In this case, calling dispatch_async typically queues the work onto the end of the main run loop and so the image won’t refresh until the end of that run loop. This means you can end up with UI flickering when it shouldn’t.
The way to solve this is to do the cache and UI check on the main thread first. If the cache check is too slow or loading the cached image is too slow, consider caching an optimised image (matching UI size, etc).
- (void)downloadImageURL:(NSURL *)URL size:(CGSize)placeholderSize completion:(void (^_Nullable)(UIImage *image, NSError *error))completion
{
NSAssert([NSThread isMainThread], @"UI method, call from main thread");
// Setup update UI completion block
void (^updateUI)(UIImage *image) = ^void(UIImage *image) {
self.image = image;
completion();
};
// Check the cache
UIImage *cachedImage = ..;
if (cachedImage) {
updateUI(cachedImage);
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
// Calculate data or load from cache
UIImage *image = ..;
// Resize and optimise image
image = [image resize:placeholderSize];
// Save to cache
// Return back to main thread.
dispatch_async(dispatch_get_main_queue(), updateUI(image));
return;
});
}
3. Watch out for Nil blocks
When designing an API using blocks be careful with creating methods that require a block such as an animation method. It is common practice to pass in a nil value and if you don’t code for it, this will crash your app. Fix this by testing for the block.
- (void)animateWithCompletion:(void (^_Nullable)(BOOL animated))completion
{
if (completion) {
completion(NO);
}
}
You might wonder why a nil value would crash when most Cocoa conventions allow nils to fall through silently… Matt Galloway has a great explanation on why a nil block crashes and he has also written some great, very deep blog posts on blocks – see A look inside blocks: Episode 1 and A look inside blocks: Episode 2. Be prepared to learn a bit of Assembly for Episode 2!
4. Block Queues
Often in an app you’ll need to download some data from a server. Very frequently this is abstracted away as a controller class with some method on the controller to fetch the required data and fire a completion block when done. You call the download method multiple times in different places but the controller only has one instance and you cache the data in the controller. This all sounds perfect, right? Not so fast! An issue arrises where if you are calling your download method multiple times before the first call has had a chance to download and cache the data, you’ll find you have multiple download requests being executed and only the last one is cached. Worse, you’ll actually notice only the last completion handler gets called because each time it is reset!
Exploring this problem in detail, it’s easy to see that each download call has a completion handler and that this gets stored for use later when the data has downloaded. However another request has still come in, found the data has not be cached yet and issues another request, releasing the old completion handler. The solution is to queue your completion blocks and flush the queue when the request completes!
#import "VRStoreController.h"
@interface VRStoreController ()
@property (nonatomic, strong) NSMutableArray *receiptBlockQueue;
@property (nonatomic, weak) NSURLSessionTask *receiptTask;
@end
@implementation VRStoreController
- (instancetype)init
{
if (self = [super init]) {
self.receiptBlockQueue = [NSMutableArray arrayWithCapacity:1];
}
return self;
}
- (void)checkReceiptWithCompletion:(void (^)(VRSubscriptionResponse *response, NSError *error))receiptBlock
{
// If we have a block, add it to the queue because blocks act like Obj-C objects
if (receiptBlock) {
[self.receiptBlockQueue addObject:receiptBlock];
}
// Make sure if a task is already executing, don't fire another one
if (self.receiptTask) {
return;
}
// Create URL task and execute
NSURLSessionTask *receiptTask = [.. completion:^{
// .. process data
[self flushCompletionBlocksWithResponse:.. error:..];
}];
[receiptTask resume];
self.receiptTask = receiptTask;
}
- (void)flushCompletionBlocksWithResponse:(VRSubscriptionResponse *)response error:(NSError *)error
{
NSArray *receiptBlocks = [self.receiptBlockQueue copy];
[self.receiptBlockQueue removeAllObjects];
for (ReceiptUploadCompletionBlock complete in receiptBlocks) {
complete(response, error);
}
}
@end
5. Don’t use blocks!
This might be a cheat tip but often a block isn’t the solution you want to use. The problem with blocks is there is no way to cancel a currently executing block. If you suspend the app on a long-running block, the iOS watchdog timer will be triggered and the app will crash. One way around this is to wrap your long-running code into an NSOperation, ensuring you have broken it up and added cancel checks regularly. You can then cancel the operation when suspending your app!