Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
.DS_Store

## Obj-C/Swift specific
*.hmap
Expand Down
4 changes: 3 additions & 1 deletion Coach Marks/DDBubble.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ typedef enum {
@property (nonatomic) BOOL bouncing;
@property (nonatomic) BOOL animationShouldStop;
@property (nonatomic) UIFont *font;
@property (nonatomic) UIFont *titleFont;

-(id)initWithAttachedView:(UIView*)view title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition andColor:(UIColor*)color;
-(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition andColor:(UIColor*)color;
-(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition color:(UIColor*)color andFont:(UIFont *)font;
-(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition color:(UIColor*)color andFont:(UIFont *)font titleFont:(UIFont*)titleFont;

-(void)animate;
@end
@end
96 changes: 75 additions & 21 deletions Coach Marks/DDBubble.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ -(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)d
}

-(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition color:(UIColor*)color andFont:(UIFont *)font
{
return [self initWithFrame:frame title:title description:description arrowPosition:arrowPosition color:color andFont:font titleFont:nil];
}

-(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)description arrowPosition:(CRArrowPosition)arrowPosition color:(UIColor*)color andFont:(UIFont *)font titleFont:(UIFont*)titleFont
{
self = [super init];
if(self)
Expand All @@ -50,7 +55,12 @@ -(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)d
if (font != nil)
self.font = font;
else
self.font = [UIFont fontWithName:@"HelveticaNeue" size:DEFAULT_TITLE_FONT_SIZE];
self.font = [UIFont systemFontOfSize:DEFAULT_TITLE_FONT_SIZE];

if (titleFont != nil)
self.titleFont = titleFont;
else
self.titleFont = [UIFont boldSystemFontOfSize:DEFAULT_TITLE_FONT_SIZE];

self.attachedFrame = frame;
self.title = title;
Expand All @@ -60,7 +70,7 @@ -(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)d
}

// position bubble
[self setFrame:[self calculateFrameWithFont:self.font]];
[self setFrame:[self calculateFrame]];
[self fixFrameIfOutOfBounds];

// Make it pass touch events through to the DDCoachMarksView
Expand All @@ -73,16 +83,52 @@ -(id)initWithFrame:(CGRect)frame title:(NSString*)title description:(NSString*)d
float actualWidth = self.frame.size.width-actualXPosition - PADDING*1.5;
float actualHeight = self.frame.size.height - actualYPosition - PADDING*1.2;

UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(actualXPosition, actualYPosition, actualWidth, actualHeight)];
[titleLabel setFont:self.font];
[titleLabel setTextColor:TEXT_COLOR];
[titleLabel setAlpha:0.9];
[titleLabel setText:title];
[titleLabel setBackgroundColor:[UIColor clearColor]];
[titleLabel setLineBreakMode:NSLineBreakByWordWrapping];
[titleLabel setNumberOfLines:0];
[titleLabel setUserInteractionEnabled:NO];
[self addSubview:titleLabel];
UILabel *titleLabel = nil;

if ((title.length > 0) && ([self respondsToSelector:@selector(addConstraint:)])) {
titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(actualXPosition, actualYPosition, actualWidth, actualHeight)];
[titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[titleLabel setFont:self.titleFont];
[titleLabel setTextColor:TEXT_COLOR];
[titleLabel setAlpha:0.9];
[titleLabel setText:title];
[titleLabel setBackgroundColor:[UIColor clearColor]];
[titleLabel setLineBreakMode:NSLineBreakByWordWrapping];
[titleLabel setNumberOfLines:0];
[titleLabel setUserInteractionEnabled:NO];
[self addSubview:titleLabel];

[self addConstraint:[NSLayoutConstraint constraintWithItem:titleLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:actualYPosition]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:titleLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0f constant:(-offsets.width - PADDING*3.0 + 1)]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:titleLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0f constant:actualXPosition]];
}

if (description.length > 0) {
UILabel *bodyLabel = [[UILabel alloc] initWithFrame:CGRectMake(actualXPosition, actualYPosition, actualWidth, actualHeight)];
if ([bodyLabel respondsToSelector:@selector(setTranslatesAutoresizingMaskIntoConstraints:)]) {
[bodyLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
}
[bodyLabel setFont:self.font];
[bodyLabel setTextColor:TEXT_COLOR];
[bodyLabel setAlpha:0.9];
[bodyLabel setText:description];
[bodyLabel setBackgroundColor:[UIColor clearColor]];
[bodyLabel setLineBreakMode:NSLineBreakByWordWrapping];
[bodyLabel setNumberOfLines:0];
[bodyLabel setUserInteractionEnabled:NO];
[self addSubview:bodyLabel];

if ([bodyLabel respondsToSelector:@selector(setTranslatesAutoresizingMaskIntoConstraints:)]) {
if (titleLabel != nil) {
// Add some constraints
[self addConstraint:[NSLayoutConstraint constraintWithItem:bodyLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:titleLabel attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]];
} else {
[self addConstraint:[NSLayoutConstraint constraintWithItem:bodyLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:actualYPosition]];
}
[self addConstraint:[NSLayoutConstraint constraintWithItem:bodyLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0f constant:(-offsets.width - PADDING*3.0 + 1)]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:bodyLabel attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0f constant:actualXPosition]];
}
}

[self setNeedsDisplay];
return self;
Expand Down Expand Up @@ -130,28 +176,28 @@ - (void)fixFrameIfOutOfBounds
self.arrowPosition = CRArrowPositionBottom;

// Restart the entire process
CGRect flippedFrame = [self calculateFrameWithFont:[self font]];
y = flippedFrame.origin.y;
CGRect flippedFrame = [self calculateFrame];
y = MAX(flippedFrame.origin.y, 10);
height = flippedFrame.size.height;
} else if ((self.arrowPosition == CRArrowPositionBottom) && (y < 0)) {
self.arrowPosition = CRArrowPositionTop;

// Restart the entire process
CGRect flippedFrame = [self calculateFrameWithFont:[self font]];
CGRect flippedFrame = [self calculateFrame];
y = flippedFrame.origin.y;
height = flippedFrame.size.height;
}

[self setFrame:CGRectMake(x, y, width, height)];
}

-(CGRect)calculateFrameWithFont:(UIFont*)font
-(CGRect)calculateFrame
{
//Calculation of the bubble position
float x = self.attachedFrame.origin.x;
float y = self.attachedFrame.origin.y;

CGSize size = [self sizeWithFont:font];
CGSize size = [self calculateSize];

float widthDelta = 0, heightDelta = 0;

Expand All @@ -171,7 +217,7 @@ -(CGRect)calculateFrameWithFont:(UIFont*)font
return CGRectMake(x, y, size.width+widthDelta, size.height+heightDelta);
}

-(CGSize)sizeWithFont:(UIFont*)font
-(CGSize)calculateSize
{
// Calcultation of the bubble size
// size of bubble title determined by the strings attributes
Expand All @@ -184,9 +230,17 @@ -(CGSize)sizeWithFont:(UIFont*)font
widthDelta = ARROW_SIZE;
}

CGSize result = [_title sizeWithFont:font constrainedToSize:CGSizeMake(window.size.width - widthDelta - (PADDING*3), FLT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
CGSize titleResult = CGSizeZero;
if (self.title.length > 0) {
titleResult = [self.title sizeWithFont:self.titleFont constrainedToSize:CGSizeMake(window.size.width - widthDelta - (PADDING*3), FLT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
}

CGSize result = CGSizeZero;
if (self.bubbleText.length > 0) {
result = [self.bubbleText sizeWithFont:self.font constrainedToSize:CGSizeMake(window.size.width - widthDelta - (PADDING*3), FLT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
}

return CGSizeMake(result.width + (PADDING*3), result.height + (PADDING*2.5));
return CGSizeMake(MAX(titleResult.width, result.width) + (PADDING*3), titleResult.height + result.height + (PADDING*2.5));
}

-(CGSize)offsets
Expand All @@ -204,7 +258,7 @@ - (void)drawRect:(CGRect)rect
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);

CGSize size = [self sizeWithFont:[self font]];
CGSize size = [self calculateSize];

CGPathRef clippath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake([self offsets].width,[self offsets].height, size.width, size.height) cornerRadius:RADIUS].CGPath;
CGContextAddPath(ctx, clippath);
Expand Down
3 changes: 2 additions & 1 deletion Coach Marks/DDCoachMarksView.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@property (nonatomic) CGFloat maxLblWidth;
@property (nonatomic) CGFloat lblSpacing;
@property (nonatomic) BOOL useBubbles;
@property (nonatomic) BOOL transmitTouchesInCutout;

- (id)initWithFrame:(CGRect)frame coachMarks:(NSArray *)marks;
- (void)setMaskColor:(UIColor *)maskColor;
Expand All @@ -37,4 +38,4 @@
- (void)coachMarksViewDidCleanup:(DDCoachMarksView*)coachMarksView;
- (void)didTapAtIndex:(NSUInteger)index;

@end
@end
66 changes: 53 additions & 13 deletions Coach Marks/DDCoachMarksView.m
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ - (void)setMaskColor:(UIColor *)maskColor {

#pragma mark - Touch handler

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {

// See if tap was inside the cutout or not
BOOL isOutsideCutout = !self.transmitTouchesInCutout || CGPathContainsPoint(mask.path, NULL, point, true);

if (!isOutsideCutout) {
[self cleanup];
}

return isOutsideCutout;
}

- (void)userDidTap:(UITapGestureRecognizer *)recognizer {

if ([self.delegate respondsToSelector:@selector(didTapAtIndex:)]) {
Expand Down Expand Up @@ -184,7 +196,7 @@ - (void)goToCoachMarkIndexed:(NSUInteger)index {

// Coach mark definition
NSDictionary *markDef = [self.coachMarks objectAtIndex:index];
CGRect markRect = [[markDef objectForKey:@"rect"] CGRectValue];
CGRect markRect = [self frameFromCoachMark:markDef];
NSString *shape = [markDef objectForKey:@"shape"];

if (self.useBubbles) {
Expand All @@ -210,7 +222,7 @@ - (void)goToCoachMarkIndexed:(NSUInteger)index {
- (void)showSwipeGesture
{
NSDictionary *coachMarkInfo = [self.coachMarks objectAtIndex:markIndex];
CGRect frame = [[coachMarkInfo objectForKey:@"rect"] CGRectValue];
CGRect frame = [self frameFromCoachMark:coachMarkInfo];
BOOL shouldAnimateSwipe = [[coachMarkInfo objectForKey:@"swipe"] boolValue];

NSString* swipeDirection = [coachMarkInfo objectForKey:@"direction"];
Expand Down Expand Up @@ -246,14 +258,44 @@ - (void)showSwipeGesture

#pragma mark - Bubble Caption

- (CGRect)frameFromCoachMark:(NSDictionary*)coachMarkInfo {
CGRect frame = [[coachMarkInfo objectForKey:@"rect"] CGRectValue];
CGRect poi = [[coachMarkInfo objectForKey:@"POI"] CGRectValue];
UIView *attachedView = [coachMarkInfo objectForKey:@"attachedview"];
NSValue *transformValue = [coachMarkInfo objectForKey:@"transform"];

// IF using point of interest (poi) frame use that instead of cutout frame
// ELSE use the cutout frame
CGRect viewBounds = CGRectZero;
if (attachedView != nil) {
viewBounds = attachedView.bounds;
} else if (CGRectIsEmpty(poi)) {
viewBounds = frame;
} else {
viewBounds = poi;
}

if (transformValue != nil) {
// Apply the given transform
CGAffineTransform transform = [transformValue CGAffineTransformValue];
viewBounds = CGRectApplyAffineTransform(viewBounds, transform);
}

if (attachedView != nil) {
viewBounds = [attachedView convertRect:viewBounds toView:self];
}

return viewBounds;
}

- (void)animateNextBubble
{
// Get current coach mark information
NSDictionary *coachMarkInfo = [self.coachMarks objectAtIndex:markIndex];
NSString *markTitle = [coachMarkInfo objectForKey:@"title"];
NSString *markCaption = [coachMarkInfo objectForKey:@"caption"];
CGRect frame = [[coachMarkInfo objectForKey:@"rect"] CGRectValue];
CGRect poi = [[coachMarkInfo objectForKey:@"POI"] CGRectValue];
UIFont *font = [coachMarkInfo objectForKey:@"font"];
UIFont *titleFont = [coachMarkInfo objectForKey:@"titlefont"];

// remove previous bubble
if (self.bubble) {
Expand All @@ -264,16 +306,12 @@ - (void)animateNextBubble
}

// return if no text for bubble
if ([markCaption length] == 0)
if (([markCaption length] == 0) && ([markTitle length] == 0))
return;

// create bubble
// IF using point of interest (poi) frame use that instead of cutout frame
// ELSE use the cutout frame
if (CGRectIsEmpty(poi)) {
self.bubble = [[DDBubble alloc] initWithFrame:frame title:markCaption description:nil arrowPosition:CRArrowPositionTop color:nil andFont:font];
} else
self.bubble = [[DDBubble alloc] initWithFrame:poi title:markCaption description:nil arrowPosition:CRArrowPositionTop color:nil andFont:font];
CGRect frame = [self frameFromCoachMark:coachMarkInfo];
self.bubble = [[DDBubble alloc] initWithFrame:frame title:markTitle description:markCaption arrowPosition:CRArrowPositionTop color:nil andFont:font titleFont:titleFont];

self.bubble.alpha = 0.0;
[self addSubview:self.bubble];
Expand All @@ -299,7 +337,9 @@ - (void)cleanup {
__weak DDCoachMarksView *weakSelf = self;

// animate & remove from super view
[UIView animateWithDuration:0.6 delay:0.3 options:0
// Need UIViewAnimationOptionAllowUserInteraction option if triggered by touching inside the cutout,
// else it cancels the touch that triggered the cleanup in the first place
[UIView animateWithDuration:0.6 delay:0.3 options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.alpha = 0.0f;
self.animatingCircle.alpha = 0.0f;
Expand Down Expand Up @@ -336,4 +376,4 @@ - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
}
}

@end
@end
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Create a new DDCoachMarksView instance and pass in an array of coach mark defini
@"shape": @"circle",
},
@{
@"rect": [NSValue valueWithCGRect:CGRectMake(0, 125, 320, 60)],
@"attachedview": self.viewToHighlight,
@"caption": @"Swipe for more options",
@"shape": @"square",
@"swipe": @"YES",
Expand Down Expand Up @@ -78,24 +78,33 @@ You can configure any coach mark properties before calling the `start` method. F
coachMarksView.animationDuration = 0.5f;
coachMarksView.maskColor = [UIColor blueColor];
coachMarksView.useBubbles = NO;
coachMarksView.transmitTouchesInCutout = NO; // Don't allow the user to interact with the element being showcased (default)
[coachMarksView start];
```

## Configuration
When creating your array of dictionary definitions for each coach mark only the `@"rect"` value is required.
Other optional values are:
* `@"caption"`
Text that goes in the bubbles
Text that goes in the bubbles
* `@"title"`
An optional title to go above the caption
* `@"shape"`
Can be set to circle or square. If nothing is defined, the default is a rounded rect.
* `@"POI"`
Stands for 'point of interest'. You can define a whole region using the `@"rect"` value, but defining a different CGRect value here makes the bubble caption position itself under the POI rect.
* `@"attachedview"`
Specify a specific UIView to highlight instead of a manually specified `@"rect"` or `@"POI"`.
* `@"transform"`
Specify an optional CGAffineTransform that will be applied to the `@"rect"`, `@"POI"` or `@"attachedview"` frame. Especially useful if you want to modify the frame of the `@"attachedview"` in any way before highlighting.
* `@"swipe"`
Use "YES" here if you want to show a row swipe gesture on a table view cell. Disabled by default.
* `@"direction"`
Direction that swipe gestures should animate in. The default is `@"lefttoright"` but you can also specify `@"righttoleft"`.
* `@"font"`
Font for the caption in the bubble. If not specified, defaults to the default HelveticaNeue size 14.0.
Font for the caption in the bubble. If not specified, defaults to the system font size 14.0.
* `@"titlefont"`
Font for the title caption in the bubble. If not specified, defaults to the bold system font size 14.0.

## DDCoachMarksViewDelegate

Expand Down