diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 428851726..e28142590 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -36,6 +36,8 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Fixed an issue preventing `MGLAnnotationImage.image` from being updated. ([#10372](https://github.com/mapbox/mapbox-gl-native/pull/10372)) * Improved performance of `MGLAnnotationView`-backed annotations that have `scalesWithViewingDistance` enabled. ([#10951](https://github.com/mapbox/mapbox-gl-native/pull/10951)) * Fix an issue where a wrong annotation may selected if annotations were set close together. ([#11438](https://github.com/mapbox/mapbox-gl-native/pull/11438)) +* The `MGLMapView.selectedAnnotations` property (backed by `-[MGLMapView setSelectedAnnotations:]`) now selects annotations that are off-screen. ([#9790](https://github.com/mapbox/mapbox-gl-native/issues/9790)) +* The `animated` parameter to `-[MGLMapView selectAnnotation:animated:]` now controls whether the annotation and its callout are brought on-screen. If `animated` is `NO` then the annotation is selected if offscreen, but the map is not panned. Currently only point annotations are supported. Setting the `MGLMapView.selectedAnnotations` property now animates. ([#3249](https://github.com/mapbox/mapbox-gl-native/issues/3249)) ### Map snapshots diff --git a/platform/ios/app/MBXCustomCalloutView.m b/platform/ios/app/MBXCustomCalloutView.m index 13564c5cb..0626b0997 100644 --- a/platform/ios/app/MBXCustomCalloutView.m +++ b/platform/ios/app/MBXCustomCalloutView.m @@ -37,10 +37,14 @@ static CGFloat const tipWidth = 10.0; return self; } - #pragma mark - API - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated +{ + [self presentCalloutFromRect:rect inView:view constrainedToRect:CGRectNull animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(nonnull UIView *)view constrainedToRect:(__unused CGRect)constrainedRect animated:(BOOL)animated { if ([self.delegate respondsToSelector:@selector(calloutViewWillAppear:)]) { @@ -108,5 +112,4 @@ static CGFloat const tipWidth = 10.0; CGPathRelease(tipPath); } - @end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index a472e3a22..528246922 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -54,6 +54,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsAnnotationsRows) { MBXSettingsAnnotationsQueryAnnotations, MBXSettingsAnnotationsCustomUserDot, MBXSettingsAnnotationsRemoveAnnotations, + MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation, + MBXSettingsAnnotationCenterSelectedAnnotation, + MBXSettingsAnnotationAddVisibleAreaPolyline }; typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) { @@ -340,6 +343,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { @"Query Annotations", [NSString stringWithFormat:@"%@ Custom User Dot", (_customUserLocationAnnnotationEnabled ? @"Disable" : @"Enable")], @"Remove Annotations", + @"Select an offscreen point annotation", + @"Center selected annotation", + @"Add visible area polyline" ]]; break; case MBXSettingsRuntimeStyling: @@ -468,6 +474,18 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { case MBXSettingsAnnotationsRemoveAnnotations: [self.mapView removeAnnotations:self.mapView.annotations]; break; + case MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation: + [self selectAnOffscreenPointAnnotation]; + break; + + case MBXSettingsAnnotationCenterSelectedAnnotation: + [self centerSelectedAnnotation]; + break; + + case MBXSettingsAnnotationAddVisibleAreaPolyline: + [self addVisibleAreaPolyline]; + break; + default: NSAssert(NO, @"All annotations setting rows should be implemented"); break; @@ -1551,6 +1569,73 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { [self presentViewController:alertController animated:YES completion:nil]; } +- (id)randomOffscreenPointAnnotation { + + NSPredicate *pointAnnotationPredicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + return [evaluatedObject isKindOfClass:[MGLPointAnnotation class]]; + }]; + + NSArray *annotations = [self.mapView.annotations filteredArrayUsingPredicate:pointAnnotationPredicate]; + + if (annotations.count == 0) { + return nil; + } + + NSArray *visibleAnnotations = [self.mapView.visibleAnnotations filteredArrayUsingPredicate:pointAnnotationPredicate]; + + if (visibleAnnotations.count == annotations.count) { + return nil; + } + + NSMutableArray *invisibleAnnotations = [annotations mutableCopy]; + + if (visibleAnnotations.count > 0) { + [invisibleAnnotations removeObjectsInArray:visibleAnnotations]; + } + + // Now pick a random offscreen annotation. + uint32_t index = arc4random_uniform((uint32_t)invisibleAnnotations.count); + return invisibleAnnotations[index]; +} + +- (void)selectAnOffscreenPointAnnotation { + id annotation = [self randomOffscreenPointAnnotation]; + if (annotation) { + [self.mapView selectAnnotation:annotation animated:YES]; + + NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); + } +} + +- (void)centerSelectedAnnotation { + id annotation = self.mapView.selectedAnnotations.firstObject; + + if (!annotation) + return; + + CGPoint point = [self.mapView convertCoordinate:annotation.coordinate toPointToView:self.mapView]; + + // Animate, so that point becomes the the center + CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; + [self.mapView setCenterCoordinate:center animated:YES]; +} + +- (void)addVisibleAreaPolyline { + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.mapView.bounds, self.mapView.contentInset); + + CLLocationCoordinate2D lineCoords[5]; + + lineCoords[0] = [self.mapView convertPoint: CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[1] = [self.mapView convertPoint: CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[2] = [self.mapView convertPoint: CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[3] = [self.mapView convertPoint: CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[4] = lineCoords[0]; + + MGLPolyline *line = [MGLPolyline polylineWithCoordinates:lineCoords + count:sizeof(lineCoords)/sizeof(lineCoords[0])]; + [self.mapView addAnnotation:line]; +} + - (void)printTelemetryLogFile { NSString *fileContents = [NSString stringWithContentsOfFile:[self telemetryDebugLogFilePath] encoding:NSUTF8StringEncoding error:nil]; @@ -1602,7 +1687,13 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { toCoordinateFromView:self.mapView]; pin.title = title ?: @"Dropped Pin"; pin.subtitle = [[[MGLCoordinateFormatter alloc] init] stringFromCoordinate:pin.coordinate]; - // Calling `addAnnotation:` on mapView is not required since `selectAnnotation:animated` has the side effect of adding the annotation if required + + + // Calling `addAnnotation:` on mapView is required here (since `selectAnnotation:animated` has + // the side effect of adding the annotation if required, but returning an incorrect callout + // positioning rect) + + [self.mapView addAnnotation:pin]; [self.mapView selectAnnotation:pin animated:YES]; } } diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h index 0481a3968..7e7cf2d02 100644 --- a/platform/ios/src/MGLCalloutView.h +++ b/platform/ios/src/MGLCalloutView.h @@ -41,6 +41,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; + +/** + Presents a callout view by adding it to `view` and pointing at the given rect + of `view`’s bounds. Constrains the callout to the rect in the space of `view`. + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; + /** Dismisses the callout view. */ @@ -48,6 +55,24 @@ NS_ASSUME_NONNULL_BEGIN @optional +/** + If implemented, should provide margins to expand the rect the callout is presented from. + + These are used to determine positioning. Currently only the top and bottom properties of the return + value are used. For example, `{ .top = -50.0, .left = -10.0, .bottom = 0.0, .right = -10.0 }` indicates + a 50 point margin above the presentation origin rect (and 10 point margins to the left and the right) + in which the callout is assumed to be displayed. + + There are no assumed defaults for these margins, as they should be calculated from the callout that + is to be presented. For example, `SMCalloutView` generates the top margin from the callout height, but + the left and right margins from a minimum width that the callout should have. + + @param rect Rect that the callout is presented from. This should be the same as the one passed in + `-[MGLCalloutView presentCalloutFromRect:inView:constrainedToRect:animated:]` + @return `UIEdgeInsets` representing the margins. Values should be negative. + */ +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect NS_SWIFT_NAME(marginInsetsHintForPresentation(from:)); + /** A Boolean value indicating whether the callout view should be anchored to the corresponding annotation. You can adjust the callout view’s precise location by diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 52d28d871..2d566f26a 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -1200,17 +1200,32 @@ MGL_EXPORT IB_DESIGNABLE Assigning a new array to this property selects only the first annotation in the array. + + If the annotation is of type `MGLPointAnnotation` and is offscreen, the camera + will animate to bring the annotation and its callout just on screen. If you + need finer control, consider using `-selectAnnotation:animated:`. + + @note In versions prior to `4.0.0` if the annotation was offscreen it was not + selected. */ @property (nonatomic, copy) NS_ARRAY_OF(id ) *selectedAnnotations; /** - Selects an annotation and displays a callout view for it. + Selects an annotation and displays its callout view. - If the given annotation is not visible within the current viewport, this - method has no effect. + The `animated` parameter determines whether the map is panned to bring the + annotation on-screen, specifically: + + | `animated` parameter | Effect | + |------------------|--------| + | `NO` | The annotation is selected, and the callout is presented. However the map is not panned to bring the annotation or callout onscreen. The presentation of the callout is animated. | + | `YES` | The annotation is selected, and the callout is presented. If the annotation is offscreen *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought just onscreen. The annotation is *not* centered within the viewport. | @param annotation The annotation object to select. - @param animated If `YES`, the callout view is animated into position. + @param animated If `YES`, the annotation and callout view are animated on-screen. + + @note In versions prior to `4.0.0` selecting an offscreen annotation did not + change the camera. */ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6b47dc05f..3daea3336 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -144,6 +144,9 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const UIEdgeInsets MGLMapViewOffscreenAnnotationPadding = UIEdgeInsetsMake(-20.0f, -20.0f, -20.0f, -20.0f); + /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -272,7 +275,7 @@ public: CADisplayLink *_displayLink; BOOL _needsDisplayRefresh; - NSUInteger _changeDelimiterSuppressionDepth; + NSInteger _changeDelimiterSuppressionDepth; /// Center coordinate of the pinch gesture on the previous iteration of the gesture. CLLocationCoordinate2D _previousPinchCenterCoordinate; @@ -1625,7 +1628,7 @@ public: { CGPoint calloutPoint = [singleTap locationInView:self]; CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; - [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; + [self selectAnnotation:annotation moveOnscreen:YES animateSelection:YES calloutPositioningRect:positionRect]; } else if (self.selectedAnnotation) { @@ -4234,6 +4237,12 @@ public: }); } + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (id )selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -4274,20 +4283,16 @@ public: if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return; - // Select the annotation if it’s visible. - if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) - { - [self selectAnnotation:firstAnnotation animated:NO]; - } + [self selectAnnotation:firstAnnotation animated:YES]; } - (void)selectAnnotation:(id )annotation animated:(BOOL)animated { CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; - [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; + [self selectAnnotation:annotation moveOnscreen:animated animateSelection:YES calloutPositioningRect:positioningRect]; } -- (void)selectAnnotation:(id )annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +- (void)selectAnnotation:(id )annotation moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect { if ( ! annotation) return; @@ -4311,22 +4316,34 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - // Annotations represented by views use the view frame as the positioning rect. - calloutPositioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + + [annotationView setSelected:YES animated:animateSelection]; } } self.selectedAnnotation = annotation; + // Determine if we're allowed to move this offscreen annotation on screen, even though we've asked it to + if (moveOnscreen) { + moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection]; + } + + CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + UIView *calloutView = nil; + if ([annotation respondsToSelector:@selector(title)] && annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout - UIView *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; @@ -4384,13 +4401,51 @@ public: // set annotation delegate to handle taps on the callout view calloutView.delegate = self; - // present popup - [calloutView presentCalloutFromRect:calloutPositioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; + // If the callout view provides inset (outset) information, we can use it to expand our positioning + // rect, which we then use to help move the annotation on-screen if want need to. + if (moveOnscreen && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) { + UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect]; + expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins); + } } + if (moveOnscreen) + { + moveOnscreen = NO; + + // Need to consider the content insets. + CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(calloutPositioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOnscreen = YES; + } + else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOnscreen = YES; + } + + if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOnscreen = YES; + } + else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOnscreen = YES; + } + } + + // Remember, calloutView can be nil here. + [calloutView presentCalloutFromRect:calloutPositioningRect + inView:self.glView + constrainedToRect:constrainedRect + animated:animateSelection]; + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { @@ -4401,6 +4456,13 @@ public: { [self.delegate mapView:self didSelectAnnotationView:annotationView]; } + + if (moveOnscreen) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animateSelection]; + } } - (MGLCompactCalloutView *)calloutViewForAnnotation:(id )annotation @@ -4591,6 +4653,7 @@ public: animated:animated]; } + #pragma mark Annotation Image Delegate - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage diff --git a/platform/ios/test/MGLAnnotationViewTests.m b/platform/ios/test/MGLAnnotationViewTests.m index fc4f35a9e..c8b140b27 100644 --- a/platform/ios/test/MGLAnnotationViewTests.m +++ b/platform/ios/test/MGLAnnotationViewTests.m @@ -51,12 +51,15 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { } +- (void)presentCalloutFromRect:(CGRect)rect inView:(nonnull UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated {} + @end @interface MGLAnnotationViewTests : XCTestCase @property (nonatomic) XCTestExpectation *expectation; @property (nonatomic) MGLMapView *mapView; @property (nonatomic, weak) MGLAnnotationView *annotationView; +@property (nonatomic) NSInteger annotationSelectedCount; @end @implementation MGLAnnotationViewTests @@ -98,6 +101,61 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu XCTAssertNotNil(customAnnotationView); } +- (void)testSelectingOffscreenAnnotation +{ + // Partial test for https://github.com/mapbox/mapbox-gl-native/issues/9790 + + // This isn't quite the same as in updateAnnotationViews, but should be sufficient for this test. + MGLCoordinateBounds coordinateBounds = [_mapView convertRect:_mapView.bounds toCoordinateBoundsFromView:_mapView]; + + // -90 latitude is invalid. TBD. + BOOL anyOffscreen = NO; + NSInteger selectionCount = 0; + + for (NSInteger latitude = -89; latitude <= 90; latitude += 10) + { + for (NSInteger longitude = -180; longitude <= 180; longitude += 10) + { + MGLTestAnnotation *annotation = [[MGLTestAnnotation alloc] init]; + + annotation.coordinate = CLLocationCoordinate2DMake(latitude, longitude); + [_mapView addAnnotation:annotation]; + + if (!(MGLCoordinateInCoordinateBounds(annotation.coordinate, coordinateBounds))) + anyOffscreen = YES; + + XCTAssertNil(_mapView.selectedAnnotations.firstObject, @"There should be no selected annotation"); + + // First selection + [_mapView selectAnnotation:annotation animated:NO]; + selectionCount++; + + XCTAssert(_mapView.selectedAnnotations.count == 1, @"There should only be 1 selected annotation"); + XCTAssertEqualObjects(_mapView.selectedAnnotations.firstObject, annotation, @"The annotation should be selected"); + + // Deselect + [_mapView deselectAnnotation:annotation animated:NO]; + XCTAssert(_mapView.selectedAnnotations.count == 0, @"There should be no selected annotations"); + + // Second selection + _mapView.selectedAnnotations = @[annotation]; + selectionCount++; + + XCTAssert(_mapView.selectedAnnotations.count == 1, @"There should be 1 selected annotation"); + XCTAssertEqualObjects(_mapView.selectedAnnotations.firstObject, annotation, @"The annotation should be selected"); + + // Deselect + [_mapView deselectAnnotation:annotation animated:NO]; + XCTAssert(_mapView.selectedAnnotations.count == 0, @"There should be no selected annotations"); + } + } + + XCTAssert(anyOffscreen, @"At least one of these annotations should be offscreen"); + XCTAssertEqual(selectionCount, self.annotationSelectedCount, @"-mapView:didSelectAnnotation: should be called for each selection"); +} + +#pragma mark - MGLMapViewDelegate - + - (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id)annotation { MGLAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:MGLTestAnnotationReuseIdentifer]; @@ -117,4 +175,9 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu [_expectation fulfill]; } +- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id)annotation +{ + self.annotationSelectedCount++; +} + @end diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.h b/platform/ios/vendor/SMCalloutView/SMCalloutView.h index 0b1491362..5bb73d4c8 100755 --- a/platform/ios/vendor/SMCalloutView/SMCalloutView.h +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.h @@ -113,6 +113,18 @@ extern NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView; */ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; +/** + @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 rect (in the space of the given view). + + @param rect @c CGRect to present the view from + @param view view to 'constrain' the @c constrainedView to + @param constrainedRect Rect to constrain the callout to + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; + /** @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.m b/platform/ios/vendor/SMCalloutView/SMCalloutView.m index 9631ca036..a0049a3e2 100755 --- a/platform/ios/vendor/SMCalloutView/SMCalloutView.m +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.m @@ -247,6 +247,35 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom); } +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect { + + // form our subviews based on our content set so far + [self rebuildSubviews]; + + // size the callout to fit the width constraint as best as possible + CGFloat height = self.calloutHeight; + CGSize size = [self sizeThatFits:CGSizeMake(0.0f, height)]; + + // Without re-jigging presentCalloutFromRect, let's just make a best-guess with what we have + // right now. + CGFloat horizontalMargin = fmaxf(0, ceilf((CALLOUT_MIN_WIDTH-rect.size.width)/2)); + + UIEdgeInsets insets = { + .top = 0.0f, + .right = -horizontalMargin, + .bottom = 0.0f, + .left = -horizontalMargin + }; + + if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp) + insets.bottom -= size.height; + else + insets.top -= size.height; + + return insets; +} + + - (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]; } @@ -255,8 +284,18 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; [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 { + // figure out the constrained view's rect in our popup view's coordinate system + CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; + [self presentCalloutFromRect:rect inLayer:layer ofView:view constrainedToRect:constrainedRect animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToRect:constrainedRect 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 constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated { // Sanity check: dismiss this callout immediately if it's displayed somewhere if (self.layer.superlayer) [self dismissCalloutAnimated:NO]; @@ -265,8 +304,6 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; [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); diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 6de2a4e87..fd2258947 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -25,6 +25,8 @@ ### Annotations * Fix an issue where a wrong annotation may selected if annotations were set close together. ([#11438](https://github.com/mapbox/mapbox-gl-native/pull/11438)) +* The `MGLMapView.selectedAnnotations` property (backed by `-[MGLMapView setSelectedAnnotations:]`) now selects annotations that are off-screen. ([#9790](https://github.com/mapbox/mapbox-gl-native/issues/9790)) +* The `animated` parameter to `-[MGLMapView selectAnnotation:animated:]` now controls whether the annotation and its callout are brought on-screen. If `animated` is `NO` then the annotation is selected if offscreen, but the map is not panned. Currently only point annotations are supported.([#3249](https://github.com/mapbox/mapbox-gl-native/issues/3249)) ### Map snapshots diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib index 4cf8d8765..8f0aeaf69 100644 --- a/platform/macos/app/Base.lproj/MainMenu.xib +++ b/platform/macos/app/Base.lproj/MainMenu.xib @@ -545,7 +545,13 @@ - + + + + + + + @@ -664,7 +670,7 @@ CA - + @@ -740,7 +746,7 @@ CA - + @@ -748,11 +754,11 @@ CA - + - + @@ -891,7 +897,7 @@ CA - +