From 5cefa515112c538fbb39aca1aa985c13a8299dc4 Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Wed, 11 Oct 2017 11:47:05 -0700 Subject: [ios] Rename SMCalloutView and stop using submodule --- .gitmodules | 3 - platform/ios/CHANGELOG.md | 1 + platform/ios/src/MGLCompactCalloutView.h | 2 +- platform/ios/src/MGLMapView.mm | 4 +- platform/ios/vendor/SMCalloutView | 1 - platform/ios/vendor/SMCalloutView/SMCalloutView.h | 205 ++++++ platform/ios/vendor/SMCalloutView/SMCalloutView.m | 851 ++++++++++++++++++++++ 7 files changed, 1060 insertions(+), 7 deletions(-) delete mode 160000 platform/ios/vendor/SMCalloutView create mode 100755 platform/ios/vendor/SMCalloutView/SMCalloutView.h create mode 100755 platform/ios/vendor/SMCalloutView/SMCalloutView.m 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 +@interface MGLCompactCalloutView : MGLSMCalloutView + (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 () @@ -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 index d6ecaba377..0000000000 --- a/platform/ios/vendor/SMCalloutView +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d6ecaba377c9f963aef630faf86e3b8f8cdb88d1 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 +#import + +/* + +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 +#endif + +@property (nonatomic, weak, nullable) id 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 +@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 -- cgit v1.2.1