Mocking a UIKit Delegate Protocol with Kiwi

I'm experimenting with mock objects in Kiwi and decided to try and mock the UIAlertViewDelegate. I ran into two issues. 1. Order is important when creating the mock delegate object. It needs to be setup with all the delegate method receive calls before you use the object associated with the delegate protocol.

2.  The delegate protocol methods need to be mocked in the order they are invoked, or you'll receive a test failure. This can be trial and error, if you don't know how the delegate protocol works.

a. For each mock delegate test, I just kept running until I was able to get the mock delegate calls passing.

b. I also discovered that the didDismissWithButtonIndex and  clickedButtonAtIndex were not invoked after using the dismissWithClickedButtonIndex method call.

[objc] #import "Kiwi.h" #import "PSMessages.h" #import "PSMessageConstants.h"

SPEC_BEGIN(PSMessagesTest)

describe(@"Create an alertview", ^{ __block PSMessages *messages = nil; beforeEach(^{ messages = [[PSMessages alloc] init]; });

NSDictionary *urlMessage = @{ @"type" : @"url", @"url" : @"http://www.PhotoTableApp.com", @"message" : @"Create a collage for the next holiday.", @"title" : @"Design Collages", @"buttons" : @[@"Yes", @"No"] };

context(@"with a url message", ^{ __block UIAlertView *alert = nil; beforeEach(^{ alert = [messages alertForMessage:urlMessage]; }); it(@"the cancel button is pressed", ^{ id delegateMock = [KWMock mockForProtocol:@protocol(UIAlertViewDelegate)]; alert.delegate = delegateMock;

int buttonIndex = alert.cancelButtonIndex;

[[[delegateMock shouldEventually] receive] alertViewShouldEnableFirstOtherButton:alert]; [[[delegateMock shouldEventually] receive] didPresentAlertView:alert]; [[[delegateMock shouldEventually] receive] willPresentAlertView:alert]; [[[delegateMock shouldEventually] receive] alertView:alert willDismissWithButtonIndex:buttonIndex]; [[[delegateMock shouldEventually] receive] alertView:alert willDismissWithButtonIndex:buttonIndex];

// Note: delegate methods not called for programatic UIAlertView dismiss [[[delegateMock shouldEventually] receive] alertView:alert didDismissWithButtonIndex:buttonIndex]; [[[delegateMock shouldEventually] receive] alertView:alert clickedButtonAtIndex:buttonIndex];

// Invoke methods after the delegateMock object is setup [alert show]; [alert dismissWithClickedButtonIndex:buttonIndex animated:NO];

});

it(@"the ok button is pressed", ^{ id delegateMock = [KWMock mockForProtocol:@protocol(UIAlertViewDelegate)]; alert.delegate = delegateMock;

int buttonIndex = alert.firstOtherButtonIndex;

[[[delegateMock shouldEventually] receive] alertViewShouldEnableFirstOtherButton:alert]; [[[delegateMock shouldEventually] receive] didPresentAlertView:alert]; [[[delegateMock shouldEventually] receive] willPresentAlertView:alert]; [[[delegateMock shouldEventually] receive] alertView:alert willDismissWithButtonIndex:buttonIndex]; [[[delegateMock shouldEventually] receive] alertView:alert willDismissWithButtonIndex:buttonIndex];

// Invoke methods after the delegateMock object is setup [alert show]; [alert dismissWithClickedButtonIndex:buttonIndex animated:NO];

}); });

}); SPEC_END [/objc]

Unit Testing Static Libraries with Kiwi for iOS Development

I've been playing with Kiwi and I'm trying some BDD (Behavior Driven Development) for a new static library component I wanted to build. I began with a new Xcode project using the Static Library template, but ran into issues with the difference between "logic tests" and "application tests". In short, all my non-UIKit code worked great, until I started to test my UIKit related functions.

The code crashed and makes it frustrating to write unit tests. If you've never experienced it before, it'll make your unit test experience unproductive. To solve the issue you'll need to create a new target (empty iOS application) and include unit tests. Xcode will automagically setup the unit tests to be "Application Tests" instead of "logic tests."

EmptyApp

#0 0x00a40881 in __HALT () #1 0x0097a971 in _CFRuntimeCreateInstance () #2 0x01337cc1 in GSFontCreateWithName () #3 0x05c32281 in UINewFont () #4 0x05c323ec in +[UIFont systemFontOfSize:traits:] () #5 0x05c32438 in +[UIFont systemFontOfSize:] () #6 0x05be24ee in +[UILabel defaultFont] () #7 0x05be32e5 in -[UILabel _commonInit] () #8 0x05be3424 in -[UILabel initWithFrame:] () #9 0x05e7cc67 in -[UIAlertView(Private) _createTitleLabelIfNeeded] () #10 0x05e8b4b9 in -[UIAlertView setTitle:] () #11 0x05e8bb37 in -[UIAlertView initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:] () #12 0x02605831 in __block_global_23 at /Users/paulsolt/dev/Photo-Slide-Show/PSMessages/PSMessagesTests/PSMessagesTest.m:165 #13 0x0261b584 in __25-[KWExample visitItNode:]_block_invoke_0 at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWExample.m:220 #14 0x0261a11e in __42-[KWContextNode performExample:withBlock:]_block_invoke_0 at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:116 #15 0x0261a11e in __42-[KWContextNode performExample:withBlock:]_block_invoke_0 at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:116 #16 0x0261a11e in __42-[KWContextNode performExample:withBlock:]_block_invoke_0 at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:116 #17 0x0261a03e in -[KWContextNode performExample:withBlock:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:132 #18 0x0261a05d in -[KWContextNode performExample:withBlock:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:135 #19 0x0261a05d in -[KWContextNode performExample:withBlock:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWContextNode.m:135 #20 0x0261b539 in -[KWExample visitItNode:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWExample.m:216 #21 0x0261a553 in -[KWItNode acceptExampleNodeVisitor:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWItNode.m:41 #22 0x0261ae22 in -[KWExample runWithDelegate:] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWExample.m:113 #23 0x02618c95 in -[KWSpec invokeTest] at /Users/paulsolt/dev/Photo-Slide-Show/Frameworks/Kiwi/Kiwi/KWSpec.m:105 #24 0x2010405b in -[SenTestCase performTest:] () #25 0x201037bf in -[SenTest run] () #26 0x2010792b in -[SenTestSuite performTest:] () #27 0x201037bf in -[SenTest run] () #28 0x2010792b in -[SenTestSuite performTest:] () #29 0x201037bf in -[SenTest run] () #30 0x201063ec in +[SenTestProbe runTests:] () #31 0x0072f5c8 in +[NSObject performSelector:withObject:] () #32 0x00002342 in ___lldb_unnamed_function11$$otest () #33 0x000025ef in ___lldb_unnamed_function13$$otest () #34 0x0000268c in ___lldb_unnamed_function14$$otest () #35 0x00002001 in ___lldb_unnamed_function4$$otest () #36 0x00001f71 in ___lldb_unnamed_function1$$otest ()

Solution

1. Add a new target with it's own unit tests. Creating unit tests with a "static library" template gives you "logic tests", while creating unit tests with a "iPhone application" gives you "application tests." The difference is that you can't use UIKit classes in logic tests, but you can in application tests.

EmptyApp

2. In an application test, the runloop of the iPhone app starts, which means all the UIKit goodies are setup. The bad news is that it loads all the default state from your previous app runs. You might need to write some methods to cleanup or reset state. (GHUnit is nice because it's more sandboxed)

3. If you're using Kiwi you'll have to setup the Kiwi environment (library/header paths) again for the application tests.

ApplicationUnitTest

UICollectionView Custom Actions and UIMenuController

The UICollectionView can provide a special UIMenuController with cut, copy, and paste actions. To add UICollectionView custom actions you need to implement a few extra methods for the shared UIMenuController object. The view controller's parent window needs to be the key window and you'll need to respond to UIResponder method canBecomeFirstResponder. In your UICollectionViewController class do the following:

[objc] // ViewController.h @interface ViewController : UICollectionViewController

// ViewController.m -(void)viewDidLoad { [super viewDidLoad]; self.collectionView.delegate = self;

UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Custom Action" action:@selector(customAction:)]; [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];

}

#pragma mark - UICollectionViewDelegate methods - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { return YES; // YES for the Cut, copy, paste actions }

- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath { return YES; }

- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { NSLog(@"performAction"); }

#pragma mark - UIMenuController required methods - (BOOL)canBecomeFirstResponder { // NOTE: This menu item will not show if this is not YES! return YES; }

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { NSLog(@"canPerformAction"); // The selector(s) should match your UIMenuItem selector if (action == @selector(customAction:)) { return YES; } return NO; }

#pragma mark - Custom Action(s) - (void)customAction:(id)sender { NSLog(@"custom action! %@", sender); } [/objc]

Here's what it looks like:

UIMenuController Custom UICollectionView

Linking to a Facebook Page from an iOS App

The Facebook app broke the old way of creating an iOS facebook page link. If the app isn't installed the old facebook link works, but when it is installed it just opens the Facebook app to the default page. Fix your app links to Facebook using this url format:

Old iOS Facebook Page Link:

http://www.facebook.com/PhotoTableApp

New iOS Facebook Page Link:

https://m.facebook.com/PhotoTableApp?_rdr

The only downside is that the user would have to login via the mobile website in order to "like" your Facebook page. It's certainly better than just seeing the "default" screen in the app. I'm not sure if the new Facebook SDK 3.1 fixes any of these issues, but I haven't seen a solution on Stackoverflow.com

 

iPhone Link to Facebook Page fails with App

Linking to a facebook page doesn't work from iOS if the new Facebook app is installed.

iPhone Link to Facebook Page Works

Linking to the mobile Facebook page with the ?_rdr flag fixes the issue.

Using the social.framework on iOS 6.0

Using the social.framework is real simple on iOS 6.0. Apple only refers to their reference, so I decided to show the code snippet example. To change between twitter and  Sina Weibo, just use the types: SLServiceTypeSinaWeibo SLServiceTypeTwitter. It just takes 10 lines of code. If you used the Twitter.framework, you can remove it and replace your Twitter code with the following code. The Social.framework will manage all social networks moving forward.

if([SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {

SLComposeViewController *socialSheet = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook];

[socialSheet setInitialText:@"posted from @PhotoTable"];

[socialSheet addImage:image];

[socialSheet setCompletionHandler:^(SLComposeViewControllerResult result) {

NSLog(@"Result: %d", result);

}];

[self presentViewController:socialSheet animated:YES completion:^ {

}];

}

iOS: Converting UIImage to RGBA8 Bitmaps and Back

Edited 8/24/11: Fixed a bug with alpha transparency not being preserved. Thanks for the tip Scott! Updated the gist and github project to test transparent images.

Edited 12/13/10: Updated the code on github/gist to fix static analyzer warnings. Changed a function name to conform to the Apple standard.

When I started working with iPhone I was working with Objective-C and C++. I created a library in C++ and needed access to a bitmap array so that I could perform image processing. In order to do so I had to create some helper functions to convert between UIImage objects and the RGBA8 bitmap arrays.

Here are the updated routines that should work on iPhone 4.1 and iPad 3.2. The iPhone 4 has a high resolution screen requires setting a scaling factor for high resolution images. I've added support to set the scaling factor based on the devices mainScreen scaling factor

UPDATE: 9/23/10 My code to work with the Retina display was incorrect, it ran fine on iPad with 3.2, but it didn't do anything "high-res" on iPhone 4. I was using the following:

__IPHONE_OS_VERSION_MAX_ALLOWED >= 30200

but it isn't safe, when I run it for a universal App 4.1/3.2 it will always return 40100, and the expression didn't make sense. (Side Note) I took this check from Apple's website when iPad 3.2 was actually ahead of iPhone 3.1.X, but that doesn't help with iPhone 4.1 being ahead of iPad 3.2.

The issue with iPad is that the imageWithCGImage:scale:orientation: selector doesn't exist on iOS 3.2, most likely it will on iOS 4.2, so the following code should be safe. Some methods in iOS 4.1 don't exist in iOS 3.2, so you need to check to see if a newer method exists before trying to execute it. There are two methods you can use depending on the class/instance (+/-) modifier on the function definition.

+ (BOOL)respondsToSelector:(SEL)aSelector   // (+) Class method check
+ (BOOL)instancesRespondToSelector:(SEL)aSelector   // (-) Instance method check

imageWithCGImage:scale:orientation is a class method, so we need to use respondsToSelector: The correct code to scale the CGImage is below:

if([UIImage respondsToSelector:@selector(imageWithCGImage:scale:orientation:)]) {
	float scale = [[UIScreen mainScreen] scale];
	image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
} else {
	image = [UIImage imageWithCGImage:imageRef];
}

[ad#Large Box]

It might help if there was some images to explain what's happening if you don't use this imageWithCGImage:scale:orientation: on the iPhone 4 with the correct scale factor. It should be 2.0 on Retina displays (iPhone 4 or the new iPod Touch) and 1.0 on the 3G, 3GS, and iPad. float scale = [[UIScreen mainScreen] scale]; will provide the correct scale factor for the device. The first image has jaggies in it, while the second does not. The third image, an iPhone 3G/3GS, also does not have jaggies.

[caption id="attachment_697" align="aligncenter" width="451" caption="iPhone 4 with default scale of 1.0 causes the image to be enlarged and with jaggies."][/caption]

[caption id="attachment_698" align="aligncenter" width="451" caption="iPhone 4 with scaling of 2.0 makes the image half the size and removes the jaggies"][/caption]

[caption id="attachment_692" align="aligncenter" width="414" caption="iPhone 3G/3GS with scaling set to 1.0"][/caption]

I hope it helps other people with image processing on the iPhone/iPad. It's based on some previous tutorials using OpenGL, which I fixed (memory leaks) and modified to work with unsigned char arrays (bitmap).

[ad#Link Banner]

Grab the two files here or the sample Universal iOS App project:

Example Usage:

NSString *path = (NSString*)[[NSBundle mainBundle] pathForResource:@"Icon4" ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
int width = image.size.width;
int height = image.size.height;

    // Create a bitmap
unsigned char *bitmap = [ImageHelper convertUIImageToBitmapRGBA8:image];

    // Create a UIImage using the bitmap
UIImage *imageCopy = [ImageHelper convertBitmapRGBA8ToUIImage:bitmap withWidth:width withHeight:height];

    // Display the image copy on the GUI
UIImageView *imageView = [[UIImageView alloc] initWithImage:imageCopy];

    // Cleanup
free(bitmap);

Below is the full source code for converting between bitmap and UIImage:

ImageHelper.h

ImageHelper.m

[ad#Large Box]