Ad

Our DNA is written in Swift
Jump

GCD, ARC, Blocks – Oh How Simple!

I’m “totally” migrating my iCatalog framework project to ARC, GCD and blocks and I’d like to share with you some of the revelations that the use of these modern technologies brings with them.

Here are two examples of the kind of simplifications you will see if you do the same. This approach is compatible with iOS 4.0 and above.

Executing a Multi-Parameter Method on Main Thread

I was looking for instances of performSelectorOnMainThread so that I could replace them with a dispatch_sync on main_queue instead. Previously when calling methods that take more than one parameter you had to jump through hoops.

Before:

- (void)reportAddProduct:(Product *)product toBagAs:(BagItem*)bagItem
{
	SEL selector = @selector(onlineIntegrationManager:willAddProduct:toBagAs:);
	if ([delegate respondsToSelector:selector])
	{
		NSMethodSignature *signature = [self methodSignatureForSelector:selector];
		if (!signature) 
		{
			return;	
		}
 
		NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
		[invocation setTarget:delegate];
		[invocation setSelector:selector];
		[invocation setArgument:&product atIndex:2];
		[invocation setArgument:&bagItem atIndex:3];
		[invocation retainArguments];
		[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:YES];
	}
}

After:

- (void)reportAddProduct:(Product *)product toBagAs:(BagItem *)bagItem
{
	if ([delegate respondsToSelector:@selector(onlineIntegrationManager:willAddProduct:toBagAs:)])
	{
		dispatch_sync(dispatch_get_main_queue(), ^{
			[delegate onlineIntegrationManager:self willAddProduct:product toBagAs:bagItem];
		});
	}
}

The handy dispatch_sync on main queue can call any method, with any number of parameters on the main thread. The sync is equivalent to the waitUntilDone:YES. Since this allows for putting the main thread code inline you no longer need to have special methods that you then performSelectorOnMainThread:.

As a second note I found that it calls to the main thread should generally be done synchronously because I had a situation where a UILabel would not get updated if I did waitUntilDone:NO. Having everything as dispatch_sync now also takes care of that.

Abusing the UIView Animation Context

Before we had block based animation I would find myself often passing some context to the animationDidStopSelector so that I can do some wrap-up work when the animation is done. This is quite unsafe, even ARC complains about that unless you add a (__bridge id) to tell it to ignore the memory management of this object.

Context parameters didn’t get retained as a rule of thumb. In this example I needed to remove a view that has been animated out also from an internal dictionary after it was done. Having a dedicated instance variable would not have been an option because you could potentially have multiple animations going on at the same time.

Before:

- (void)removalAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
	UIView *removedView = (__bridge UIView *)context;
 
	for (NSNumber *oneKey in [self.viewsForItems allKeys])
	{
		UIView *view = [self.viewsForItems objectForKey:oneKey];
 
		if (view == removedView)
		{
			[removedView removeFromSuperview];
			[self.viewsForItems removeObjectForKey:oneKey];
		}
	}
}
 
// the above being called by:
 
for (id oneItem in itemsToRemove)
{
	// we must have a view for this item to remove it
	__unsafe_unretained UIView *itemView = [self viewForItem:oneItem];
 
	if (animated)
	{
		[UIView beginAnimations:@"ItemRemoval" context:(__bridge void *)itemView];
		[UIView setAnimationDuration:deleteAnimationDuration];
		if (delaySoFar>0)
		{
			[UIView setAnimationDelay:delaySoFar];
		}
		[UIView setAnimationBeginsFromCurrentState:YES];
		[UIView setAnimationDidStopSelector:@selector(removalAnimationDidStop:finished:context:)];
		[UIView setAnimationDelegate:self];
	}
 
	itemView.alpha = 0;
	itemView.transform =CGAffineTransformMakeScale(0.5, 0.5);
 
	if (animated)
	{
		[UIView commitAnimations];
	}
 
	delaySoFar +=deleteAnimationOffset;
}

After:

// moved into a convenience method
- (void)removeCachedView:(UIView *)removedView
{
	for (NSNumber *oneKey in [self.viewsForItems allKeys])
	{
		UIView *view = [self.viewsForItems objectForKey:oneKey];
 
		if (view == removedView)
		{
			[removedView removeFromSuperview];
			[self.viewsForItems removeObjectForKey:oneKey];
		}
	}
}
 
// called from:
 
for (id oneItem in itemsToRemove)
{
	// we must have a view for this item to remove it
	__unsafe_unretained UIView *itemView = [self viewForItem:oneItem];
 
	[UIView animateWithDuration:(animated?deleteAnimationDuration:0)
						  delay:(animated?delaySoFar:0)
						options:UIViewAnimationOptionBeginFromCurrentState 
					 animations:^{
						 itemView.alpha = 0;
						 itemView.transform =CGAffineTransformMakeScale(0.5, 0.5);
					 } completion:^(BOOL finished) {
						 [self removeCachedView:itemView];
					 }];
 
	delaySoFar +=deleteAnimationOffset;
}

A boolean animated value can easily be replaced with a duration of 0. Animation delays are passed in the appropriate method and the beginsFromCurrentState is now a UIViewAnimationOption. This method is way safer because the blocks “capture” (i.e.) retain all the used variables making it impossible for the thing that was previously passed via the context to already been deallocated.

Conclusion

Sorry for not having more wise words to add here. The beauty of the possible simplifications you get if you decide to make iOS 4 your minimum deployment target should speak for itself.


Categories: Updates

Leave a Comment