Showing posts with label custom controls. Show all posts
Showing posts with label custom controls. Show all posts

Sunday, May 22, 2011

UITabBarController with custom UITabBarItem colors


In the last few days my team and I were tasked with creating an app for a brand which has orange colors. We wanted a tab bar. We wanted the selected tab bar item to highlight in orange rather than the default aqua color. If you're reading this than you probably already know that a straightforward way to achieving this using UITabBarController doesn't exist. In this post I'd like to go through the code I've written for a custom tab bar controller that allows you to set custom image masks for your highlighted items. The project hasn't been implemented to fully replicate the functionality of UITabBarController, however the basics are in and you can add additional features if you like.


What does the project consist of?


The project consists of 3 classes as shown below.


How do you use it?


Very simple. You use it just like you would use UITabBarController with two exceptions:

  1. If you're loading your current tab bar controller from a NIB file you will need to set it up programatically.
  2. You need to set up the image masks used for selected and unselected items.

So where do you start? Download the project code here. Copy the files from the TATabBarController folder into your project and change all references of UITabBarController to TATabBarController. Once you've done that you're left with going through 4 easy steps to get things up and running.



Don't forget to set the tabBarItem property for the view controllers you're adding to the tab bar controller - otherwise nothing will appear.


How does it work?


Once you set the view controllers the magic begins to happen. Below I'll describe how I went about drawing the tab bar images and text. The rest of the details regarding the functioning of the tab bar will be left out of the scope of this post. The code that shows how all this is done is located in TATabBars - (UIImage*)_tabBarImage:(UIImage*)tabBarImage withGradient:(UIImage*)gradientImage method.



Additionally, if we are drawing the selected tab bar item we apply a shadow to the image and text.


You can download the project code here. Have fun!

Monday, May 11, 2009

Binding NSArrayControllers arrangedObjects to custom NSView

Some functionality in cocoa is taken for granted. It's sooo easy to create something that works - drag here, drop there, bind, bind, presto! Some time during your adventure with cocoa you might start wondering how some of these mechanisms work. I did :) When that moment comes, don't back down. Instead, try pursuing the idea. It's a great time to delve deeper into cocoa and objective-c and see how apple engineers solved issues you might have been implementing wierd workarounds for.


I like the way some people write about cocoa's bindings - "It's magic". It sure does seem so for some one who knows nothing about them (or knows a whole bunch and doesn't feel like explaining). The truth is bindings are nothing more than an automated form of KVO (Key-Value Observing). Binding an object to another object results in those objects being in sync. We can do the exact same thing by observing those objects and manually updating their values, but in such a case we would end up writing a lot of our own glue code.


Recently I've created a custom NSView that essentially mimicked the way NSTableView worked. I didn't want to subclass NSTableView mainly because it seems to have been designed to work well when inside an NSScrollView, not as an independent view. Seeing as how NSTableView had a content property you could bind to I created an NSMutableArray object called content in my class and made it KVO compliant. This is where the problems started. Simply binding content to my NSArrayControllers arrangedObjects array wasn't going to inform my custom view about the changes that were being made to arrangedObjects. I figured I could overcome this by observing arrangedObjects. Unfortunately, it turns out that key-value observing for collections isn't fully functional. Apples documentation for:



- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;


states that:


NSKeyValueChangeKindKey

An NSNumber object that contains a value corresponding to one of the NSKeyValueChangeKindKey enumerations, indicating what sort of change has occurred.

A value of NSKeyValueChangeSetting indicates that the observed object has received a setValue:forKey: message, or that the key-value-coding-compliant set method for the key has been invoked, or that willChangeValueForKey:/didChangeValueForKey: has otherwise been invoked.

A value of NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, or NSKeyValueChangeReplacement indicates that mutating messages have been sent to the array returned by amutableArrayValueForKey: message sent to the object, or that one of the key-value-coding-compliant array mutation methods for the key has been invoked, or thatwillChange:valuesAtIndexes:forKey:/didChange:valuesAtIndexes:forKey: has otherwise been invoked.

You can use NSNumber's intValue method to retrieve the integer value of the change kind.

Available in Mac OS X v10.3 and later.

Declared in NSKeyValueObserving.h.

NSKeyValueChangeNewKey

If the value of the NSKeyValueChangeKindKey entry is NSKeyValueChangeSetting, and NSKeyValueObservingOptionNew was specified when the observer was registered, the value of this key is the new value for the attribute.

For NSKeyValueChangeInsertion or NSKeyValueChangeReplacement, if NSKeyValueObservingOptionNew was specified when the observer was registered, the value for this key is an NSArray instance that contains the objects that have been inserted or replaced other objects, respectively.

Available in Mac OS X v10.3 and later.

Declared in NSKeyValueObserving.h.


If this were true, I wouldn't be writing this post :) If the value of NSKeyValueChangeKindKey for the change dictionary contains NSKeyValueChangeInsertion, NSKeyValueChangeRemoval or NSKeyValueChangeReplacement the value for NSKeyValueChangeNewKey will always be [NSNull null]. From what I've read, this is something Apple is aware of, but hasn't established a date for when it will be fixed (this could mean that it will never get fixed).


What this implied for my custom view is the fact that I needed to establish what the changes were myself. However, if I bind content to arrangedObjects and observe arrangedObjects the value of both will always be the same once my code in the observing method is reached. This in turn implies that I will not be able to get the changes and that bindings will not be a feasible solution for this problem. I started wondering how Apple solved this issue. What I came up with is a solution that perfectly mimics NSTableViews behaviour.


I came to terms with the fact that I won't be able to use bindings, but I really wanted the illusion of bindings to stay. I decided to overwrite the method used for binding like so:


- (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if([binding isEqualToString:@"content"] && [keyPath isEqualToString:@"arrangedObjects"] && [observableController isKindOfClass:[NSArrayController class]])
{
[observableController addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
_contentArrayController = observableController;
[self setContent: [_contentArrayController arrangedObjects]];
}
else
[super bind:binding toObject:observableController withKeyPath:keyPath options:options];
}


From the outside this still looks like the normal binding procedure is taking place, when in reality it's only observing. I also assigned my NSArrayController to a class variable in my custom view called _contentArrayController. This becomes useful in the unbind method:


- (void)unbind:(NSString*)keyPath
{
if([keyPath isEqualToString:@"content"])
{
if(_contentArrayController != nil)
{
[_contentArrayController removeObserver:self forKeyPath:@"arrangedObjects"];
_contentArrayController = nil;
}
}

[super unbind:keyPath];
}


Once all that is done you can try to observe changes in the observeValueForKeyPath method. What you will notice is that there is another issue - the only value the change dictionary will return for the NSKeyValueChangeKindKey is NSKeyValueChangeSetting. This is due to the way the arrangedObjects array is set in NSArrayController. So what we are left with is determining changes ourselves. If you need a custom view that displays a small amount of data then you might as well just set the new value for your views content based on what arrangedObjects contains. If you NEED to know what changed then I guess the reasonable way to go about it would be to compare your views current content array with the new arrangedObjects array like so:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:@"arrangedObjects"] && _contentArrayController != nil)
{
NSMutableArray *array = [_contentArrayController arrangedObjects];

if(array != nil)
{
if([array count] > [content count])
{
NSLog(@"items added");
}
else if([array count] < [content count])    {     NSLog(@"items removed");    }    else if([array count] == [content count])    {     NSLog(@"items replaces");    }   }                 //don't forget to set the content array!!!   [self setContent:array];   [self setNeedsDisplay:YES];  } } 

The problem with this is that if you assign a completely new array to NSArrayControllers content the above code won't be worth a damn. You could ofcourse go through every object in the array every time arrangedObject changes, but for large amounts of data that seems like a bad use of resources.

Anyhow, I encourage readers to look for interesting solutions to fun problems.

Monday, March 30, 2009

Drawing an NSCell from a flipped NSView to a bitmap context


Recently I found myself in need of a custom drawn list view. It's purpose would be to display 1 column and a bunch of rows, similarly to the way NSTableView does, except my view needed more flexibility in terms of the way it shapes and displays items. I thought about subclassing NSTableView, but after careful consideration I've realized that would be an overkill and went with subclassing NSView instead. To make my control efficient, I used the same approach as Apple did with NSTableView - each row is drawn by the same NSCell instance. This way I didn't have to waste memory creating a corresponding NSCell object for each item in the list views content. This was all fairly basic. I added some simple methods, did a little math and presto - custom control a 'la cocoa.


I came across a small issue when implementing drag and drop and I thought the solution might be of interest to some. My custom list views isFlipped method returns YES. This allows the NSScrollView that owns my control to work intuitively - from top to bottom (it's also much more intuitive for me). I do realize there are other ways of accomplishing this, but this just felt like it required the least hassle. All was fine in Cocoa land until I wanted to draw my NSCells to a bitmap and use it as a drag and drop image. There were two goals I wanted to accomplish:

  • Draw the NSCell into a bitmap context without changing anything in it's drawing method - (void)drawInteriorWithFrame:(NSRect)theCellFrame inView:(NSView *)theControlView
  • Draw a few cells and then arrange them into a nice image suitable for drag and drop. This made it unsuitable for me to use any of the NSViews standard methods for drawing to a bitmap context.


The cell I want to draw to the bitmap context is displayed below:


So the standard way to go about drawing to a bitmap context is:

//creating the rectangle that defines the bounds of our bitmap image
NSRect offscreenRect = NSMakeRect(0.0, 0.0, 100, 20);
NSBitmapImageRep* offscreenRep = nil;

//creating the bitmap image
offscreenRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:offscreenRect.size.width
pixelsHigh:offscreenRect.size.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:0
bytesPerRow:(4 * offscreenRect.size.width)
bitsPerPixel:32];

[NSGraphicsContext saveGraphicsState];

//setting the current context to a bitmap context
[NSGraphicsContext setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:offscreenRep]];

//do some drawing

[NSGraphicsContext restoreGraphicsState];

//creating an NSImage setting whatever we drew above to be it's representation
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


Unfortunately, this left me with:


If you compare that to the original, you will notice that the text views got switched. This was of course, unacceptable.

I started googling and found a bunch of blogs stating that using an NSAffineTransform would solve the problem. So I added:

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];


I really hoped this method would work, because it was the only solution that seemed, at the time, reasonable. It produced the image you see below:


This time the text views were in their proper places, but the text was flipped. Having wasted some time trying to find a solution I decided to give my own wacky idea a go. When using a "ported" graphics context one can set the context as flipped. I decided to create a CGContextRef, use it as a ported and flipped NSGraphicsContext and draw to it like so:

//create a CGContextRef so that we can later port it and make it flipped
CGContextRef context = CGBitmapContextCreate (bitmapData,
offscreenRect.size.width,
offscreenRect.size.height,
8,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);

[NSGraphicsContext saveGraphicsState];
//here we port the context and make it flipped
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]];

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];

//perform drawing

//create a CGImageRef from the CGContextRef
CGImageRef myImage = CGBitmapContextCreateImage (context);

//port the CGImageRef to an NSBitmapImageRep
NSBitmapImageRep* offscreenRep = [[[NSBitmapImageRep alloc] initWithCGImage:myImage] autorelease];

[NSGraphicsContext restoreGraphicsState];

//create the image and set it's representation to what was drawn above
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


As a result I got exactly what I wanted: