Blob Blame History Raw
/***************************************************************************

 Toast+UIView.h
 Toast
 Version 0.1

 Copyright (c) 2011 Charles Scalesse.

 Permission is hereby granted, free of charge, to any person obtaining a
 copy of this software and associated documentation files (the
 "Software"), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 distribute, sublicense, and/or sell copies of the Software, and to
 permit persons to whom the Software is furnished to do so, subject to
 the following conditions:

 The above copyright notice and this permission notice shall be included
 in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

 ***************************************************************************/

#import "Toast+UIView.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>

#define kMaxWidth 0.8
#define kMaxHeight 0.8

#define kHorizontalPadding 10.0
#define kVerticalPadding 10.0
#define kCornerRadius 10.0
#define kOpacity 0.8
#define kFontSize 16.0
#define kMaxTitleLines 999
#define kMaxMessageLines 999

#define kFadeDuration 0.2

#define kDefaultLength 3
#define kDefaultPosition @"bottom"

#define kImageWidth 80.0
#define kImageHeight 80.0

static NSString *kDurationKey = @"duration";

@interface UIView (ToastPrivate)

- (CGPoint)getPositionFor:(id)position toast:(UIView *)toast;
- (UIView *)makeViewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image;

@end

@implementation UIView (Toast)

#pragma mark -
#pragma mark Toast Methods

- (void)makeToast:(NSString *)message
{
	[self makeToast:message duration:kDefaultLength position:kDefaultPosition];
}

- (void)makeToast:(NSString *)message duration:(float)interval position:(id)point
{
	UIView *toast = [self makeViewForMessage:message title:nil image:nil];
	[self showToast:toast duration:interval position:point];
}

- (void)makeToast:(NSString *)message
         duration:(float)interval
         position:(id)point
            title:(NSString *)title
{
	UIView *toast = [self makeViewForMessage:message title:title image:nil];
	[self showToast:toast duration:interval position:point];
}

- (void)makeToast:(NSString *)message
         duration:(float)interval
         position:(id)point
            image:(UIImage *)image
{
	UIView *toast = [self makeViewForMessage:message title:nil image:image];
	[self showToast:toast duration:interval position:point];
}

- (void)makeToast:(NSString *)message
         duration:(float)interval
         position:(id)point
            title:(NSString *)title
            image:(UIImage *)image
{
	UIView *toast = [self makeViewForMessage:message title:title image:image];
	[self showToast:toast duration:interval position:point];
}

- (void)showToast:(UIView *)toast
{
	[self showToast:toast duration:kDefaultLength position:kDefaultPosition];
}

- (void)showToast:(UIView *)toast duration:(float)interval position:(id)point
{

	/****************************************************
	 *                                                  *
	 * Displays a view for a given duration & position. *
	 *                                                  *
	 ****************************************************/

	CGPoint toastPoint = [self getPositionFor:point toast:toast];

	// use an associative reference to associate the toast view with the display interval
	objc_setAssociatedObject(toast, &kDurationKey, [NSNumber numberWithFloat:interval],
	                         OBJC_ASSOCIATION_RETAIN);

	[toast setCenter:toastPoint];
	[toast setAlpha:0.0];
	[self addSubview:toast];

	[UIView beginAnimations:@"fade_in" context:toast];
	[UIView setAnimationDuration:kFadeDuration];
	[UIView setAnimationDelegate:self];
	[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
	[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
	[toast setAlpha:1.0];
	[UIView commitAnimations];
}

#pragma mark -
#pragma mark Animation Delegate Method

- (void)animationDidStop:(NSString *)animationID finished:(BOOL)finished context:(void *)context
{

	UIView *toast = (UIView *)context;

	// retrieve the display interval associated with the view
	float interval = [(NSNumber *)objc_getAssociatedObject(toast, &kDurationKey) floatValue];

	if ([animationID isEqualToString:@"fade_in"])
	{

		[UIView beginAnimations:@"fade_out" context:toast];
		[UIView setAnimationDelay:interval];
		[UIView setAnimationDuration:kFadeDuration];
		[UIView setAnimationDelegate:self];
		[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
		[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
		[toast setAlpha:0.0];
		[UIView commitAnimations];
	}
	else if ([animationID isEqualToString:@"fade_out"])
	{

		[toast removeFromSuperview];
	}
}

#pragma mark -
#pragma mark Private Methods

- (CGPoint)getPositionFor:(id)point toast:(UIView *)toast
{

	/*************************************************************************************
	 *                                                                                   *
	 * Converts string literals @"top", @"bottom", @"center", or any point wrapped in an *
	 * NSValue object into a CGPoint                                                     *
	 *                                                                                   *
	 *************************************************************************************/

	if ([point isKindOfClass:[NSString class]])
	{

		if ([point caseInsensitiveCompare:@"top"] == NSOrderedSame)
		{
			return CGPointMake(self.bounds.size.width / 2,
			                   (toast.frame.size.height / 2) + kVerticalPadding);
		}
		else if ([point caseInsensitiveCompare:@"bottom"] == NSOrderedSame)
		{
			return CGPointMake(self.bounds.size.width / 2,
			                   (self.bounds.size.height - (toast.frame.size.height / 2)) -
			                       kVerticalPadding);
		}
		else if ([point caseInsensitiveCompare:@"center"] == NSOrderedSame)
		{
			return CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
		}
	}
	else if ([point isKindOfClass:[NSValue class]])
	{
		return [point CGPointValue];
	}

	NSLog(@"Error: Invalid position for toast.");
	return [self getPositionFor:kDefaultPosition toast:toast];
}

- (UIView *)makeViewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image
{

	/***********************************************************************************
	 *                                                                                 *
	 * Dynamically build a toast view with any combination of message, title, & image. *
	 *                                                                                 *
	 ***********************************************************************************/

	if ((message == nil) && (title == nil) && (image == nil))
		return nil;

	UILabel *messageLabel = nil;
	UILabel *titleLabel = nil;
	UIImageView *imageView = nil;

	// create the parent view
	UIView *wrapperView = [[[UIView alloc] init] autorelease];
	[wrapperView.layer setCornerRadius:kCornerRadius];
	[wrapperView setBackgroundColor:[[UIColor blackColor] colorWithAlphaComponent:kOpacity]];
	wrapperView.autoresizingMask =
	    UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin |
	    UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;

	if (image != nil)
	{
		imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
		[imageView setContentMode:UIViewContentModeScaleAspectFit];
		[imageView
		    setFrame:CGRectMake(kHorizontalPadding, kVerticalPadding, kImageWidth, kImageHeight)];
	}

	float imageWidth, imageHeight, imageLeft;

	// the imageView frame values will be used to size & position the other views
	if (imageView != nil)
	{
		imageWidth = imageView.bounds.size.width;
		imageHeight = imageView.bounds.size.height;
		imageLeft = kHorizontalPadding;
	}
	else
	{
		imageWidth = imageHeight = imageLeft = 0;
	}

	if (title != nil)
	{
		titleLabel = [[[UILabel alloc] init] autorelease];
		[titleLabel setNumberOfLines:kMaxTitleLines];
		[titleLabel setFont:[UIFont boldSystemFontOfSize:kFontSize]];
		[titleLabel setTextAlignment:UITextAlignmentLeft];
		[titleLabel setLineBreakMode:UILineBreakModeWordWrap];
		[titleLabel setTextColor:[UIColor whiteColor]];
		[titleLabel setBackgroundColor:[UIColor clearColor]];
		[titleLabel setAlpha:1.0];
		[titleLabel setText:title];

		// size the title label according to the length of the text
		CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * kMaxWidth) - imageWidth,
		                                 self.bounds.size.height * kMaxHeight);
		CGSize expectedSizeTitle = [title sizeWithFont:titleLabel.font
		                             constrainedToSize:maxSizeTitle
		                                 lineBreakMode:titleLabel.lineBreakMode];
		[titleLabel setFrame:CGRectMake(0, 0, expectedSizeTitle.width, expectedSizeTitle.height)];
	}

	if (message != nil)
	{
		messageLabel = [[[UILabel alloc] init] autorelease];
		[messageLabel setNumberOfLines:kMaxMessageLines];
		[messageLabel setFont:[UIFont systemFontOfSize:kFontSize]];
		[messageLabel setLineBreakMode:UILineBreakModeWordWrap];
		[messageLabel setTextColor:[UIColor whiteColor]];
		[messageLabel setTextAlignment:UITextAlignmentCenter];
		[messageLabel setBackgroundColor:[UIColor clearColor]];
		[messageLabel setAlpha:1.0];
		[messageLabel setText:message];

		// size the message label according to the length of the text
		CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * kMaxWidth) - imageWidth,
		                                   self.bounds.size.height * kMaxHeight);
		CGSize expectedSizeMessage = [message sizeWithFont:messageLabel.font
		                                 constrainedToSize:maxSizeMessage
		                                     lineBreakMode:messageLabel.lineBreakMode];
		[messageLabel
		    setFrame:CGRectMake(0, 0, expectedSizeMessage.width, expectedSizeMessage.height)];
	}

	// titleLabel frame values
	float titleWidth, titleHeight, titleTop, titleLeft;

	if (titleLabel != nil)
	{
		titleWidth = titleLabel.bounds.size.width;
		titleHeight = titleLabel.bounds.size.height;
		titleTop = kVerticalPadding;
		titleLeft = imageLeft + imageWidth + kHorizontalPadding;
	}
	else
	{
		titleWidth = titleHeight = titleTop = titleLeft = 0;
	}

	// messageLabel frame values
	float messageWidth, messageHeight, messageLeft, messageTop;

	if (messageLabel != nil)
	{
		messageWidth = messageLabel.bounds.size.width;
		messageHeight = messageLabel.bounds.size.height;
		messageLeft = imageLeft + imageWidth + kHorizontalPadding;
		messageTop = titleTop + titleHeight + kVerticalPadding;
	}
	else
	{
		messageWidth = messageHeight = messageLeft = messageTop = 0;
	}

	// compare the title & message widths and use the longer value to calculate the size of the
	// wrapper width the same logic applies to the x value (left)
	float longerWidth = (messageWidth < titleWidth) ? titleWidth : messageWidth;
	float longerLeft = (messageLeft < titleLeft) ? titleLeft : messageLeft;

	// if the image width is larger than longerWidth, use the image width to calculate the wrapper
	// width. the same logic applies to the wrapper height
	float wrapperWidth =
	    ((longerLeft + longerWidth + kHorizontalPadding) < imageWidth + (kHorizontalPadding * 2))
	        ? imageWidth + (kHorizontalPadding * 2)
	        : (longerLeft + longerWidth + kHorizontalPadding);
	float wrapperHeight =
	    ((messageTop + messageHeight + kVerticalPadding) < imageHeight + (kVerticalPadding * 2))
	        ? imageHeight + (kVerticalPadding * 2)
	        : (messageTop + messageHeight + kVerticalPadding);

	[wrapperView setFrame:CGRectMake(0, 0, wrapperWidth, wrapperHeight)];

	if (titleLabel != nil)
	{
		[titleLabel setFrame:CGRectMake(titleLeft, titleTop, titleWidth, titleHeight)];
		[wrapperView addSubview:titleLabel];
	}

	if (messageLabel != nil)
	{
		[messageLabel setFrame:CGRectMake(messageLeft, messageTop, messageWidth, messageHeight)];
		[wrapperView addSubview:messageLabel];
	}

	if (imageView != nil)
	{
		[wrapperView addSubview:imageView];
	}

	return wrapperView;
}

@end