From ae5ed02b72c813877d21cce6bd6e1738f0edcf84 Mon Sep 17 00:00:00 2001 From: Fabian Guerra Date: Fri, 6 Sep 2019 20:09:17 -0700 Subject: [ios] Fix an issue that caused the ornaments ignore the contentInsets. Fixed an issue that caused ornaments ignore the contentInset. Added a new property automaticallyAdjustContentInset that has the same purpose as UIViewController. automaticallyAdjustsScrollViewInsets. This was changed due to the latter being deprecated. --- platform/ios/src/MGLMapView.h | 31 +++++--- platform/ios/src/MGLMapView.mm | 98 +++++++++++++++---------- platform/ios/test/MGLMapViewContentInsetTests.m | 33 +++++++-- 3 files changed, 111 insertions(+), 51 deletions(-) diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 017ba525c4..1ad7e8f594 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -286,6 +286,19 @@ MGL_EXPORT */ - (IBAction)reloadStyle:(nullable id)sender; +/** + A boolean value that indicates if whether the map view should automatically + adjust its content insets. + + When this property is set to `YES` the map automatically updates its + `contentInset` property to account for any area not covered by navigation bars, + tab bars, toolbars, and other ancestors that obscure the map view. + + The default value of this property is `YES`. + + */ +@property (assign) BOOL automaticallyAdjustContentInset; + /** A Boolean value indicating whether the map may display scale information. @@ -1309,9 +1322,9 @@ MGL_EXPORT frame from the viewport. For instance, if the only the top edge is inset, the map center is effectively shifted downward. - When the map view’s superview is an instance of `UIViewController` whose - `automaticallyAdjustsScrollViewInsets` property is `YES`, the value of this - property may be overridden at any time. + When the map view’s property `automaticallyAdjustContentInset` is set to `YES`, + the value of this property may be overridden at any time. To persist the value + set it to `NO`. Changing the value of this property updates the map view immediately. If you want to animate the change, use the `-setContentInset:animated:completionHandler:` @@ -1329,9 +1342,9 @@ MGL_EXPORT frame from the viewport. For instance, if the only the top edge is inset, the map center is effectively shifted downward. - When the map view’s superview is an instance of `UIViewController` whose - `automaticallyAdjustsScrollViewInsets` property is `YES`, the value of this - property may be overridden at any time. + When the map view’s property `automaticallyAdjustContentInset` is set to `YES`, + the value of this property may be overridden at any time. To persist the value + set it to `NO`. To specify a completion handler to execute after the animation finishes, use the `-setContentInset:animated:completionHandler:` method. @@ -1354,9 +1367,9 @@ MGL_EXPORT frame from the viewport. For instance, if the only the top edge is inset, the map center is effectively shifted downward. - When the map view’s superview is an instance of `UIViewController` whose - `automaticallyAdjustsScrollViewInsets` property is `YES`, the value of this - property may be overridden at any time. + When the map view’s property `automaticallyAdjustContentInset` is set to `YES`, + the value of this property may be overridden at any time. To persist the value + set it to `NO`. @param contentInset The new values to inset the content by. @param animated Specify `YES` if you want the map view to animate the change to diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 62b943fd3d..6f0257a0d8 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -274,6 +274,10 @@ public: /// Tilt gesture recognizer helper @property (nonatomic, assign) CGPoint dragGestureMiddlePoint; +/// This property is used to keep track of the view's safe edge insets +/// and calculate the ornament's position +@property (nonatomic, assign) UIEdgeInsets safeMapViewContentInsets; + - (mbgl::Map &)mbglMap; @end @@ -518,6 +522,8 @@ public: _annotationViewReuseQueueByIdentifier = [NSMutableDictionary dictionary]; _selectedAnnotationTag = MGLAnnotationTagNotFound; _annotationsNearbyLastTap = {}; + + _automaticallyAdjustContentInset = YES; // setup logo // @@ -832,23 +838,31 @@ public: size:(CGSize)size margins:(CGPoint)margins { NSMutableArray *updatedConstraints = [NSMutableArray array]; + UIEdgeInsets inset = UIEdgeInsetsZero; + + if (! self.automaticallyAdjustContentInset) { + inset = UIEdgeInsetsMake(self.contentInset.top - self.safeMapViewContentInsets.top, + self.contentInset.left - self.safeMapViewContentInsets.left, + self.contentInset.bottom - self.safeMapViewContentInsets.bottom, + self.contentInset.right - self.safeMapViewContentInsets.right); + } switch (position) { case MGLOrnamentPositionTopLeft: - [updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y]]; - [updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x]]; + [updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y + inset.top]]; + [updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x + inset.left]]; break; case MGLOrnamentPositionTopRight: - [updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y]]; - [updatedConstraints addObject:[self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x]]; + [updatedConstraints addObject:[view.topAnchor constraintEqualToAnchor:self.mgl_safeTopAnchor constant:margins.y + inset.top]]; + [updatedConstraints addObject:[self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x + inset.right]]; break; case MGLOrnamentPositionBottomLeft: - [updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y]]; - [updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x]]; + [updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y + inset.bottom]]; + [updatedConstraints addObject:[view.leadingAnchor constraintEqualToAnchor:self.mgl_safeLeadingAnchor constant:margins.x + inset.left]]; break; case MGLOrnamentPositionBottomRight: - [updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y]]; - [updatedConstraints addObject: [self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x]]; + [updatedConstraints addObject:[self.mgl_safeBottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:margins.y + inset.bottom]]; + [updatedConstraints addObject: [self.mgl_safeTrailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margins.x + inset.right]]; break; } @@ -967,40 +981,44 @@ public: /// Updates `contentInset` to reflect the current window geometry. - (void)adjustContentInset { - // We could crawl all the way up the responder chain using - // -viewControllerForLayoutGuides, but an intervening view means that any - // manual contentInset should not be overridden; something other than the - // top and bottom bars may be influencing the manual inset. - UIViewController *viewController; - if ([self.nextResponder isKindOfClass:[UIViewController class]]) - { - // This map view is the content view of a view controller. - viewController = (UIViewController *)self.nextResponder; - } - else if ([self.superview.nextResponder isKindOfClass:[UIViewController class]]) + UIEdgeInsets adjustedContentInsets = UIEdgeInsetsZero; + + if (@available(iOS 11.0, *)) { - // This map view is an immediate child of a view controller’s content view. - viewController = (UIViewController *)self.superview.nextResponder; - } + adjustedContentInsets = self.safeAreaInsets; + + } else { + // We could crawl all the way up the responder chain using + // -viewControllerForLayoutGuides, but an intervening view means that any + // manual contentInset should not be overridden; something other than the + // top and bottom bars may be influencing the manual inset. + UIViewController *viewController; + if ([self.nextResponder isKindOfClass:[UIViewController class]]) + { + // This map view is the content view of a view controller. + viewController = (UIViewController *)self.nextResponder; + } + else if ([self.superview.nextResponder isKindOfClass:[UIViewController class]]) + { + // This map view is an immediate child of a view controller’s content view. + viewController = (UIViewController *)self.superview.nextResponder; + } + + adjustedContentInsets.top = viewController.topLayoutGuide.length; + CGFloat bottomPoint = CGRectGetMaxY(viewController.view.bounds) - + (CGRectGetMaxY(viewController.view.bounds) + - viewController.bottomLayoutGuide.length); + adjustedContentInsets.bottom = bottomPoint; - if ( ! viewController.automaticallyAdjustsScrollViewInsets) + } + + self.safeMapViewContentInsets = adjustedContentInsets; + if ( ! self.automaticallyAdjustContentInset) { return; } - - UIEdgeInsets contentInset = UIEdgeInsetsZero; - CGPoint topPoint = CGPointMake(0, viewController.topLayoutGuide.length); - contentInset.top = [self convertPoint:topPoint fromView:viewController.view].y; - CGPoint bottomPoint = CGPointMake(0, CGRectGetMaxY(viewController.view.bounds) - - viewController.bottomLayoutGuide.length); - contentInset.bottom = (CGRectGetMaxY(self.bounds) - - [self convertPoint:bottomPoint fromView:viewController.view].y); - - // Negative insets are invalid, replace with 0. - contentInset.top = fmaxf(contentInset.top, 0); - contentInset.bottom = fmaxf(contentInset.bottom, 0); - - self.contentInset = contentInset; + + self.contentInset = adjustedContentInsets; } - (void)setContentInset:(UIEdgeInsets)contentInset @@ -1015,6 +1033,12 @@ public: - (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion { + // makes sure the insets don't have negative values that could hide the ornaments + // thus violating our ToS + contentInset = UIEdgeInsetsMake(fmaxf(contentInset.top, 0), + fmaxf(contentInset.left, 0), + fmaxf(contentInset.bottom, 0), + fmaxf(contentInset.right, 0)); MGLLogDebug(@"Setting contentInset: %@ animated:", NSStringFromUIEdgeInsets(contentInset), MGLStringFromBOOL(animated)); if (UIEdgeInsetsEqualToEdgeInsets(contentInset, self.contentInset)) { diff --git a/platform/ios/test/MGLMapViewContentInsetTests.m b/platform/ios/test/MGLMapViewContentInsetTests.m index aa6f57cc0a..6ec669d847 100644 --- a/platform/ios/test/MGLMapViewContentInsetTests.m +++ b/platform/ios/test/MGLMapViewContentInsetTests.m @@ -5,6 +5,7 @@ @property (nonatomic) MGLMapView *mapView; @property (nonatomic) UIWindow *window; +@property (nonatomic) UIViewController *viewController; @property (nonatomic) XCTestExpectation *styleLoadingExpectation; @property (assign) CGRect screenBounds; @@ -22,11 +23,11 @@ self.mapView.zoomLevel = 16; self.mapView.delegate = self; - - UIView *view = [[UIView alloc] initWithFrame:self.screenBounds]; - [view addSubview:self.mapView]; + self.viewController = [[UIViewController alloc] init]; + self.viewController.view = [[UIView alloc] initWithFrame:self.screenBounds]; + [self.viewController.view addSubview:self.mapView]; self.window = [[UIWindow alloc] initWithFrame:self.screenBounds]; - [self.window addSubview:view]; + [self.window addSubview:self.viewController.view]; [self.window makeKeyAndVisible]; if (!self.mapView.style) { @@ -99,7 +100,7 @@ XCTAssertTrue(CGPointEqualToPoint(attributionView.frame.origin, expectedAttributionOrigin)); UIEdgeInsets insets = UIEdgeInsetsMake(15, 10, 20, 5); - self.mapView.automaticallyAdjustContentInset = YES; + self.mapView.automaticallyAdjustContentInset = NO; self.mapView.contentInset = insets; [self.mapView setNeedsLayout]; @@ -121,6 +122,28 @@ expectedAttributionOrigin = CGPointMake(x, y); XCTAssertTrue(CGPointEqualToPoint(attributionView.frame.origin, expectedAttributionOrigin)); + // tests that passing negative values result in a 0 inset value + insets = UIEdgeInsetsMake(-100, -100, -100, -100); + self.mapView.contentInset = insets; + + [self.mapView setNeedsLayout]; + [self.mapView layoutIfNeeded]; + + expectedScaleBarOrigin = CGPointMake(margin, margin); + XCTAssertTrue(CGPointEqualToPoint(scaleBar.frame.origin, expectedScaleBarOrigin)); + + x = self.screenBounds.size.width - compassView.bounds.size.width - margin; + expectedCompassOrigin = CGPointMake(x, margin); + XCTAssertTrue(CGPointEqualToPoint(compassView.frame.origin, expectedCompassOrigin)); + + y = self.screenBounds.size.height - logoView.bounds.size.height - margin; + expectedLogoOrigin = CGPointMake(margin, y); + XCTAssertTrue(CGPointEqualToPoint(logoView.frame.origin, expectedLogoOrigin)); + + x = self.screenBounds.size.width - attributionView.bounds.size.width - margin; + y = self.screenBounds.size.height - attributionView.bounds.size.height - margin; + expectedAttributionOrigin = CGPointMake(x, y); + XCTAssertTrue(CGPointEqualToPoint(attributionView.frame.origin, expectedAttributionOrigin)); } @end -- cgit v1.2.1