Full Source code: https://github.com/boctor/idev-recipes/tree/master/TabBarAnimation

Problem:

The Twitter iPhone App has a small arrow indicator above the tab bar that animates when a tab is selected. We want to recreate this animation.





Solution:

The arrow is simply another image added on top of the tab bar which is animated every time the tab selection changes. So we have two tasks:

  • Add the arrow on top of the tab bar. This is similar to what we did in our last recipe.
  • Animate the arrow when a new tab is selected.

To add arrow on top of the tab, we have to figure out the proper horizontal and vertical locations.

Vertical Location

The vertical location is always the same so we’ll figure it out just once. To calculate the vertical location, we start at the bottom of the window, go up by height of the tab bar, go up again by the height of arrow and then come back down 2 pixels so the arrow is slightly on top of the tab bar:

CGFloat verticalLocation = self.window.frame.size.height - tabBarController.tabBar.frame.size.height - tabBarArrowImage.size.height + 2;

Horizontal Location

The horizontal location will change depending on which tab bar is currently selected. So we’ll write a method that given a tab index figures out the horizontal location.
There is nothing too complicated here: We divide the width of the tab bar by the number of items to calculate the width of a single item. We then multiply the index by the width of single item and add half the width of an item so the arrow lands in the middle:

- (CGFloat) horizontalLocationFor:(NSUInteger)tabIndex
{
  // A single tab item's width is the entire width of the tab bar divided by number of items
  CGFloat tabItemWidth = tabBarController.tabBar.frame.size.width / tabBarController.tabBar.items.count;
  // A half width is tabItemWidth divided by 2 minus half the width of the arrow
  CGFloat halfTabItemWidth = (tabItemWidth / 2.0) - (tabBarArrow.frame.size.width / 2.0);

  // The horizontal location is the index times the width plus a half width
  return (tabIndex * tabItemWidth) + halfTabItemWidth;
}

Add the arrow on top of the tab bar

On app startup we add the arrow on top of the selected tab. Our sample app doesn’t remember which tab you had selected before you quit, so we always start at index 0 ([self horizontalLocationFor:0]):

- (void) addTabBarArrow
{
  UIImage* tabBarArrowImage = [UIImage imageNamed:@"TabBarNipple.png"];
  self.tabBarArrow = [[[UIImageView alloc] initWithImage:tabBarArrowImage] autorelease];
  // To get the vertical location we start at the bottom of the window, go up by height of the tab bar, go up again by the height of arrow and then come back down 2 pixels so the arrow is slightly on top of the tab bar.
  CGFloat verticalLocation = self.window.frame.size.height - tabBarController.tabBar.frame.size.height - tabBarArrowImage.size.height + 2;
  tabBarArrow.frame = CGRectMake([self horizontalLocationFor:0], verticalLocation, tabBarArrowImage.size.width, tabBarArrowImage.size.height);

  [self.window addSubview:tabBarArrow];
}

Animate the arrow when a new tab is selected

A UITabBarController delegate gets notified every time a view controller was selected. We’ll use this as the trigger for starting the animation.

The actual animation is very simple. We use animation blocks available on every view.

If you haven’t used animation blocks before, here is a simple description:

  • Before you start the animation block set the frame of the item you want to animate to the start location.
  • Inside the animation block set the frame of the item you want to animate to the end location.

That’s all you have to do. The OS figures out the intermediate frames and does the actual animation for you. Doesn’t get simpler than that.

The arrow is already at the location we want it to animate from, so we don’t have to do anything before we start the animation block.

Inside the animation block block, all we have to do is set the final location of the arrow. So we take the existing frame of the arrow and change its horizontal location based on the newly selected tab index:

- (void)tabBarController:(UITabBarController *)theTabBarController didSelectViewController:(UIViewController *)viewController
{
  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:0.2];
  CGRect frame = tabBarArrow.frame;
  frame.origin.x = [self horizontalLocationFor:tabBarController.selectedIndex];
  tabBarArrow.frame = frame;
  [UIView commitAnimations];
}

There a couple of things you can play around with to customize the animation:

  • The value you pass to setAnimationDuration will speed up or slow down the animation.
  • You can also set an animation curve. The default curve is UIViewAnimationCurveEaseInOut which causes the animation to start slowly, get faster in the middle and then slow before the animation is complete. Other curves like UIViewAnimationCurveEaseIn cause the animation to start slowly and then get faster until completion.

Full Source code: https://github.com/boctor/idev-recipes/tree/master/TabBarAnimation

Tweet This!Hacker NewsShare on Facebook

Advertisements

Full Source code: https://github.com/boctor/idev-recipes/tree/master/RaisedCenterTabBar

Problem:

Apps like Instagram, DailyBooth and Path™ have what looks like a standard UITabBarController, but the center tab bar is raised or colored. How do we recreate this look?


Solution:

These tab bars look pretty standard with the exception of the center item, so we’ll start out with a standard UITabBarController which contains a UITabBar.

Looking at the images inside each app, it is quickly apparent that the middle tab bar is simply a custom UIButton.

A UITabBar contains an array of UITabBarItems, which inherit from UIBarItem. But unlike UIBarButtonItem that also inherits from UIBarItem, there is no API to create a UITabBarItem with a customView.

So instead of trying to create a custom UITabBarItem, we’ll just create a regular one and then put the custom UIButton on top of the UITabBar.

Our basic recipe is then to create a subclass of UITabBarController and add a custom UIButton on top of the UITabBar.

If the button is the same height as the UITabBar, then we set the center of the button to the center of the UITabBar. If the button is slightly higher, then we do the the same thing except we adjust the center’s y value to account for the difference in height.

UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0.0, 0.0, buttonImage.size.width, buttonImage.size.height);
[button setBackgroundImage:buttonImage forState:UIControlStateNormal];
[button setBackgroundImage:highlightImage forState:UIControlStateHighlighted];

CGFloat heightDifference = buttonImage.size.height - self.tabBar.frame.size.height;
if (heightDifference < 0)
  button.center = self.tabBar.center;
else
{
  CGPoint center = self.tabBar.center;
  center.y = center.y - heightDifference/2.0;
  button.center = center;
}

[self.view addSubview:button];

You might notice that in the code we don’t add the button as a subview in viewDidLoad. This is because we have our UITabBarControllers within a UINavigationController. When viewDidLoad is called, our UITabBarController’s view is the entire height of the screen. If we add the button as a subview in viewDidLoad we’d have to manually account for the navigation bar or properly setup the button’s autoresizingMask both of which complicate the code. But after the UITabBarController is pushed onto the UINavigationController stack, the UITabBarController’s view is auto resized to account for the navigation bar. So we delay adding the button as a subview until the UITabBarController’s has been pushed onto the UINavigationController stack. We do this by registering for the navigationController’s willShowViewController callback.

Full Source code: https://github.com/boctor/idev-recipes/tree/master/RaisedCenterTabBar

Tweet This!Hacker NewsShare on Facebook

Full Source code: https://github.com/boctor/idev-recipes/tree/master/WoodUINavigation

Problem:

Recreate the wood themed navigation bar of Apple’s iBooks app.

Solution:

First we’ll extract the images from the iBooks app and find the main navigation bar image:

Next we’ll add this wood background image as a subview of the UINavigationBar. We add it at the bottom of the z-order so that the buttons are drawn on top:

    UIImageView* imageView = [[[UIImageView alloc] initWithFrame:navigationController.navigationBar.frame] autorelease];
    imageView.contentMode = UIViewContentModeLeft;
    imageView.image = [UIImage imageNamed:@"NavBar-iPhone.png"];
    [navigationController.navigationBar insertSubview:imageView atIndex:0];

Then we replace those standard UIBarButtonItems with custom ones. To do this we create custom buttons with stretchable images we find in the iBooks app:

and add the custom buttons as the customView of the UIBarButtonItems:

  self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:[self woodButtonWithText:@"Store" stretch:CapLeftAndRight]] autorelease];
  self.navigationItem.leftBarButtonItem =  [[[UIBarButtonItem alloc] initWithCustomView:[self woodButtonWithText:@"Edit" stretch:CapLeftAndRight]] autorelease];

Finally, we add a custom segmented control as the titleView:

  segmentControlTitles = [[NSArray arrayWithObjects:@"Books", @"PDFs", nil] retain];
  UIImage* dividerImage = [UIImage imageNamed:@"view-control-divider.png"];
  self.navigationItem.titleView = [[[CustomSegmentedControl alloc] initWithSegmentCount:segmentControlTitles.count segmentsize:CGSizeMake(BUTTON_SEGMENT_WIDTH, dividerImage.size.height) dividerImage:dividerImage tag:0 delegate:self] autorelease];

Our end result is nearly indistinguishable from the original:

Full Source code: https://github.com/boctor/idev-recipes/tree/master/WoodUINavigation

Tweet This!Hacker NewsShare on Facebook

Full Source code: https://github.com/boctor/idev-recipes/blob/master/CustomSegmentedControls/Classes/CustomSegmentedControlsViewController.m

Problem:

We have an image that we need to crop. Specifically we have a stretchable image but we want to control which cap is visible on the image.

We ran into this when creating custom segmented controls. All we had was a stretchable image with rounded corner caps on both sides and a stretchable 1 pixel in the middle.

But the buttons for a segmented control have either only the left or right cap showing for the end buttons and for the middle button, neither cap is showing.

Solution:

We will use a graphics context to do the image cropping. If you haven’t used contexts before, Apple’s Quartz 2D Overview has a nice description.

To demonstrate, let’s use a stretchable image (‘image’ variable) with 14px caps (‘capWidth’ variable) and let’s generate images 150px wide (‘buttonWidth’ variable).

In all cases we create an image context that is 150px wide and as high as the image:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(buttonWidth, image.size.height), NO, 0.0);

Remember that you can tell stretchable images to draw at any width and they will stretch to accomodate this width.

Left Cap Only

To draw only the left cap, we’ll tell the image to draw at (0,0), but we’ll expand the width enough so the right cap is drawn beyond the bounds of the context.

[image drawInRect:CGRectMake(0, 0, buttonWidth + capWidth, image.size.height)];

We then ask the context to draw itself into an image:

UIImage* resultImage = UIGraphicsGetImageFromCurrentImageContext();

Resulting in this image:

Right Cap Only

To draw only the right cap, we’ll tell the image to draw at (-14,0), and we’ll also expand the width enough so the left cap is drawn beyond the bounds of the context.

[image drawInRect:CGRectMake(0.0-capWidth, 0, buttonWidth + capWidth, image.size.height)];

Resulting in this image:

No Caps

To draw no caps, we’ll tell the image to draw at (-14,0), and we’ll also expand the width enough so that both caps are drawn beyond the bounds of the context.

[image drawInRect:CGRectMake(0.0-capWidth, 0, buttonWidth + (capWidth * 2), image.size.height)]

Resulting in this image:

You can see the full source in the custom segmented controls source: https://github.com/boctor/idev-recipes/blob/master/CustomSegmentedControls/Classes/CustomSegmentedControlsViewController.m

Full Source code: https://github.com/boctor/idev-recipes/tree/master/CustomSegmentedControls

Problem:

UISegmentedControls have only four styles, each with preset heights and colors that you can’t change. How do you create a custom segmented control?

Solution:

The Apple docs say:

A UISegmentedControl object is a horizontal control made of multiple segments, each segment functioning as a discrete button

In other words a UISegmentedControl is simply a group of buttons. To create a custom segmented control we can use a simple recipe:

  • Create a custom view that manages a button for each segment.
  • Use a divider image to visually separate segments.
  • Manage the touches on the buttons so when one is selected, the others are deselected.

We need to also figure out the proper appearance of each button but there are at least two ways to make the images for the buttons:

Instead of having the implementation dictate one way, we’ll use a delegate callback so each instance of the custom control can decide how to build the buttons. The delegate also has optional callbacks to get notified when a touch up or touch down occurs on one of the segments. This will be were we takes actions like swapping out views when a user selects a segment.

CustomSegmentedControl.h

@protocol CustomSegmentedControlDelegate

- (UIButton*) buttonFor:(CustomSegmentedControl*)segmentedControl atIndex:(NSUInteger)segmentIndex;

@optional
- (void) touchUpInsideSegmentIndex:(NSUInteger)segmentIndex;
- (void) touchDownAtSegmentIndex:(NSUInteger)segmentIndex;
@end

@interface CustomSegmentedControl : UIView
{
NSObject <CustomSegmentedControlDelegate> *delegate;
NSMutableArray* buttons;
}

@property (nonatomic, retain) NSMutableArray* buttons;

- (id) initWithSegmentCount:(NSUInteger)segmentCount segmentsize:(CGSize)segmentsize dividerImage:(UIImage*)dividerImage tag:(NSInteger)objectTag delegate:(NSObject <CustomSegmentedControlDelegate>*)customSegmentedControlDelegate;

You can see the full source for CustomSegmentedControl.m as well as a sample app showing the custom control in action: https://github.com/boctor/idev-recipes/tree/master/CustomSegmentedControls

Full Source code: https://github.com/boctor/idev-recipes/tree/master/StretchableImages

Problem:

Images for your iOS apps are too big. You’re creating variations of the same image that only differ in width.

Solution:

A great way to reduce the size of images and reuse images is to use stretchable images.

Smaller image sizes reduce the app size and users will have to wait less for the app to download from the AppStore.

Smaller images also reduce your app’s memory footprint:

Make resource files as small as possible.
Files reside on the disk but must be loaded into memory before they can be used; compress all image files to make them as small as possible.

A stretchable image has 3 parts: A left cap, a one pixel stretchable area and a right cap.
Keith Peters over at Bit-101 has a great image showing this in action:

These images when scaled or resized will draw both caps on either side and repeat the middle pixel.

The most common ways to use stretchable images are:

An entire image stretched by using a 1 pixel wide source image

For example this simple 1 pixel wide image:

UIImage* image = [[UIImage imageNamed:@"1-pixel-image.png"] stretchableImageWithLeftCapWidth:0.0 topCapHeight:0.0]

If we then use this image in a 300 pixel wide image view:

UIImageView* imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
imageView.frame = CGRectMake(0, 0, 300.0, image.size.height);

we get this image:

An image stretched with equal right and left caps

The source image needs to contain both caps with an extra pixel in the middle. So for example this image is 11 pixels wide, 5 pixels for each cap and a 1 pixel stretchable area in the middle:

UIImage* buttonImage =[[UIImage imageNamed:@"button.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0]

results in this image when used in a 300 pixel wide image view:

UIImageView* imageView = [[UIImageView alloc] initWithImage:buttonImage];
imageView.frame = CGRectMake(0, 0, 300.0, buttonImage.size.height);

If we create two images then we can set the background image of a button for the normal and highlighted states and get some very nice looking buttons using very small images.

Here is some sample source code to see stretchable images and buttons in action: https://github.com/boctor/idev-recipes/tree/master/StretchableImages

To recreate features of existing apps, we can use a big clue: the images an app uses. This will often give us insight into how the feature was built.

You can right click on an App in iTunes and see the app’s .ipa file.

An .ipa file is just a zip file that is easily expanded resulting in a Payload folder that has the actual .app.

But we can’t just double click on the images and open them in something like Preview.app.

During app compilation Xcode optimizes all images so they aren’t readable by standard tools like Preview.app.

We need to undo the optimization and restore the images back to their original form.

The tool that the SDK uses to optimize the images is pngcrush and starting with the 3.2 SDK, Apple added the ‘revert-iphone-optimizations’ option to undo this optimization.

I wrote a quick ruby script called appcrush that automates this process.

Point appcrush at an .ipa file from the iTunes AppStore and it:

  • expands the zip file
  • finds all the images
  • runs pngcrush with the revert-iphone-optimizations option on each image
appcrush '/Users/boctor/Music/iTunes/Mobile Applications/iBooks.ipa'

You can find appcrush here: https://github.com/boctor/idev-recipes/tree/master/Utilities/appcrush

Tweet This!Hacker NewsShare on Facebook

Full Source code: http://github.com/boctor/idev-recipes/tree/master/TransparentUIWebViews

Problem:

UIWebViews have a built in gradient at the top and bottom.

If you set the UIWebView’s background color to clearColor, this gradient is still visible.

How can we turn off this gradient to make the UIWebView completely transparent?

Solution:

The UIWebView API doesn’t expose this gradient, but we can be sneaky and look at it’s view hierarchy.

If we put a breakpoint in the debugger where we have access to a UIWebView then in the console’s gdb prompt, we can take a look at the view hierarchy:

(gdb) po [webView recursiveDescription]
<UIWebView: 0x68220e0; frame = (0 0; 320 460); >
| <UIScrollView: 0x4b2bee0; frame = (0 0; 320 460); >
|    | <UIImageView: 0x4b2dca0; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b2da20; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b2d9c0; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b12030; frame = (0 0; 54 54) >
|    | <UIImageView: 0x4b11fd0; frame = (-14.5 14.5; 30 1); >
|    | <UIImageView: 0x4b11f70; frame = (-14.5 14.5; 30 1); >
|    | <UIImageView: 0x4b11f10; frame = (0 0; 1 30); >
|    | <UIImageView: 0x4b11eb0; frame = (0 0; 1 30); >
|    | <UIImageView: 0x4b11e50; frame = (0 430; 320 30); >
|    | <UIImageView: 0x4b2d0c0; frame = (0 0; 320 30);  >
|    | <UIWebBrowserView: 0x6005800; frame = (0 0; 320 460); >

So a UIScrollView contains a UIBrowserView filling up the UIWebView which we can assume is the actual web view and then a bunch of UIImageViews at the top and bottom are used to show the gradients. So how do we hide them?

This hierarchy may well change in future iOS versions, so the less assumptions  we make the better. We shouldn’t assume that there is a UIScrollView with embedded UIImageViews, but we do have to make at least one assumption: That the only UIImageViews are the ones used for the gradients.

So how do we find all the UIImageViews and hide them? A view can contain any number of subviews, so we need to walk the view hierarchy and hide any UIImageViews we find. To be safe, we should do this before we load the UIWebView with any content.

- (void)viewDidLoad
{
  [super viewDidLoad];
  [webView setBackgroundColor:[UIColor clearColor]];
  [self hideGradientBackground:webView];
}

- (void) hideGradientBackground:(UIView*)theView
{
  for (UIView* subview in theView.subviews)
  {
    if ([subview isKindOfClass:[UIImageView class]])
      subview.hidden = YES;

    [self hideGradientBackground:subview];
}

Full Source code: http://github.com/boctor/idev-recipes/tree/master/TransparentUIWebViews

If you think think this is something that Apple should provide an API for, then please fill out an enhancement request at http://bugreport.apple.com