Exploring NSDateFormatter using F-Script as a REPL

April 26th, 2010 ento No comments

(here be preface)

Let’s start by instantiating a formatter and a date.

> fmt := NSDateFormatter new.
 
> fmt
<NSDateFormatter: 0x2004cec80>
 
> now := NSDate date.
 
> now
2010-04-25 18:03:26 +0900

Note that assignment is :=, not =, and no angled brackets around method calls. Also, a sentence ends in a period, not a semicolon.

OK, what does the virgin formatter output when we feed it a date?

> fmt stringFromDate:now
''

Hmm, after consulting the Xcode Developer Documentation, it seems that we need to set a format string to get a non-empty result. There are also shortcut methods provided to easily set the formats for the date part and the time part.

The argument to these methods is a constant, which we have easy access to in F-Script.

> NSDateFormatterNoStyle
0
 
> fmt setDateStyle:NSDateFormatterMediumStyle
 
> fmt dateFormat
'MMM d, yyyy'
 
> fmt stringFromDate:now
'Apr 25, 2010'

There, the date. Let’s try setting the time part format.

> fmt setTimeStyle:NSDateFormatterMediumStyle
 
> fmt dateFormat
'MMM d, yyyy h:mm:ss a'
 
> fmt stringFromDate:now
'Apr 25, 2010 6:03:26 PM'

With #setDateFormat:, we have more control over the formatting. You can even include arbitrary string. (Which I found out after reading the date format patterns specification adopted by NSDateFormatter.)

> fmt setDateFormat:'''Year''YYYY'
 
> fmt dateFormat
''Year'YYYY'
 
> fmt stringFromDate:now
'Year2010'

(Note: this is a draft. More details will be filled in afterwards.)

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • StumbleUpon
  • Twitter
Categories: Uncategorized Tags:

Implicit animation of custom CALayer properties

March 14th, 2010 ento No comments

Our quest through various frameworks for creating GUI on Cocoa Touch started out from the UIKit. Unsatisfied with its performance, we then moved on to OpenGL, which is the workhorse of virtually all of our image intensive apps.

However, since last week, I’ve been playing with Quartz and Core Animation to create a simple clock application and explore their capabilities.

Things have been going well until I tried to animate a clock hand by changing its angle. You can automatically animate a layer’s “animatable properties” like opacity, size, etc., just by changing its value. How do I go about applying this “implicit animation” feature to custom parameters like a clock hand’s angle?

After three hours of googling, I found that starting from iPhone OS 3.0, CALayer lets you specify which property its content depends on (via needsDisplayForKey:), and which action should be run when the property is changed (via actionForKey:). However, there was one more thing that needed to be taken care of, which I will explain later.

So lets get to the code. Code is worth a thousand words, I believe.

First off, creating the clock hand layer:

@implementation ClockView
 
- (void)awakeFromNib {
    [self setupLayers];
    [self start];
}
 
- (void)start {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 
        target:self
        selector:@selector(tick)
        userInfo:nil
        repeats:YES];
    self.animationTimer = timer;
}
 
- (void)setupLayers {
    /*
     Add layers for drawing each hand.
     Animation is handled by animating the angle key.
     */
    {
        HandLayer *hand = [[HandLayer alloc] init];
        hand.frame = self.frame;
        self.secondHand = hand;
        [hand release];
        [self.layer addSublayer:self.secondHand];
    }
    // ...
}
 
- (void) tick {
    // Tell each clock hand that its angle has changed.
}

That wasn’t particularly interesting. Here comes the climax of this post; the animation:

@interface HandLayer : CALayer {
}
 
@property CGFloat angle;
 
@end
 
 
@implementation HandLayer
 
@dynamic angle;
 
+ (BOOL)needsDisplayForKey:(NSString*)aKey {
    // Changes in angle require redisplay
    if ([aKey isEqualToString:@"angle"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:aKey];
    }
}
 
- (id)actionForKey:(NSString *) aKey {
    if ([aKey isEqualToString:@"angle"]) {
        // Create a basic interpolation for "angle" animation
        CABasicAnimation *theAnimation = [CABasicAnimation
            animationWithKeyPath:aKey];
        // Attention: the animation needs to know the fromValue
        theAnimation.fromValue = [[self presentationLayer] valueForKey:aKey];
        return theAnimation;
    } else {
        return [super actionForKey:aKey];
    }
}
 
- (void)drawInContext:(CGContextRef)context {
    // Draw a hand using the angle property
}

Note the line that sets theAnimation.fromValue to the current angle of hand in display. It’s something not in the textbook. The reason I was able to figure it out is thanks to this Omni Group’s blog post on “Animating CALayer content”, which is from a time when you had to “whip up [...] a superclass” and implement your own mechanism of needsDisplayForKey:.

Finally, a video!

Note: The timing function of the animation used in the video is kCAMediaTimingFunctionEaseIn, not the default linear one.

References:

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • StumbleUpon
  • Twitter
Categories: dev Tags:

Asynchronous unit testing with GHUnit and NSInvocation

November 22nd, 2009 ento No comments

1. Decoupling the network access code

Suppose you have an app that calls a remote API to store the user’s rating of your content:

@implementation StarService
 
- (void)star:(NSUInteger)pictureNumber count:(NSUInteger)count {
	// create the request
	NSString *url = [NSString stringWithFormat:@"http://%@:%d/api/star/", serviceHostname, servicePort, nil];
	NSMutableURLRequest *theRequest=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]
											  cachePolicy:NSURLRequestUseProtocolCachePolicy
										  timeoutInterval:60.0];
	// ... setup the API parameters and HTTP headers ...
	StarRequestDelegate *requestDelegate = [[StarRequestDelegate alloc] initWithService:self delegate:serviceDelegate];
	/* Starting API call! */
	NSURLConnection *theConnection = [NSURLConnection connectionWithRequest:aRequest delegate:requestDelegate];
}
...
// NSURLConnection delegate for the star API call
@implementation StarRequestDelegate
 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
	if (delegate && [(NSObject*)delegate respondsToSelector:@selector(starService:didFinishStar:)]) {
		/* API call returned normally, invoke the delegate's callback */
		[delegate starService:service didFinishStar:receivedData];
	}
	[super connectionDidFinishLoading:connection];
}

The first thing you want to do is to implement a fake version of the calling class to slice out the network dependency. Then you can use the fake service to develop other parts of the app without worrying about setting up the API server each time you do a test run.

@implementation FakeStarService
 
- (void)star:(NSUInteger)pictureNumber count:(NSUInteger)count {
	/* invocation to call the delegate method directly */
	NSInvocation *invocation;
	[[NSInvocation retainedInvocationWithTarget:serviceDelegate invocationOut:&invocation]
	 starService:self didFinishStar:nil];
 
	/* wrap it around with another invocation to simulate network delay */
	NSInvocation *delayInvocation;
	[[NSInvocation retainedInvocationWithTarget:invocation invocationOut:&delayInvocation]
	 performSelector:@selector(invoke) withObject:nil afterDelay:delay];
	[delayInvocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
}

Here I’m creating two NSInvocations to simulate a delayed API call. Wait, there must be more to creating an NSInvocation? You’re right. This code wouldn’t be possible without the excellent ForwardedConstruction extension for NSInvocation.

To use the extension on iPhone, download the code from the original article and add/modify the following parts:

--- Downloads/NSInvocationForwardedConstruction/NSInvocation(ForwardedConstruction).h	2009-05-04 11:55:34.000000000 +0900
+++ NSInvocation(ForwardedConstruction).h	2009-12-02 10:17:07.000000000 +0900
@@ -11,7 +11,7 @@
 //  appreciated but not required.
 //
 
-#import <Cocoa/Cocoa.h>
+#import <UIKit/UIKit.h>
 
 @interface NSInvocation (ForwardedConstruction)
 
@@ -21,3 +21,10 @@
 	invocationOut:(NSInvocation **)invocationOut;
 
 @end
+
+#if (TARGET_OS_IPHONE)
+@interface NSObject (ForwardedConstruction)
+- (NSString *)className;
++ (NSString *)className;
+@end
+#endif
--- Downloads/NSInvocationForwardedConstruction/NSInvocation+ForwardedConstruction.m	2009-05-04 11:55:34.000000000 +0900
+++ NSInvocation(ForwardedConstruction).m	2009-12-02 10:17:43.000000000 +0900
@@ -12,7 +12,9 @@
 //
 
 #import "NSInvocation(ForwardedConstruction).h"
-#import <objc/objc-runtime.h>
+//#import <objc/objc-runtime.h>
+#import <objc/runtime.h>
+#import <objc/message.h>
 
 //
 // InvocationProxy is a private class for receiving invocations via the
@@ -376,4 +378,21 @@
 	return invocationProxy;
 }
 
+@end 
+
+#if (TARGET_OS_IPHONE)
+
+@implementation NSObject (ForwardedConstruction)
+
+- (NSString *)className
+{
+	return [NSString stringWithUTF8String:class_getName([self class])];
+}
++ (NSString *)className
+{
+	return [NSString stringWithUTF8String:class_getName(self)];
+}
+
 @end
+
+#endif

2. Choosing the right thread

GHUnit is a test framework for Objective-C (Mac OS X 10.5 and iPhone 2.x/3.x) with a pretty GUI test runner. It has the ability to run itself on a separate thread, which comes in handy when dealing with NSURLConnection related tests.

Why? Because for some internal working of NSURLConnection, invoking it from the main thread seems to be the most hassle-free way of using. By having the test framework running on a separate thread, we can keep the network related code on the main thread while enjoying a smooth testing UI.

So here is the working setup I have:

// test cases for the real service class.
// there's another similar class for testing the fake service.
@implementation HttpNetTest
 
- (BOOL)shouldRunOnMainThread {
	/* Tell GHUnit to run on a separate thread */
	return NO;
}
 
- (void)test_send_star {
	[tester do_test_send_star:service];
}
 
@implementation StarServiceTests
 
- (void)do_test_send_star:(id)service {
	// setup an invocation and
	NSInvocation *invocation;
	[[NSInvocation retainedInvocationWithTarget:service invocationOut:&invocation]
	  star:0 count:1];
	/* invoke it on the main thread */
	[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:NO];
 
	/* wait */
	BOOL notTimeout = [AsyncTestHelper wait:service.delegate property:@selector(receivedDidFinishGetStarsCount) atLeast:1];
 
	/* asserts */
	GHAssertTrue(notTimeout, @"Should not timeout");
	GHAssertEquals((NSUInteger)1, [service.delegate receivedDidFinishStarCount], @"delegate should receive star callback");		
}

3. Waiting for the API call to end

The final snippet is a little helper for testing asynchronous operation. You might have noticed the line using it in the test case:

@implementation StarServiceTests
 
- (void)do_test_send_star:(id)service {
	// .. 
	[AsyncTestHelper wait:service.delegate property:@selector(receivedDidFinishStarCount) atLeast:1];
	// ..
}

Basically the method just loops until the specified property value is equal or more than a certain threshold:

@implementation AsyncTestHelper
 
+ (BOOL)wait:(id)target property:(SEL)getter atLeast:(NSUInteger)count {
	int tried = 0;
	while((NSUInteger)[target performSelector:getter]  10) {
			return FALSE;
		}
		[NSThread sleepForTimeInterval:0.5];
	}
	return TRUE;
}

Now you can get the pleasure of full green tests with iPhone network programming too.

ghunit_test_runner

Finally, here’s a class diagram of all the classes mentioned above, produced with a little help from Xcode’s Core Data modeling interface.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • StumbleUpon
  • Twitter
Categories: dev Tags:

Hello world!

July 7th, 2009 admin 1 comment

Welcome to Moonshine Project.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • StumbleUpon
  • Twitter
Categories: Uncategorized Tags: