summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--platform/ios/CHANGELOG.md1
-rw-r--r--platform/ios/src/MGLCompactCalloutView.h2
-rw-r--r--platform/ios/src/MGLMapView.mm4
m---------platform/ios/vendor/SMCalloutView0
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.h205
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.m851
7 files changed, 1060 insertions, 6 deletions
diff --git a/.gitmodules b/.gitmodules
index 72cbe56da7..422fc3930e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
-[submodule "platform/ios/vendor/SMCalloutView"]
- path = platform/ios/vendor/SMCalloutView
- url = https://github.com/nfarina/calloutview.git
[submodule "platform/ios/uitest/KIF"]
path = platform/ios/uitest/KIF
url = https://github.com/kif-framework/KIF.git
diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md
index 3035be70e1..82de619d7b 100644
--- a/platform/ios/CHANGELOG.md
+++ b/platform/ios/CHANGELOG.md
@@ -36,6 +36,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Fixed an issue that could cause line label rendering glitches when the line geometry is projected to a point behind the plane of the camera. ([#9865](https://github.com/mapbox/mapbox-gl-native/pull/9865))
* Fixed an issue that could cause a crash when using `-[MGLMapView flyToCamera:completionHandler:]` and related methods with zoom levels at or near the maximum value. ([#9381](https://github.com/mapbox/mapbox-gl-native/pull/9381))
* Added `-[MGLMapView showAttribution:]` to allow custom attribution buttons to show the default attribution interface. ([#10085](https://github.com/mapbox/mapbox-gl-native/pull/10085))
+* Fixed a conflict between multiple copies of SMCalloutView in a project. ([#10183](https://github.com/mapbox/mapbox-gl-native/pull/10183))
## 3.6.4 - September 25, 2017
diff --git a/platform/ios/src/MGLCompactCalloutView.h b/platform/ios/src/MGLCompactCalloutView.h
index 56c48a99e5..5cecf37ff6 100644
--- a/platform/ios/src/MGLCompactCalloutView.h
+++ b/platform/ios/src/MGLCompactCalloutView.h
@@ -7,7 +7,7 @@
callout view displays the represented annotation’s title, subtitle, and
accessory views in a compact, two-line layout.
*/
-@interface MGLCompactCalloutView : SMCalloutView <MGLCalloutView>
+@interface MGLCompactCalloutView : MGLSMCalloutView <MGLCalloutView>
+ (instancetype)platformCalloutView;
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 0e76c0c71c..ac608dd074 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -232,7 +232,7 @@ public:
@interface MGLMapView () <UIGestureRecognizerDelegate,
GLKViewDelegate,
CLLocationManagerDelegate,
- SMCalloutViewDelegate,
+ MGLSMCalloutViewDelegate,
MGLCalloutViewDelegate,
MGLMultiPointDelegate,
MGLAnnotationImageDelegate>
@@ -1929,7 +1929,7 @@ public:
return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)];
}
-- (void)calloutViewClicked:(__unused SMCalloutView *)calloutView
+- (void)calloutViewClicked:(__unused MGLSMCalloutView *)calloutView
{
if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)])
{
diff --git a/platform/ios/vendor/SMCalloutView b/platform/ios/vendor/SMCalloutView
deleted file mode 160000
-Subproject d6ecaba377c9f963aef630faf86e3b8f8cdb88d
diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.h b/platform/ios/vendor/SMCalloutView/SMCalloutView.h
new file mode 100755
index 0000000000..0b14913626
--- /dev/null
+++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.h
@@ -0,0 +1,205 @@
+#import <UIKit/UIKit.h>
+#import <QuartzCore/QuartzCore.h>
+
+/*
+
+SMCalloutView
+-------------
+Created by Nick Farina (nfarina@gmail.com)
+Version 2.1.5
+
+*/
+
+/// options for which directions the callout is allowed to "point" in.
+typedef NS_OPTIONS(NSUInteger, MGLSMCalloutArrowDirection) {
+ MGLSMCalloutArrowDirectionUp = 1 << 0,
+ MGLSMCalloutArrowDirectionDown = 1 << 1,
+ MGLSMCalloutArrowDirectionAny = MGLSMCalloutArrowDirectionUp | MGLSMCalloutArrowDirectionDown
+};
+
+/// options for the callout present/dismiss animation
+typedef NS_ENUM(NSInteger, MGLSMCalloutAnimation) {
+ /// the "bounce" animation we all know and love from @c UIAlertView
+ MGLSMCalloutAnimationBounce,
+ /// a simple fade in or out
+ MGLSMCalloutAnimationFade,
+ /// grow or shrink linearly, like in the iPad Calendar app
+ MGLSMCalloutAnimationStretch
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// when delaying our popup in order to scroll content into view, you can use this amount to match the
+/// animation duration of UIScrollView when using @c -setContentOffset:animated.
+extern NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView;
+
+@protocol MGLSMCalloutViewDelegate;
+@class MGLSMCalloutBackgroundView;
+
+//
+// Callout view.
+//
+
+// iOS 10+ expects CAAnimationDelegate to be set explicitly.
+#if __IPHONE_OS_VERSION_MAX_ALLOWED < 100000
+@interface MGLSMCalloutView : UIView
+#else
+@interface MGLSMCalloutView : UIView <CAAnimationDelegate>
+#endif
+
+@property (nonatomic, weak, nullable) id<MGLSMCalloutViewDelegate> delegate;
+/// title/titleView relationship mimics UINavigationBar.
+@property (nonatomic, copy, nullable) NSString *title;
+@property (nonatomic, copy, nullable) NSString *subtitle;
+
+/// Left accessory view for the call out
+@property (nonatomic, strong, nullable) UIView *leftAccessoryView;
+/// Right accessoty view for the call out
+@property (nonatomic, strong, nullable) UIView *rightAccessoryView;
+/// Default @c SMCalloutArrowDirectionDown
+@property (nonatomic, assign) MGLSMCalloutArrowDirection permittedArrowDirection;
+/// The current arrow direction
+@property (nonatomic, readonly) MGLSMCalloutArrowDirection currentArrowDirection;
+/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets.
+@property (nonatomic, assign) UIEdgeInsets constrainedInsets;
+/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView
+@property (nonatomic, strong) MGLSMCalloutBackgroundView *backgroundView;
+
+/**
+ @brief Custom title view.
+
+ @disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view
+ may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
+
+ @warning If this is set, the respective @c title property will be ignored.
+ */
+@property (nonatomic, strong, nullable) UIView *titleView;
+
+/**
+ @brief Custom subtitle view.
+
+ @discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view
+ may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
+
+ @warning If this is set, the respective @c subtitle property will be ignored.
+ */
+@property (nonatomic, strong, nullable) UIView *subtitleView;
+
+/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored.
+@property (nonatomic, retain, nullable) UIView *contentView;
+
+/// Custom content view margin
+@property (nonatomic, assign) UIEdgeInsets contentViewInset;
+
+/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown.
+@property (nonatomic, assign) CGPoint calloutOffset;
+
+/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively
+@property (nonatomic, assign) MGLSMCalloutAnimation presentAnimation, dismissAnimation;
+
+/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available.
++ (MGLSMCalloutView *)platformCalloutView;
+
+/**
+ @brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds.
+
+ @discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins)
+ if @c -delegate is set and responds to @c -delayForRepositionWithSize.
+
+ @param rect @c CGRect to present the view from
+ @param view view to 'constrain' the @c constrainedView to
+ @param constrainedView @c UIView to be constrainted in @c view
+ @param animated @c BOOL if presentation should be animated
+ */
+- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated;
+
+/**
+ @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds
+
+ @discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead.
+ @note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events.
+
+ @param rect @c CGRect to present the view from
+ @param layer layer to 'constrain' the @c constrainedLayer to
+ @param constrainedLayer @c CALayer to be constrained in @c layer
+ @param animated @c BOOL if presentation should be animated
+ */
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated;
+
+/**
+ Dismiss the callout view
+
+ @param animated @c BOOL if dismissal should be animated
+ */
+- (void)dismissCalloutAnimated:(BOOL)animated;
+
+/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout.
+- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting;
+
+@end
+
+//
+// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow).
+//
+
+/// Abstract base class
+@interface MGLSMCalloutBackgroundView : UIView
+/// indicates where the tip of the arrow should be drawn, as a pixel offset
+@property (nonatomic, assign) CGPoint arrowPoint;
+/// will be set by the callout when the callout is in a highlighted state
+@property (nonatomic, assign) BOOL highlighted;
+/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView )
+@property (nonatomic, assign) CALayer *contentMask;
+/// height of the callout "arrow"
+@property (nonatomic, assign) CGFloat anchorHeight;
+/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
+@property (nonatomic, assign) CGFloat anchorMargin;
+@end
+
+/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view.
+/// Accessories are masked by the shape of the callout (including the arrow itself).
+@interface MGLSMCalloutMaskedBackgroundView : MGLSMCalloutBackgroundView
+@end
+
+//
+// Delegate methods
+//
+
+@protocol MGLSMCalloutViewDelegate <NSObject>
+@optional
+
+/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below.
+/// Not honored by @c SMClassicCalloutView.
+- (BOOL)calloutViewShouldHighlight:(MGLSMCalloutView *)calloutView;
+
+/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView.
+- (void)calloutViewClicked:(MGLSMCalloutView *)calloutView;
+
+/**
+ Called when the callout view detects that it will be outside the constrained view when it appears,
+ or if the target rect was already outside the constrained view. You can implement this selector
+ to respond to this situation by repositioning your content first in order to make everything visible.
+ The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin).
+ It expects you to return the amount of time you need to reposition things so the popup can be delayed.
+ Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:].
+
+ @param calloutView the @c SMCalloutView to reposition
+ @param offset caluclated offset necessary to make everything visible
+ @returns @c NSTimeInterval to delay the repositioning
+ */
+- (NSTimeInterval)calloutView:(MGLSMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset;
+
+/// Called before the callout view appears on screen, or before the appearance animation will start.
+- (void)calloutViewWillAppear:(MGLSMCalloutView *)calloutView;
+
+/// Called after the callout view appears on screen, or after the appearance animation is complete.
+- (void)calloutViewDidAppear:(MGLSMCalloutView *)calloutView;
+
+/// Called before the callout view is removed from the screen, or before the disappearance animation is complete.
+- (void)calloutViewWillDisappear:(MGLSMCalloutView *)calloutView;
+
+/// Called after the callout view is removed from the screen, or after the disappearance animation is complete.
+- (void)calloutViewDidDisappear:(MGLSMCalloutView *)calloutView;
+
+NS_ASSUME_NONNULL_END
+@end
diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.m b/platform/ios/vendor/SMCalloutView/SMCalloutView.m
new file mode 100755
index 0000000000..9631ca0367
--- /dev/null
+++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.m
@@ -0,0 +1,851 @@
+#import "SMCalloutView.h"
+
+//
+// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
+//
+
+@interface UIView (SMFrameAdditions)
+@property (nonatomic, assign) CGPoint frameOrigin;
+@property (nonatomic, assign) CGSize frameSize;
+@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
+@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
+@end
+
+//
+// Callout View.
+//
+
+#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow
+#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present)
+#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout
+#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories
+#define TITLE_TOP 11 // the top of the title view when no subtitle is present
+#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present
+#define TITLE_HEIGHT 21 // title height, fixed
+#define SUBTITLE_TOP 28 // the top of the subtitle, when present
+#define SUBTITLE_HEIGHT 15 // subtitle height, fixed
+#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present
+#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.
+#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
+
+NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
+
+@interface MGLSMCalloutView ()
+@property (nonatomic, strong) UIButton *containerView; // for masking and interaction
+@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
+@property (nonatomic, assign) MGLSMCalloutArrowDirection currentArrowDirection;
+@property (nonatomic, assign) BOOL popupCancelled;
+@end
+
+@implementation MGLSMCalloutView
+
++ (MGLSMCalloutView *)platformCalloutView {
+ // MGL: Mapbox does not need or include the custom flavor, so this is modified to just use SMCalloutView.
+ return [MGLSMCalloutView new];
+}
+
+- (id)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.permittedArrowDirection = MGLSMCalloutArrowDirectionDown;
+ self.presentAnimation = MGLSMCalloutAnimationBounce;
+ self.dismissAnimation = MGLSMCalloutAnimationFade;
+ self.backgroundColor = [UIColor clearColor];
+ self.containerView = [UIButton new];
+ self.containerView.isAccessibilityElement = NO;
+ self.isAccessibilityElement = NO;
+ self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12);
+
+ [self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
+ [self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
+ [self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return self;
+}
+
+- (BOOL)supportsHighlighting {
+ if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)])
+ return NO;
+ if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)])
+ return [self.delegate calloutViewShouldHighlight:self];
+ return YES;
+}
+
+- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; }
+- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; }
+
+- (void)calloutClicked {
+ if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)])
+ [self.delegate calloutViewClicked:self];
+}
+
+- (UIView *)titleViewOrDefault {
+ if (self.titleView)
+ // if you have a custom title view defined, return that.
+ return self.titleView;
+ else {
+ if (!self.titleLabel) {
+ // create a default titleView
+ self.titleLabel = [UILabel new];
+ self.titleLabel.frameHeight = TITLE_HEIGHT;
+ self.titleLabel.opaque = NO;
+ self.titleLabel.backgroundColor = [UIColor clearColor];
+ self.titleLabel.font = [UIFont systemFontOfSize:17];
+ self.titleLabel.textColor = [UIColor blackColor];
+ }
+ return self.titleLabel;
+ }
+}
+
+- (UIView *)subtitleViewOrDefault {
+ if (self.subtitleView)
+ // if you have a custom subtitle view defined, return that.
+ return self.subtitleView;
+ else {
+ if (!self.subtitleLabel) {
+ // create a default subtitleView
+ self.subtitleLabel = [UILabel new];
+ self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
+ self.subtitleLabel.opaque = NO;
+ self.subtitleLabel.backgroundColor = [UIColor clearColor];
+ self.subtitleLabel.font = [UIFont systemFontOfSize:12];
+ self.subtitleLabel.textColor = [UIColor blackColor];
+ }
+ return self.subtitleLabel;
+ }
+}
+
+- (MGLSMCalloutBackgroundView *)backgroundView {
+ // create our default background on first access only if it's nil, since you might have set your own background anyway.
+ return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]);
+}
+
+- (MGLSMCalloutBackgroundView *)defaultBackgroundView {
+ return [MGLSMCalloutMaskedBackgroundView new];
+}
+
+- (void)rebuildSubviews {
+ // remove and re-add our appropriate subviews in the appropriate order
+ [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [self setNeedsDisplay];
+
+ [self addSubview:self.backgroundView];
+ [self addSubview:self.containerView];
+
+ if (self.contentView) {
+ [self.containerView addSubview:self.contentView];
+ }
+ else {
+ if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault];
+ if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault];
+ }
+ if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView];
+ if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView];
+}
+
+// Accessory margins. Accessories are centered vertically when shorter
+// than the callout, otherwise they grow from the upper corner.
+
+- (CGFloat)leftAccessoryVerticalMargin {
+ if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight)
+ return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2);
+ else
+ return 0;
+}
+
+- (CGFloat)leftAccessoryHorizontalMargin {
+ return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN);
+}
+
+- (CGFloat)rightAccessoryVerticalMargin {
+ if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight)
+ return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2);
+ else
+ return 0;
+}
+
+- (CGFloat)rightAccessoryHorizontalMargin {
+ return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN);
+}
+
+- (CGFloat)innerContentMarginLeft {
+ if (self.leftAccessoryView)
+ return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN;
+ else
+ return self.contentViewInset.left;
+}
+
+- (CGFloat)innerContentMarginRight {
+ if (self.rightAccessoryView)
+ return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN;
+ else
+ return self.contentViewInset.right;
+}
+
+- (CGFloat)calloutHeight {
+ return self.calloutContainerHeight + self.backgroundView.anchorHeight;
+}
+
+- (CGFloat)calloutContainerHeight {
+ if (self.contentView)
+ return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top;
+ else if (self.subtitleView || self.subtitle.length > 0)
+ return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT;
+ else
+ return CALLOUT_DEFAULT_CONTAINER_HEIGHT;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+
+ // calculate how much non-negotiable space we need to reserve for margin and accessories
+ CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
+
+ // how much room is left for text?
+ CGFloat availableWidthForText = size.width - margin - 1;
+
+ // no room for text? then we'll have to squeeze into the given size somehow.
+ if (availableWidthForText < 0)
+ availableWidthForText = 0;
+
+ CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
+ CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
+
+ // total width we'd like
+ CGFloat preferredWidth;
+
+ if (self.contentView) {
+
+ // if we have a content view, then take our preferred size directly from that
+ preferredWidth = self.contentView.frameWidth + margin;
+ }
+ else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
+
+ // if we have a title or subtitle, then our assumed margins are valid, and we can apply them
+ preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
+ }
+ else {
+ // ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
+ // at all! But we can handle it.
+ preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin;
+
+ if (self.leftAccessoryView && self.rightAccessoryView)
+ preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
+ }
+
+ // ensure we're big enough to fit our graphics!
+ preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH);
+
+ // ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
+ return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
+}
+
+- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
+ CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
+ CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
+ CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
+ CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
+ return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
+}
+
+- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
+ [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
+}
+
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
+ [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
+}
+
+// this private method handles both CALayer and UIView parents depending on what's passed.
+- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
+
+ // Sanity check: dismiss this callout immediately if it's displayed somewhere
+ if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
+
+ // cancel all animations that may be in progress
+ [self.layer removeAnimationForKey:@"present"];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ // figure out the constrained view's rect in our popup view's coordinate system
+ CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
+
+ // apply our edge constraints
+ constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
+
+ constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN);
+
+ // form our subviews based on our content set so far
+ [self rebuildSubviews];
+
+ // apply title/subtitle (if present
+ self.titleLabel.text = self.title;
+ self.subtitleLabel.text = self.subtitle;
+
+ // size the callout to fit the width constraint as best as possible
+ self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
+
+ // how much room do we have in the constraint box, both above and below our target rect?
+ CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
+ CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
+
+ // we prefer to point our arrow down.
+ MGLSMCalloutArrowDirection bestDirection = MGLSMCalloutArrowDirectionDown;
+
+ // we'll point it up though if that's the only option you gave us.
+ if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp)
+ bestDirection = MGLSMCalloutArrowDirectionUp;
+
+ // or, if we don't have enough space on the top and have more space on the bottom, and you
+ // gave us a choice, then pointing up is the better option.
+ if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
+ bestDirection = MGLSMCalloutArrowDirectionUp;
+
+ self.currentArrowDirection = bestDirection;
+
+ // we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
+ // target view's coordinate system. make sure to offset the anchor point as requested if necessary.
+ CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
+ CGFloat anchorY = self.calloutOffset.y + (bestDirection == MGLSMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
+
+ // we prefer to sit centered directly above our anchor
+ CGFloat calloutX = roundf(anchorX - self.frameWidth / 2);
+
+ // but not if it's going to get too close to the edge of our constraints
+ if (calloutX < constrainedRect.origin.x)
+ calloutX = constrainedRect.origin.x;
+
+ if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth)
+ calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth;
+
+ // what's the farthest to the left and right that we could point to, given our background image constraints?
+ CGFloat minPointX = calloutX + self.backgroundView.anchorMargin;
+ CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin;
+
+ // we may need to scoot over to the left or right to point at the correct spot
+ CGFloat adjustX = 0;
+ if (anchorX < minPointX) adjustX = anchorX - minPointX;
+ if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
+
+ // add the callout to the given layer (or view if possible, to receive touch events)
+ if (view)
+ [view addSubview:self];
+ else
+ [layer addSublayer:self.layer];
+
+ CGPoint calloutOrigin = {
+ .x = calloutX + adjustX,
+ .y = bestDirection == MGLSMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY
+ };
+
+ self.frameOrigin = calloutOrigin;
+
+ // now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
+ CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
+
+ // pass on the anchor point to our background view so it knows where to draw the arrow
+ self.backgroundView.arrowPoint = anchorPoint;
+
+ // adjust it to unit coordinates for the actual layer.anchorPoint property
+ anchorPoint.x /= self.frameWidth;
+ anchorPoint.y /= self.frameHeight;
+ self.layer.anchorPoint = anchorPoint;
+
+ // setting the anchor point moves the view a bit, so we need to reset
+ self.frameOrigin = calloutOrigin;
+
+ // make sure our frame is not on half-pixels or else we may be blurry!
+ CGFloat scale = [UIScreen mainScreen].scale;
+ self.frameX = floorf(self.frameX*scale)/scale;
+ self.frameY = floorf(self.frameY*scale)/scale;
+
+ // layout now so we can immediately start animating to the final position if needed
+ [self setNeedsLayout];
+ [self layoutIfNeeded];
+
+ // if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
+ // 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.
+ CGRect contentRect = CGRectUnion(self.frame, rect);
+ CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
+
+ NSTimeInterval delay = 0;
+ self.popupCancelled = NO; // reset this before calling our delegate below
+
+ if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
+ delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
+
+ // there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
+ // happened then we need to bail!
+ if (self.popupCancelled) return;
+
+ // now we want to mask our contents to our background view (if requested) to match the iOS 7 style
+ self.containerView.layer.mask = self.backgroundView.contentMask;
+
+ // if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
+ self.hidden = YES;
+
+ // create the appropriate animation, even if we're not animated
+ CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
+
+ // nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
+ if (!animated)
+ animation.duration = 0.0000001; // can't be zero or the animation won't "run"
+
+ animation.beginTime = CACurrentMediaTime() + delay;
+ animation.delegate = self;
+
+ [self.layer addAnimation:animation forKey:@"present"];
+}
+
+- (void)animationDidStart:(CAAnimation *)anim {
+ BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
+
+ if (presenting) {
+ if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)])
+ [_delegate calloutViewWillAppear:(id)self];
+
+ // ok, animation is on, let's make ourselves visible!
+ self.hidden = NO;
+ }
+ else if (!presenting) {
+ if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
+ [_delegate calloutViewWillDisappear:(id)self];
+ }
+}
+
+- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
+ BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
+
+ if (presenting && finished) {
+ if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)])
+ [_delegate calloutViewDidAppear:(id)self];
+ }
+ else if (!presenting && finished) {
+
+ [self removeFromParent];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
+ [_delegate calloutViewDidDisappear:(id)self];
+ }
+}
+
+- (void)dismissCalloutAnimated:(BOOL)animated {
+
+ // cancel all animations that may be in progress
+ [self.layer removeAnimationForKey:@"present"];
+ [self.layer removeAnimationForKey:@"dismiss"];
+
+ self.popupCancelled = YES;
+
+ if (animated) {
+ CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
+ animation.delegate = self;
+ [self.layer addAnimation:animation forKey:@"dismiss"];
+ }
+ else {
+ [self removeFromParent];
+ }
+}
+
+- (void)removeFromParent {
+ if (self.superview)
+ [self removeFromSuperview];
+ else {
+ // removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+ [self.layer removeFromSuperlayer];
+ [CATransaction commit];
+ }
+}
+
+- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting {
+ CAAnimation *animation = nil;
+
+ if (type == MGLSMCalloutAnimationBounce) {
+
+ CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ fade.duration = 0.23;
+ fade.fromValue = presenting ? @0.0 : @1.0;
+ fade.toValue = presenting ? @1.0 : @0.0;
+ fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+
+ CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
+ bounce.duration = 0.23;
+ bounce.fromValue = presenting ? @0.7 : @1.0;
+ bounce.toValue = presenting ? @1.0 : @0.7;
+ bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814];
+
+ CAAnimationGroup *group = [CAAnimationGroup animation];
+ group.animations = @[fade, bounce];
+ group.duration = 0.23;
+
+ animation = group;
+ }
+ else if (type == MGLSMCalloutAnimationFade) {
+ CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ fade.duration = 1.0/3.0;
+ fade.fromValue = presenting ? @0.0 : @1.0;
+ fade.toValue = presenting ? @1.0 : @0.0;
+ animation = fade;
+ }
+ else if (type == MGLSMCalloutAnimationStretch) {
+ CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
+ stretch.duration = 0.1;
+ stretch.fromValue = presenting ? @0.0 : @1.0;
+ stretch.toValue = presenting ? @1.0 : @0.0;
+ animation = stretch;
+ }
+
+ // CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
+ [animation setValue:@(presenting) forKey:@"presenting"];
+
+ animation.fillMode = kCAFillModeForwards;
+ animation.removedOnCompletion = NO;
+ return animation;
+}
+
+- (void)layoutSubviews {
+
+ self.containerView.frame = self.bounds;
+ self.backgroundView.frame = self.bounds;
+
+ // if we're pointing up, we'll need to push almost everything down a bit
+ CGFloat dy = self.currentArrowDirection == MGLSMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
+
+ self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
+ self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
+ self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
+
+ self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
+ self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
+ self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
+
+ self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin;
+ self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy;
+
+ self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth;
+ self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy;
+
+ if (self.contentView) {
+ self.contentView.frameX = self.innerContentMarginLeft;
+ self.contentView.frameY = self.contentViewInset.top + dy;
+ }
+}
+
+#pragma mark - Accessibility
+
+- (NSInteger)accessibilityElementCount {
+ return (!!self.leftAccessoryView + !!self.titleViewOrDefault +
+ !!self.subtitleViewOrDefault + !!self.rightAccessoryView);
+}
+
+- (id)accessibilityElementAtIndex:(NSInteger)index {
+ if (index == 0) {
+ return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault;
+ }
+ if (index == 1) {
+ return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault;
+ }
+ if (index == 2) {
+ return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView;
+ }
+ if (index == 3) {
+ return self.leftAccessoryView ? self.rightAccessoryView : nil;
+ }
+ return nil;
+}
+
+- (NSInteger)indexOfAccessibilityElement:(id)element {
+ if (element == nil) return NSNotFound;
+ if (element == self.leftAccessoryView) return 0;
+ if (element == self.titleViewOrDefault) {
+ return self.leftAccessoryView ? 1 : 0;
+ }
+ if (element == self.subtitleViewOrDefault) {
+ return self.leftAccessoryView ? 2 : 1;
+ }
+ if (element == self.rightAccessoryView) {
+ return self.leftAccessoryView ? 3 : 2;
+ }
+ return NSNotFound;
+}
+
+@end
+
+// import this known "private API" from SMCalloutBackgroundView
+@interface MGLSMCalloutBackgroundView (EmbeddedImages)
++ (UIImage *)embeddedImageNamed:(NSString *)name;
+@end
+
+//
+// Callout Background View.
+//
+
+@interface MGLSMCalloutMaskedBackgroundView ()
+@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView;
+@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView;
+@end
+
+static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil;
+
+@implementation MGLSMCalloutMaskedBackgroundView
+
+- (id)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+
+ // Here we're mimicking the very particular (and odd) structure of the system callout view.
+ // The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app
+
+ self.containerView = [UIView new];
+ self.containerView.backgroundColor = [UIColor whiteColor];
+ self.containerView.alpha = 0.96;
+ self.containerView.layer.cornerRadius = 8;
+ self.containerView.layer.shadowRadius = 30;
+ self.containerView.layer.shadowOpacity = 0.1;
+
+ self.containerBorderView = [UIView new];
+ self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
+ self.containerBorderView.layer.borderWidth = 0.5;
+ self.containerBorderView.layer.cornerRadius = 8.5;
+
+ if (!blackArrowImage) {
+ blackArrowImage = [MGLSMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"];
+ whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]];
+ grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]];
+ }
+
+ self.anchorHeight = 13;
+ self.anchorMargin = 27;
+
+ self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)];
+ self.arrowView.alpha = 0.96;
+ self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage];
+ self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage];
+ self.arrowHighlightedImageView.hidden = YES;
+ self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage];
+ self.arrowBorderView.alpha = 0.1;
+ self.arrowBorderView.frameY = 0.5;
+
+ [self addSubview:self.containerView];
+ [self.containerView addSubview:self.containerBorderView];
+ [self addSubview:self.arrowView];
+ [self.arrowView addSubview:self.arrowBorderView];
+ [self.arrowView addSubview:self.arrowImageView];
+ [self.arrowView addSubview:self.arrowHighlightedImageView];
+ }
+ return self;
+}
+
+// Make sure we relayout our images when our arrow point changes!
+- (void)setArrowPoint:(CGPoint)arrowPoint {
+ [super setArrowPoint:arrowPoint];
+ [self setNeedsLayout];
+}
+
+- (void)setHighlighted:(BOOL)highlighted {
+ [super setHighlighted:highlighted];
+ self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor];
+ self.arrowImageView.hidden = highlighted;
+ self.arrowHighlightedImageView.hidden = !highlighted;
+}
+
+- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color {
+
+ UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
+ CGRect imageRect = (CGRect){.size=image.size};
+ CGContextRef c = UIGraphicsGetCurrentContext();
+ CGContextTranslateCTM(c, 0, image.size.height);
+ CGContextScaleCTM(c, 1, -1);
+ CGContextClipToMask(c, imageRect, image.CGImage);
+ [color setFill];
+ CGContextFillRect(c, imageRect);
+ UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return whiteImage;
+}
+
+- (void)layoutSubviews {
+
+ BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
+
+ // if we're pointing up, we'll need to push almost everything down a bit
+ CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
+
+ self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5);
+ self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5);
+
+ self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2);
+
+ if (pointingUp) {
+ self.arrowView.frameY = 1;
+ self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
+ }
+ else {
+ self.arrowView.frameY = self.containerView.frameHeight - 0.5;
+ self.arrowView.transform = CGAffineTransformIdentity;
+ }
+}
+
+- (CALayer *)contentMask {
+
+ UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
+
+ [self.layer renderInContext:UIGraphicsGetCurrentContext()];
+
+ UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ CALayer *layer = [CALayer layer];
+ layer.frame = self.bounds;
+ layer.contents = (id)maskImage.CGImage;
+ return layer;
+}
+
+@end
+
+@implementation MGLSMCalloutBackgroundView
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string {
+ //
+ // NSData+Base64.m
+ //
+ // Version 1.0.2
+ //
+ // Created by Nick Lockwood on 12/01/2012.
+ // Copyright (C) 2012 Charcoal Design
+ //
+ // Distributed under the permissive zlib License
+ // Get the latest version from here:
+ //
+ // https://github.com/nicklockwood/Base64
+ //
+ // This software is provided 'as-is', without any express or implied
+ // warranty. In no event will the authors be held liable for any damages
+ // arising from the use of this software.
+ //
+ // Permission is granted to anyone to use this software for any purpose,
+ // including commercial applications, and to alter it and redistribute it
+ // freely, subject to the following restrictions:
+ //
+ // 1. The origin of this software must not be misrepresented; you must not
+ // claim that you wrote the original software. If you use this software
+ // in a product, an acknowledgment in the product documentation would be
+ // appreciated but is not required.
+ //
+ // 2. Altered source versions must be plainly marked as such, and must not be
+ // misrepresented as being the original software.
+ //
+ // 3. This notice may not be removed or altered from any source distribution.
+ //
+ const char lookup[] = {
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
+ 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
+ 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
+ };
+
+ NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
+ long long inputLength = [inputData length];
+ const unsigned char *inputBytes = [inputData bytes];
+
+ long long maxOutputLength = (inputLength / 4 + 1) * 3;
+ NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength];
+ unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
+
+ int accumulator = 0;
+ long long outputLength = 0;
+ unsigned char accumulated[] = {0, 0, 0, 0};
+ for (long long i = 0; i < inputLength; i++) {
+ unsigned char decoded = lookup[inputBytes[i] & 0x7F];
+ if (decoded != 99) {
+ accumulated[accumulator] = decoded;
+ if (accumulator == 3) {
+ outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
+ }
+ accumulator = (accumulator + 1) % 4;
+ }
+ }
+
+ //handle left-over data
+ if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ if (accumulator > 2) outputLength++;
+
+ //truncate data to match actual output length
+ outputData.length = (NSUInteger)outputLength;
+ return outputLength? outputData: nil;
+}
+
++ (UIImage *)embeddedImageNamed:(NSString *)name {
+ CGFloat screenScale = [UIScreen mainScreen].scale;
+ if (screenScale > 1.0) {
+ name = [name stringByAppendingString:@"_2x"];
+ screenScale = 2.0;
+ }
+
+ SEL selector = NSSelectorFromString(name);
+
+ if (![(id)self respondsToSelector:selector]) {
+ NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name);
+ return nil;
+ }
+
+ // We need to hush the compiler here - but we know what we're doing!
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ NSString *base64String = [(id)self performSelector:selector];
+ #pragma clang diagnostic pop
+
+ UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]];
+ return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp];
+}
+
++ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; }
+
++ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; }
+
+@end
+
+//
+// Our UIView frame helpers implementation
+//
+
+@implementation UIView (SMFrameAdditions)
+
+- (CGPoint)frameOrigin { return self.frame.origin; }
+- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; }
+
+- (CGFloat)frameX { return self.frame.origin.x; }
+- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; }
+
+- (CGFloat)frameY { return self.frame.origin.y; }
+- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; }
+
+- (CGSize)frameSize { return self.frame.size; }
+- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; }
+
+- (CGFloat)frameWidth { return self.frame.size.width; }
+- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; }
+
+- (CGFloat)frameHeight { return self.frame.size.height; }
+- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; }
+
+- (CGFloat)frameLeft { return self.frame.origin.x; }
+- (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 }; }
+
+- (CGFloat)frameTop { return self.frame.origin.y; }
+- (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) }; }
+
+- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; }
+- (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 }; }
+
+- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; }
+- (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) }; }
+
+@end