Recreate recipient bubble behaviour in Mail.app / Three20

Solution 1:

I don't like Three20 having had bad experiences trying to customise things, fix analyser warnings and make it build in Xcode 4. Will never use it again in a new project. But it will probably do what you want in this case.

However you can make your own bubbles fairly simply. I wish I could give credit to the author of the blog that I got this from, but it was two years ago and now I can't find it with Google. Basically you're picking a color, drawing the round endcaps, putting a rectangle between them and then putting the required text over the top.

UIGraphicsBeginImageContext(CGSizeMake(SIDELENGTH, SIDELENGTH));

CGContextRef context = UIGraphicsGetCurrentContext();

// the image should have a white background
[[UIColor clearColor] set];
CGRect myRect = CGRectMake(0.0f, 0.0f, SIDELENGTH, SIDELENGTH);
UIRectFill(myRect);

[self.color set];

// Drawing code
CGSize textSize = [self.text sizeWithFont:[UIFont boldSystemFontOfSize:[UIFont systemFontSize]]];

double capDiameter = textSize.height;
double capRadius = capDiameter / 2.0;
double capPadding = capDiameter / 4.0;
double textWidth = MAX( capDiameter, textSize.width ) ;

CGRect textBounds = CGRectMake(capPadding, 0.0, textWidth, textSize.height);

CGRect badgeBounds = CGRectMake(0.0, 0.0, textWidth + (2.0 * capPadding), textSize.height);

double offsetX = (CGRectGetMaxX(myRect) - CGRectGetMaxX(badgeBounds)) / 2.0;
double offsetY = (CGRectGetMaxY(myRect) - CGRectGetMaxY(badgeBounds)) / 2.0;
badgeBounds = CGRectOffset(badgeBounds, offsetX, offsetY);
textBounds = CGRectOffset(textBounds, offsetX, offsetY);

CGContextFillEllipseInRect(context, 
                           CGRectMake(badgeBounds.origin.x, badgeBounds.origin.y, capDiameter, capDiameter));

CGContextFillEllipseInRect(context, 
                           CGRectMake(badgeBounds.origin.x + badgeBounds.size.width - capDiameter, badgeBounds.origin.y, 
                                      capDiameter, capDiameter));

CGContextFillRect(context, CGRectMake(badgeBounds.origin.x + capRadius, badgeBounds.origin.y, 
                                      badgeBounds.size.width - capDiameter, capDiameter));

if(self.textColor != nil) {
    const CGFloat* colors = CGColorGetComponents(self.textColor.CGColor);
    CGColorSpaceRef space = CGColorGetColorSpace(self.textColor.CGColor);
    CGColorSpaceModel model = CGColorSpaceGetModel(space);

    if(model == kCGColorSpaceModelMonochrome)
        // monochrome color space has one grayscale value and one alpha
        CGContextSetRGBFillColor(context, *(colors + 0), *(colors + 0), *(colors + 0), *(colors + 1));
    else
        // else a R,G,B,A scheme is assumed.
        CGContextSetRGBFillColor(context, *(colors + 0), *(colors + 1), *(colors + 2), *(colors + 3));
} else 
    // else use plain white
    CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f);

[self.text drawInRect:textBounds 
 withFont:[UIFont boldSystemFontOfSize:[UIFont systemFontSize]] 
 lineBreakMode:UILineBreakModeClip alignment:UITextAlignmentCenter];

UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return image;

The delete button behaviour you describe is pretty simple too - once you have a match that is selected and accepted draw the bubble at the insertion point and change the frame of the UITextField to begin after the bubble. If you're at the beginning of the UITextField frame and you get another delete, remove the bubble UIImageView, reset the UITextField frame to where the UIImageView used to begin.

Or you could use a UIWebView as the address entry field in which you display bubble images created with the code shown along with text (see this StackOverflow question) as you edit it. You would just need to write a handler for the delegate method shouldChangeCharactersInRange to handle deleting the added address, and the bubble image, if a bubble is the item the delete action is targeting.

Solution 2:

Of course, you should just use the Message UI framework from Apple now that they released it.