Archives for category: Images

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

Problem:

The Twitter iPhone app pioneered the ability to swipe on a tweet and have a menu appear, letting you do things like reply or favorite the tweet.

Tweets in the Twitter app are table view cells in a table view. How do we recreate this feature and add the ability to side swipe on table view cells?

Solution:

This feature has two distinct parts. The first is detecting that the user swiped on the table. The second is animating in and animating out the menu view.

Detecting swipes in iOS 4

iOS 4 introduced Gesture Recognizers which make gestures like swiping, tapping and pinching very east to detect. Specifically we can create a couple of UISwipeGestureRecognizer objects, one for the right direction and another for the left detection and attach them to the table view:

// Setup a right swipe gesture recognizer
UISwipeGestureRecognizer* rightSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)] autorelease];
rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
[tableView addGestureRecognizer:rightSwipeGestureRecognizer];

// Setup a left swipe gesture recognizer
UISwipeGestureRecognizer* leftSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)] autorelease];
leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[tableView addGestureRecognizer:leftSwipeGestureRecognizer];

Now when the user swipes left or right anywhere in the table, our swipeRight: or swipeLeft: methods will get called. The touch handling code that tracks the user’s finger and figures out their intent is blissfully handled for us.

Detecting swipes in iOS 3

When this feature was introduced in what was then the Tweetie app, it worked in iOS 3 well before iOS 4 was released. You might smartly argue that if iOS 3 currently makes up 1-2% of users out there it isn’t worth developing for and I admit this is a valid point. Still it’s an interesting technical mystery and that’s just the kind of thing we love solving!

You might have thought like I did, that the Twitter app must have implemented its own touch handling, guessing based on the location of your finger whether you were trying to do a swipe, but this is wrong.

In the Twitter app it doesn’t matter if you swipe left or swipe right, the animation of the menu always happens from left to right. This is the same behavior as the editing of table view cells and it turns out this is how the Twitter app does it: It hijacks the built in swipe to delete feature of table view cells. There are 3 parts to making this work:

1. Enabling swipe to delete

Per Apple’s documentation:

To enable the swipe-to-delete feature of table views (wherein a user swipes horizontally across a row to display a Delete button), you must implement the tableView:commitEditingStyle:forRowAtIndexPath: method

So the first step is to implement the tableView:commitEditingStyle:forRowAtIndexPath: method.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
}

The method doesn’t have to do anything. Once it is implemented, you’ll be able to side swipe on a cell and the Delete button will appear.

2. Disabling the Delete button

Apple’s Inserting and Deleting Rows and Sections documentation indicates that when you explicitly put a table in editing mode by calling setEditing:animated:, the same message is then sent to each of the visible cells.

The documentation for a table view cell’s setEditing:animated: indicates that when this method is called, insertion/deletion control are animated in.

So disabling the Delete button turns out to be relatively simple: Override the table view cell’s setEditing:animated: and don’t call the superclass’s implementation.

- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
  // We suppress the Delete button by explicitly not calling
  // super's implementation
  if (supressDeleteButton)
  {
    // Reset the editing state of the table back to NO
    UITableView* tableView = [self getTableView:self];
    tableView.editing = NO;
  }
  else
    [super setEditing:editing animated:animated];
}

3. Getting notified when a swipe occurred

Apple’s docs for tableView:willBeginEditingRowAtIndexPath: are crystal clear: This method is called when the user swipes horizontally across a row.

The implementation of this method in iOS 3 is a parallel to the swipeLeft: and swipeRight: methods we registered with UISwipeGestureRecognizers under iOS 4. When any of these methods are called, we know that a swipe happened and we are ready to animate in the menu.

Animating in the menu view

Before we animate in the menu, we first add it as a subview of the table view.

As you can see in the image at the top of this post, we are animating the existing cell content offscreen while simultaneously animating in the menu. Here is a rough illustration of how both the cell content and the menu have to animate in sync during a left to right animation:

So we first set the frame of the menu, placing it offscreen. Depending of the direction, we’d put it offscreen on the right or left side of the table. Next we’d start an animation block and set the frame of the menu to be at 0 x-offset. Inside the same animation block we also set the cell’s frame to be offscreen on the other side of the table.

- (void) addSwipeViewTo:(UITableViewCell*)cell direction:(UISwipeGestureRecognizerDirection)direction
{
  // Change the frame of the side swipe view to match the cell
  sideSwipeView.frame = cell.frame;

  // Add the side swipe view to the table
  [tableView addSubview:sideSwipeView];
  
  // Remember which cell the side swipe view is displayed on and the swipe direction
  self.sideSwipeCell = cell;
  sideSwipeDirection = direction;

  // Move the side swipe view offscreen either to the left or the right depending on the swipe direction
  CGRect cellFrame = cell.frame;
  sideSwipeView.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? -cellFrame.size.width : cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);

  // Animate in the side swipe view
  animatingSideSwipe = YES;
  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:0.2];
  [UIView setAnimationDelegate:self];
  [UIView setAnimationDidStopSelector:@selector(animationDidStopAddingSwipeView:finished:context:)];
  // Move the side swipe view to offset 0
  sideSwipeView.frame = CGRectMake(0, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
  // While simultaneously moving the cell's frame offscreen
  // The net effect is that the side swipe view is pushing the cell offscreen
  cell.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? cellFrame.size.width : -cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
  [UIView commitAnimations];
}

Animating out the menu view

When the menu is animated away, there is a little bounce of the cell content as it comes back into view.

Here is another rough illustration showing the three animations that make up a bounce, showing at each step where the cell content and menu are:

There are multiple ways we can achieve this animation. One is CAKeyframeAnimation where you specify a path and the animation follows that path.

Instead the code simply chains together the 3 separate animations. Since we might care about iOS 3, we don’t use animation blocks, but instead use begin/commit animation methods and register an animation stop selector where we start the next animation.

Just like we did when animating the menu in, at each step we animate both the menu as well as the cell to give the illusion that the cell content is pushing the menu out of view.

UPDATE: It was pointed out on Hacker News that the Twitter app actually puts the menu behind the cell and then only animates the cell content in and out. The menu isn’t animated at all. I’ve updated the code so that by default it now does this style of animation. If you really liked the pushing behavior where both the menu and cell content are animated, there is a PUSH_STYLE_ANIMATION #define that you can set to YES to get it back.

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

Tweet This!Hacker NewsShare on Facebook

Advertisements

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

Problem:

Apps like Instagram, Reeder and DailyBooth have a UINavigationBar with custom background and sometimes custom back buttons. How do we build a similar custom UINavigationBar?

Solution:

In our recipe for Recreating the iBooks wood themed navigation bar we added a wood  image as a subview of UINavigationBar. This worked but it turns out that we were lucky.

If we had tried to push another view controller onto our UINavigationController we would’ve ended up with a UINavigationBar that has nothing but our wood image. This is because as the standard UINavigationBar implementation adds items, it also then sends them to the back of its view hierarchy. So even if we add our custom image to the bottom of the UINavigationBar view hierarchy, the rightBarButtonItem, titleView and leftBarButtonItem end up below the custom image and out of sight.

The nav bar background

We need a new, more flexible solution: Instead of adding to a UINavigationBar’s view hierarchy we will subclass UINavigationBar. We’ll override UINavigationBar’s drawRect and draw our custom background image directly:

// If we have a custom background image, then draw it, othwerwise call super and draw the standard nav bar
- (void)drawRect:(CGRect)rect
{
  if (navigationBarBackgroundImage)
    [navigationBarBackgroundImage.image drawInRect:rect];
  else
    [super drawRect:rect];
}

Any time navigationBarBackgroundImage changes we also need to call [self setNeedsDisplay] so the new background image is properly drawn.

A couple of methods allow us to set or clear the background image:

// Save the background image and call setNeedsDisplay to force a redraw
-(void) setBackgroundWith:(UIImage*)backgroundImage
{
  self.navigationBarBackgroundImage = [[[UIImageView alloc] initWithFrame:self.frame] autorelease];
  navigationBarBackgroundImage.image = backgroundImage;
  [self setNeedsDisplay];
}

// clear the background image and call setNeedsDisplay to force a redraw
-(void) clearBackground
{
  self.navigationBarBackgroundImage = nil;
  [self setNeedsDisplay];
}

Multiple nav bar backgrounds

In most cases, you’ll have a single custom background image that you’ll set once.

If you need to change the custom background more than once, say every time the user enters a new section, then simply call setBackgroundWith every time you want the nav bar’s background to change.

The source code for this recipe is one app with a single UINavigationController and a table row for each of the apps: InstagramReeder and DailyBooth. When you select a row, we push a view controller that calls setBackgroundWith to set the background image for each app.

Custom back button

In the iBooks recipe we created a custom UIBarButtonItem by creating a button with a stretchable image and adding the button as the customView of UIBarButtonItem.

We do the same thing here except we use an image has an arrow on the left side and we adjust the button’s titleEdgeInsets to center the text properly.

The backButtonWith:highlight:leftCapWidth: convenience method on our CustomNavigationBar class creates a back button which we can then add as the leftBarButtonItem. This replaces the built in back button.

To resize our custom back button to match width of the text, we use UILabel’s sizeWithFont to measure the width of the text and resize the button appropriately.

Trying it out

Just like the standard back bar button implementation, the CustomNavigationBar will set the title of the back button to the title of the previous controller on the UINavigationController stack.

The source code for this recipe also lets you dynamically change the back button text after a view controller is pushed onto the UINavigationController stack. The back button resizes as needed and you get to see and experiment with different text and button widths.

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

Tweet This!Hacker NewsShare on Facebook

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

Problem:

The Twitter iPhone App has a custom tab bar that is shorter than the standard tab bar, doesn’t have titles for the tab bar items and has a blue glow indicating when a section has new content. We want to recreate this custom tab bar.

Solution:

Just like segmented controls, the best way to customize the tab bar is to build it from scratch. In fact we’re going to start by using a recipe similar to what we used for custom segment controls:

  • Create a button for every tab bar item.
  • Manage the touches on the buttons so when one is selected, the others are deselected.

But how do we recreate the look of the buttons and how about that nice background for the tab bar?

The tab bar background

Looking at the images of the Twitter app, we find the TabBarGradient.png image which is 22px, exactly half the 44px height of this custom tab bar.

Taking a screenshot of the Twitter app and looking at it carefully reveals how the background is built:

  • The top half is a stretchable image of TabBarGradient.png
  • The bottom half is simply solid black

The custom tab bar asks its delegate for the background image and here is how we build it:

// Get the image that will form the top of the background
UIImage* topImage = [UIImage imageNamed:@"TabBarGradient.png"];

// Create a new image context
UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, topImage.size.height*2), NO, 0.0);

// Create a stretchable image for the top of the background and draw it
UIImage* stretchedTopImage = [topImage stretchableImageWithLeftCapWidth:0 topCapHeight:0];
[stretchedTopImage drawInRect:CGRectMake(0, 0, width, topImage.size.height)];

// Draw a solid black color for the bottom of the background
[[UIColor blackColor] set];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, topImage.size.height, width, topImage.size.height));

// Generate a new image
UIImage* resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return resultImage;

The buttons

The buttons have the following visual states:

  • Drawn in gray when unselected
  • Drawn with blue gradient when selected
  • Embossed border is drawn around the selected item

A button has both an image and a background image and they can both be set for the various control states. When a button is selected, the blue gradient appears to be on top and the embossed border is behind it. So here is how we’ll setup the button:

  • The button’s image for the normal state will be gray
  • The button’s image for the selected/highlighted state will be blue
  • The button’s background image for the selected/highlighted state will be the embossed border

The images for the tab bar items

A standard UITabBar only uses the alpha values of the tab bar item images. It doesn’t matter what color the images are, they will always appear in gray and blue. For our custom tab bar to be truly reusable, it will need to do the same thing.

But how exactly do we do this? It takes several steps:

  1. First we take the image and use CGContextClipToMask to generate a new image that has a white background and black content:
  2. Next we take this black and white image and use CGImageMaskCreate to create an image mask.
  3. Finally we combine the image mask with a background color.

For every tab bar item we generate two images: one with a solid gray background and another with a blue gradient background.

The blue glow

The blue glow is an image that is simply added to each button as a subview. In the Twitter app, a tab bar item will get a blue glow after the app has downloaded new content. It is a visual cue that there is more content in that section.

Our custom tab bar asks its delegate for the glowImage and it exposes a couple of methods to manage the glow: glowItemAtIndex and removeGlowAtIndex.

The current tab bar indicator

When a tab bar item is selected, a triangle at the top of the tab bar animates into place. We covered this animation in an earlier post. We use the code from that post to get the same animation for the custom tab bar.

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

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/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