1#import "SMCalloutView.h" 2 3// 4// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable. 5// 6 7@interface UIView (SMFrameAdditions) 8@property (nonatomic, assign) CGPoint frameOrigin; 9@property (nonatomic, assign) CGSize frameSize; 10@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties 11@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect 12@end 13 14// 15// Callout View. 16// 17 18#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow 19#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present) 20#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout 21#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories 22#define TITLE_TOP 11 // the top of the title view when no subtitle is present 23#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present 24#define TITLE_HEIGHT 21 // title height, fixed 25#define SUBTITLE_TOP 28 // the top of the subtitle, when present 26#define SUBTITLE_HEIGHT 15 // subtitle height, fixed 27#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present 28#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything. 29#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect 30 31NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; 32 33@interface SMCalloutView () 34@property (nonatomic, strong) UIButton *containerView; // for masking and interaction 35@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel; 36@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection; 37@property (nonatomic, assign) BOOL popupCancelled; 38@end 39 40@implementation SMCalloutView 41 42+ (SMCalloutView *)platformCalloutView { 43 44 // if you haven't compiled SMClassicCalloutView into your app, then we can't possibly create an instance of it! 45 if (!NSClassFromString(@"SMClassicCalloutView")) 46 return [SMCalloutView new]; 47 48 // ok we have both - so choose the best one based on current platform 49 if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) 50 return [SMCalloutView new]; // iOS 7+ 51 else 52 return [NSClassFromString(@"SMClassicCalloutView") new]; 53} 54 55- (id)initWithFrame:(CGRect)frame { 56 if (self = [super initWithFrame:frame]) { 57 self.permittedArrowDirection = SMCalloutArrowDirectionDown; 58 self.presentAnimation = SMCalloutAnimationBounce; 59 self.dismissAnimation = SMCalloutAnimationFade; 60 self.backgroundColor = [UIColor clearColor]; 61 self.containerView = [UIButton new]; 62 self.containerView.isAccessibilityElement = NO; 63 self.isAccessibilityElement = NO; 64 self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12); 65 66 [self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside]; 67 [self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside]; 68 [self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside]; 69 } 70 return self; 71} 72 73- (BOOL)supportsHighlighting { 74 if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)]) 75 return NO; 76 if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)]) 77 return [self.delegate calloutViewShouldHighlight:self]; 78 return YES; 79} 80 81- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; } 82- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; } 83 84- (void)calloutClicked { 85 if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)]) 86 [self.delegate calloutViewClicked:self]; 87} 88 89- (UIView *)titleViewOrDefault { 90 if (self.titleView) 91 // if you have a custom title view defined, return that. 92 return self.titleView; 93 else { 94 if (!self.titleLabel) { 95 // create a default titleView 96 self.titleLabel = [UILabel new]; 97 self.titleLabel.frameHeight = TITLE_HEIGHT; 98 self.titleLabel.opaque = NO; 99 self.titleLabel.backgroundColor = [UIColor clearColor]; 100 self.titleLabel.font = [UIFont systemFontOfSize:17]; 101 self.titleLabel.textColor = [UIColor blackColor]; 102 } 103 return self.titleLabel; 104 } 105} 106 107- (UIView *)subtitleViewOrDefault { 108 if (self.subtitleView) 109 // if you have a custom subtitle view defined, return that. 110 return self.subtitleView; 111 else { 112 if (!self.subtitleLabel) { 113 // create a default subtitleView 114 self.subtitleLabel = [UILabel new]; 115 self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT; 116 self.subtitleLabel.opaque = NO; 117 self.subtitleLabel.backgroundColor = [UIColor clearColor]; 118 self.subtitleLabel.font = [UIFont systemFontOfSize:12]; 119 self.subtitleLabel.textColor = [UIColor blackColor]; 120 } 121 return self.subtitleLabel; 122 } 123} 124 125- (SMCalloutBackgroundView *)backgroundView { 126 // create our default background on first access only if it's nil, since you might have set your own background anyway. 127 return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]); 128} 129 130- (SMCalloutBackgroundView *)defaultBackgroundView { 131 return [SMCalloutMaskedBackgroundView new]; 132} 133 134- (void)rebuildSubviews { 135 // remove and re-add our appropriate subviews in the appropriate order 136 [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 137 [self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 138 [self setNeedsDisplay]; 139 140 [self addSubview:self.backgroundView]; 141 [self addSubview:self.containerView]; 142 143 if (self.contentView) { 144 [self.containerView addSubview:self.contentView]; 145 } 146 else { 147 if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault]; 148 if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault]; 149 } 150 if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView]; 151 if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView]; 152} 153 154// Accessory margins. Accessories are centered vertically when shorter 155// than the callout, otherwise they grow from the upper corner. 156 157- (CGFloat)leftAccessoryVerticalMargin { 158 if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight) 159 return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2); 160 else 161 return 0; 162} 163 164- (CGFloat)leftAccessoryHorizontalMargin { 165 return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN); 166} 167 168- (CGFloat)rightAccessoryVerticalMargin { 169 if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight) 170 return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2); 171 else 172 return 0; 173} 174 175- (CGFloat)rightAccessoryHorizontalMargin { 176 return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN); 177} 178 179- (CGFloat)innerContentMarginLeft { 180 if (self.leftAccessoryView) 181 return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN; 182 else 183 return self.contentViewInset.left; 184} 185 186- (CGFloat)innerContentMarginRight { 187 if (self.rightAccessoryView) 188 return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN; 189 else 190 return self.contentViewInset.right; 191} 192 193- (CGFloat)calloutHeight { 194 return self.calloutContainerHeight + self.backgroundView.anchorHeight; 195} 196 197- (CGFloat)calloutContainerHeight { 198 if (self.contentView) 199 return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top; 200 else if (self.subtitleView || self.subtitle.length > 0) 201 return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT; 202 else 203 return CALLOUT_DEFAULT_CONTAINER_HEIGHT; 204} 205 206- (CGSize)sizeThatFits:(CGSize)size { 207 208 // calculate how much non-negotiable space we need to reserve for margin and accessories 209 CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight; 210 211 // how much room is left for text? 212 CGFloat availableWidthForText = size.width - margin - 1; 213 214 // no room for text? then we'll have to squeeze into the given size somehow. 215 if (availableWidthForText < 0) 216 availableWidthForText = 0; 217 218 CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)]; 219 CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)]; 220 221 // total width we'd like 222 CGFloat preferredWidth; 223 224 if (self.contentView) { 225 226 // if we have a content view, then take our preferred size directly from that 227 preferredWidth = self.contentView.frameWidth + margin; 228 } 229 else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) { 230 231 // if we have a title or subtitle, then our assumed margins are valid, and we can apply them 232 preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin; 233 } 234 else { 235 // ok we have no title or subtitle to speak of. In this case, the system callout would actually not display 236 // at all! But we can handle it. 237 preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin; 238 239 if (self.leftAccessoryView && self.rightAccessoryView) 240 preferredWidth += BETWEEN_ACCESSORIES_MARGIN; 241 } 242 243 // ensure we're big enough to fit our graphics! 244 preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH); 245 246 // ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle. 247 return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight); 248} 249 250- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect { 251 CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect)); 252 CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect)); 253 CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect)); 254 CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect)); 255 return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom); 256} 257 258- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { 259 [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated]; 260} 261 262- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { 263 [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated]; 264} 265 266// this private method handles both CALayer and UIView parents depending on what's passed. 267- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { 268 269 // Sanity check: dismiss this callout immediately if it's displayed somewhere 270 if (self.layer.superlayer) [self dismissCalloutAnimated:NO]; 271 272 // cancel any presenting animation that may be in progress 273 [self.layer removeAnimationForKey:@"present"]; 274 275 // figure out the constrained view's rect in our popup view's coordinate system 276 CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; 277 278 // apply our edge constraints 279 constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets); 280 281 constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN); 282 283 // form our subviews based on our content set so far 284 [self rebuildSubviews]; 285 286 // apply title/subtitle (if present 287 self.titleLabel.text = self.title; 288 self.subtitleLabel.text = self.subtitle; 289 290 // size the callout to fit the width constraint as best as possible 291 self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)]; 292 293 // how much room do we have in the constraint box, both above and below our target rect? 294 CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect); 295 CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect); 296 297 // we prefer to point our arrow down. 298 SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown; 299 300 // we'll point it up though if that's the only option you gave us. 301 if (self.permittedArrowDirection == SMCalloutArrowDirectionUp) 302 bestDirection = SMCalloutArrowDirectionUp; 303 304 // or, if we don't have enough space on the top and have more space on the bottom, and you 305 // gave us a choice, then pointing up is the better option. 306 if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace) 307 bestDirection = SMCalloutArrowDirectionUp; 308 309 self.currentArrowDirection = bestDirection; 310 311 // we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our 312 // target view's coordinate system. make sure to offset the anchor point as requested if necessary. 313 CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect); 314 CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect)); 315 316 // we prefer to sit centered directly above our anchor 317 CGFloat calloutX = roundf(anchorX - self.frameWidth / 2); 318 319 // but not if it's going to get too close to the edge of our constraints 320 if (calloutX < constrainedRect.origin.x) 321 calloutX = constrainedRect.origin.x; 322 323 if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth) 324 calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth; 325 326 // what's the farthest to the left and right that we could point to, given our background image constraints? 327 CGFloat minPointX = calloutX + self.backgroundView.anchorMargin; 328 CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin; 329 330 // we may need to scoot over to the left or right to point at the correct spot 331 CGFloat adjustX = 0; 332 if (anchorX < minPointX) adjustX = anchorX - minPointX; 333 if (anchorX > maxPointX) adjustX = anchorX - maxPointX; 334 335 // add the callout to the given layer (or view if possible, to receive touch events) 336 if (view) 337 [view addSubview:self]; 338 else 339 [layer addSublayer:self.layer]; 340 341 CGPoint calloutOrigin = { 342 .x = calloutX + adjustX, 343 .y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY 344 }; 345 346 self.frameOrigin = calloutOrigin; 347 348 // now set the *actual* anchor point for our layer so that our "popup" animation starts from this point. 349 CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer]; 350 351 // pass on the anchor point to our background view so it knows where to draw the arrow 352 self.backgroundView.arrowPoint = anchorPoint; 353 354 // adjust it to unit coordinates for the actual layer.anchorPoint property 355 anchorPoint.x /= self.frameWidth; 356 anchorPoint.y /= self.frameHeight; 357 self.layer.anchorPoint = anchorPoint; 358 359 // setting the anchor point moves the view a bit, so we need to reset 360 self.frameOrigin = calloutOrigin; 361 362 // make sure our frame is not on half-pixels or else we may be blurry! 363 CGFloat scale = [UIScreen mainScreen].scale; 364 self.frameX = floorf(self.frameX*scale)/scale; 365 self.frameY = floorf(self.frameY*scale)/scale; 366 367 // layout now so we can immediately start animating to the final position if needed 368 [self setNeedsLayout]; 369 [self layoutIfNeeded]; 370 371 // if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position. 372 // consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view. 373 CGRect contentRect = CGRectUnion(self.frame, rect); 374 CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect]; 375 376 NSTimeInterval delay = 0; 377 self.popupCancelled = NO; // reset this before calling our delegate below 378 379 if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero)) 380 delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset]; 381 382 // there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that 383 // happened then we need to bail! 384 if (self.popupCancelled) return; 385 386 // now we want to mask our contents to our background view (if requested) to match the iOS 7 style 387 self.containerView.layer.mask = self.backgroundView.contentMask; 388 389 // if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup 390 self.hidden = YES; 391 392 // create the appropriate animation, even if we're not animated 393 CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES]; 394 395 // nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks 396 if (!animated) 397 animation.duration = 0.0000001; // can't be zero or the animation won't "run" 398 399 animation.beginTime = CACurrentMediaTime() + delay; 400 animation.delegate = self; 401 402 [self.layer addAnimation:animation forKey:@"present"]; 403} 404 405- (void)animationDidStart:(CAAnimation *)anim { 406 BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; 407 408 if (presenting) { 409 if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)]) 410 [_delegate calloutViewWillAppear:(id)self]; 411 412 // ok, animation is on, let's make ourselves visible! 413 self.hidden = NO; 414 } 415 else if (!presenting) { 416 if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)]) 417 [_delegate calloutViewWillDisappear:(id)self]; 418 } 419} 420 421- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished { 422 BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; 423 424 if (presenting && finished) { 425 if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)]) 426 [_delegate calloutViewDidAppear:(id)self]; 427 } 428 else if (!presenting && finished) { 429 430 [self removeFromParent]; 431 [self.layer removeAnimationForKey:@"dismiss"]; 432 433 if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)]) 434 [_delegate calloutViewDidDisappear:(id)self]; 435 } 436} 437 438- (void)dismissCalloutAnimated:(BOOL)animated { 439 440 // cancel all animations that may be in progress 441 [self.layer removeAnimationForKey:@"present"]; 442 [self.layer removeAnimationForKey:@"dismiss"]; 443 444 self.popupCancelled = YES; 445 446 if (animated) { 447 CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO]; 448 animation.delegate = self; 449 [self.layer addAnimation:animation forKey:@"dismiss"]; 450 } 451 else { 452 [self removeFromParent]; 453 } 454} 455 456- (void)removeFromParent { 457 if (self.superview) 458 [self removeFromSuperview]; 459 else { 460 // removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable. 461 [CATransaction begin]; 462 [CATransaction setDisableActions:YES]; 463 [self.layer removeFromSuperlayer]; 464 [CATransaction commit]; 465 } 466} 467 468- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting { 469 CAAnimation *animation = nil; 470 471 if (type == SMCalloutAnimationBounce) { 472 473 CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; 474 fade.duration = 0.23; 475 fade.fromValue = presenting ? @0.0 : @1.0; 476 fade.toValue = presenting ? @1.0 : @0.0; 477 fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 478 479 CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; 480 bounce.duration = 0.23; 481 bounce.fromValue = presenting ? @0.7 : @1.0; 482 bounce.toValue = presenting ? @1.0 : @0.7; 483 bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814]; 484 485 CAAnimationGroup *group = [CAAnimationGroup animation]; 486 group.animations = @[fade, bounce]; 487 group.duration = 0.23; 488 489 animation = group; 490 } 491 else if (type == SMCalloutAnimationFade) { 492 CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; 493 fade.duration = 1.0/3.0; 494 fade.fromValue = presenting ? @0.0 : @1.0; 495 fade.toValue = presenting ? @1.0 : @0.0; 496 animation = fade; 497 } 498 else if (type == SMCalloutAnimationStretch) { 499 CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; 500 stretch.duration = 0.1; 501 stretch.fromValue = presenting ? @0.0 : @1.0; 502 stretch.toValue = presenting ? @1.0 : @0.0; 503 animation = stretch; 504 } 505 506 // CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods 507 [animation setValue:@(presenting) forKey:@"presenting"]; 508 509 animation.fillMode = kCAFillModeForwards; 510 animation.removedOnCompletion = NO; 511 return animation; 512} 513 514- (void)layoutSubviews { 515 516 self.containerView.frame = self.bounds; 517 self.backgroundView.frame = self.bounds; 518 519 // if we're pointing up, we'll need to push almost everything down a bit 520 CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0; 521 522 self.titleViewOrDefault.frameX = self.innerContentMarginLeft; 523 self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy; 524 self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight; 525 526 self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX; 527 self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy; 528 self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth; 529 530 self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin; 531 self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy; 532 533 self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth; 534 self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy; 535 536 if (self.contentView) { 537 self.contentView.frameX = self.innerContentMarginLeft; 538 self.contentView.frameY = self.contentViewInset.top + dy; 539 } 540} 541 542#pragma mark - Accessibility 543 544- (NSInteger)accessibilityElementCount { 545 return (!!self.leftAccessoryView + !!self.titleViewOrDefault + 546 !!self.subtitleViewOrDefault + !!self.rightAccessoryView); 547} 548 549- (id)accessibilityElementAtIndex:(NSInteger)index { 550 if (index == 0) { 551 return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault; 552 } 553 if (index == 1) { 554 return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault; 555 } 556 if (index == 2) { 557 return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView; 558 } 559 if (index == 3) { 560 return self.leftAccessoryView ? self.rightAccessoryView : nil; 561 } 562 return nil; 563} 564 565- (NSInteger)indexOfAccessibilityElement:(id)element { 566 if (element == nil) return NSNotFound; 567 if (element == self.leftAccessoryView) return 0; 568 if (element == self.titleViewOrDefault) { 569 return self.leftAccessoryView ? 1 : 0; 570 } 571 if (element == self.subtitleViewOrDefault) { 572 return self.leftAccessoryView ? 2 : 1; 573 } 574 if (element == self.rightAccessoryView) { 575 return self.leftAccessoryView ? 3 : 2; 576 } 577 return NSNotFound; 578} 579 580@end 581 582// import this known "private API" from SMCalloutBackgroundView 583@interface SMCalloutBackgroundView (EmbeddedImages) 584+ (UIImage *)embeddedImageNamed:(NSString *)name; 585@end 586 587// 588// Callout Background View. 589// 590 591@interface SMCalloutMaskedBackgroundView () 592@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView; 593@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView; 594@end 595 596static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil; 597 598@implementation SMCalloutMaskedBackgroundView 599 600- (id)initWithFrame:(CGRect)frame { 601 if (self = [super initWithFrame:frame]) { 602 603 // Here we're mimicking the very particular (and odd) structure of the system callout view. 604 // The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app 605 606 self.containerView = [UIView new]; 607 self.containerView.backgroundColor = [UIColor whiteColor]; 608 self.containerView.alpha = 0.96; 609 self.containerView.layer.cornerRadius = 8; 610 self.containerView.layer.shadowRadius = 30; 611 self.containerView.layer.shadowOpacity = 0.1; 612 613 self.containerBorderView = [UIView new]; 614 self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; 615 self.containerBorderView.layer.borderWidth = 0.5; 616 self.containerBorderView.layer.cornerRadius = 8.5; 617 618 if (!blackArrowImage) { 619 blackArrowImage = [SMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"]; 620 whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]]; 621 grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]]; 622 } 623 624 self.anchorHeight = 13; 625 self.anchorMargin = 27; 626 627 self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)]; 628 self.arrowView.alpha = 0.96; 629 self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage]; 630 self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage]; 631 self.arrowHighlightedImageView.hidden = YES; 632 self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage]; 633 self.arrowBorderView.alpha = 0.1; 634 self.arrowBorderView.frameY = 0.5; 635 636 [self addSubview:self.containerView]; 637 [self.containerView addSubview:self.containerBorderView]; 638 [self addSubview:self.arrowView]; 639 [self.arrowView addSubview:self.arrowBorderView]; 640 [self.arrowView addSubview:self.arrowImageView]; 641 [self.arrowView addSubview:self.arrowHighlightedImageView]; 642 } 643 return self; 644} 645 646// Make sure we relayout our images when our arrow point changes! 647- (void)setArrowPoint:(CGPoint)arrowPoint { 648 [super setArrowPoint:arrowPoint]; 649 [self setNeedsLayout]; 650} 651 652- (void)setHighlighted:(BOOL)highlighted { 653 [super setHighlighted:highlighted]; 654 self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor]; 655 self.arrowImageView.hidden = highlighted; 656 self.arrowHighlightedImageView.hidden = !highlighted; 657} 658 659- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color { 660 661 UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); 662 CGRect imageRect = (CGRect){.size=image.size}; 663 CGContextRef c = UIGraphicsGetCurrentContext(); 664 CGContextTranslateCTM(c, 0, image.size.height); 665 CGContextScaleCTM(c, 1, -1); 666 CGContextClipToMask(c, imageRect, image.CGImage); 667 [color setFill]; 668 CGContextFillRect(c, imageRect); 669 UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext(); 670 UIGraphicsEndImageContext(); 671 return whiteImage; 672} 673 674- (void)layoutSubviews { 675 676 BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2; 677 678 // if we're pointing up, we'll need to push almost everything down a bit 679 CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0; 680 681 self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5); 682 self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5); 683 684 self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2); 685 686 if (pointingUp) { 687 self.arrowView.frameY = 1; 688 self.arrowView.transform = CGAffineTransformMakeRotation(M_PI); 689 } 690 else { 691 self.arrowView.frameY = self.containerView.frameHeight - 0.5; 692 self.arrowView.transform = CGAffineTransformIdentity; 693 } 694} 695 696- (CALayer *)contentMask { 697 698 UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); 699 700 [self.layer renderInContext:UIGraphicsGetCurrentContext()]; 701 702 UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext(); 703 UIGraphicsEndImageContext(); 704 705 CALayer *layer = [CALayer layer]; 706 layer.frame = self.bounds; 707 layer.contents = (id)maskImage.CGImage; 708 return layer; 709} 710 711@end 712 713@implementation SMCalloutBackgroundView 714 715+ (NSData *)dataWithBase64EncodedString:(NSString *)string { 716 // 717 // NSData+Base64.m 718 // 719 // Version 1.0.2 720 // 721 // Created by Nick Lockwood on 12/01/2012. 722 // Copyright (C) 2012 Charcoal Design 723 // 724 // Distributed under the permissive zlib License 725 // Get the latest version from here: 726 // 727 // https://github.com/nicklockwood/Base64 728 // 729 // This software is provided 'as-is', without any express or implied 730 // warranty. In no event will the authors be held liable for any damages 731 // arising from the use of this software. 732 // 733 // Permission is granted to anyone to use this software for any purpose, 734 // including commercial applications, and to alter it and redistribute it 735 // freely, subject to the following restrictions: 736 // 737 // 1. The origin of this software must not be misrepresented; you must not 738 // claim that you wrote the original software. If you use this software 739 // in a product, an acknowledgment in the product documentation would be 740 // appreciated but is not required. 741 // 742 // 2. Altered source versions must be plainly marked as such, and must not be 743 // misrepresented as being the original software. 744 // 745 // 3. This notice may not be removed or altered from any source distribution. 746 // 747 const char lookup[] = { 748 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 749 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 750 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, 751 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, 752 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 753 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, 754 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 755 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 756 }; 757 758 NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; 759 long long inputLength = [inputData length]; 760 const unsigned char *inputBytes = [inputData bytes]; 761 762 long long maxOutputLength = (inputLength / 4 + 1) * 3; 763 NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength]; 764 unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; 765 766 int accumulator = 0; 767 long long outputLength = 0; 768 unsigned char accumulated[] = {0, 0, 0, 0}; 769 for (long long i = 0; i < inputLength; i++) { 770 unsigned char decoded = lookup[inputBytes[i] & 0x7F]; 771 if (decoded != 99) { 772 accumulated[accumulator] = decoded; 773 if (accumulator == 3) { 774 outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); 775 outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); 776 outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; 777 } 778 accumulator = (accumulator + 1) % 4; 779 } 780 } 781 782 //handle left-over data 783 if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); 784 if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); 785 if (accumulator > 2) outputLength++; 786 787 //truncate data to match actual output length 788 outputData.length = (NSUInteger)outputLength; 789 return outputLength? outputData: nil; 790} 791 792+ (UIImage *)embeddedImageNamed:(NSString *)name { 793 CGFloat screenScale = [UIScreen mainScreen].scale; 794 if (screenScale > 1.0) { 795 name = [name stringByAppendingString:@"_2x"]; 796 screenScale = 2.0; 797 } 798 799 SEL selector = NSSelectorFromString(name); 800 801 if (![(id)self respondsToSelector:selector]) { 802 NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name); 803 return nil; 804 } 805 806 // We need to hush the compiler here - but we know what we're doing! 807#pragma clang diagnostic push 808#pragma clang diagnostic ignored "-Warc-performSelector-leaks" 809 NSString *base64String = [(id)self performSelector:selector]; 810#pragma clang diagnostic pop 811 812 UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]]; 813 return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp]; 814} 815 816+ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; } 817 818+ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; } 819 820@end 821 822// 823// Our UIView frame helpers implementation 824// 825 826@implementation UIView (SMFrameAdditions) 827 828- (CGPoint)frameOrigin { return self.frame.origin; } 829- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; } 830 831- (CGFloat)frameX { return self.frame.origin.x; } 832- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; } 833 834- (CGFloat)frameY { return self.frame.origin.y; } 835- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; } 836 837- (CGSize)frameSize { return self.frame.size; } 838- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; } 839 840- (CGFloat)frameWidth { return self.frame.size.width; } 841- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; } 842 843- (CGFloat)frameHeight { return self.frame.size.height; } 844- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; } 845 846- (CGFloat)frameLeft { return self.frame.origin.x; } 847- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; } 848 849- (CGFloat)frameTop { return self.frame.origin.y; } 850- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; } 851 852- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; } 853- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; } 854 855- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; } 856- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; } 857 858@end