diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index daa9c92..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index de7b430..7c386a9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint +.DS_Store ## Obj-C/Swift specific *.hmap diff --git a/Coach Marks/DDBubble.h b/Coach Marks/DDBubble.h index 3fbd8a9..91a00dc 100755 --- a/Coach Marks/DDBubble.h +++ b/Coach Marks/DDBubble.h @@ -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 \ No newline at end of file +@end diff --git a/Coach Marks/DDBubble.m b/Coach Marks/DDBubble.m index 00d7362..f3e9165 100755 --- a/Coach Marks/DDBubble.m +++ b/Coach Marks/DDBubble.m @@ -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) @@ -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; @@ -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 @@ -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; @@ -130,14 +176,14 @@ - (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; } @@ -145,13 +191,13 @@ - (void)fixFrameIfOutOfBounds [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; @@ -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 @@ -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 @@ -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); diff --git a/Coach Marks/DDCoachMarksView.h b/Coach Marks/DDCoachMarksView.h index 1053544..0409ff9 100755 --- a/Coach Marks/DDCoachMarksView.h +++ b/Coach Marks/DDCoachMarksView.h @@ -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; @@ -37,4 +38,4 @@ - (void)coachMarksViewDidCleanup:(DDCoachMarksView*)coachMarksView; - (void)didTapAtIndex:(NSUInteger)index; -@end \ No newline at end of file +@end diff --git a/Coach Marks/DDCoachMarksView.m b/Coach Marks/DDCoachMarksView.m index 9330a7c..77a8a2d 100755 --- a/Coach Marks/DDCoachMarksView.m +++ b/Coach Marks/DDCoachMarksView.m @@ -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:)]) { @@ -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) { @@ -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"]; @@ -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) { @@ -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]; @@ -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; @@ -336,4 +376,4 @@ - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { } } -@end \ No newline at end of file +@end diff --git a/README.md b/README.md index 90e43bf..8721b19 100644 --- a/README.md +++ b/README.md @@ -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", @@ -78,6 +78,7 @@ 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]; ``` @@ -85,17 +86,25 @@ coachMarksView.useBubbles = NO; 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