From a6224abc33f599f0352682c411b3831b896fb97c Mon Sep 17 00:00:00 2001 From: Jesse Bounds Date: Thu, 14 Sep 2017 13:51:05 -0700 Subject: [ios] Use constraints to manage ornament view placement (again) (#9995) This reverts the manual layout triggered by KVO approach in favor of a constraints based approach that the KVO method had previously replaced. Although the manual layout approach worked well in most cases and was cleaner in that it did not manipulate any containing views in the app space, using KVO on UIViewControllers has proven to be dangerous. This also updates the gitshas for KIF and OHHTTPStubs to unbreak UI tests that verify these related changes are working. This also adds an ornament constraint system for iOS 11+ --- platform/ios/src/MGLMapView.mm | 318 ++++++++++++++++++++++++++++------------ platform/ios/uitest/KIF | 2 +- platform/ios/uitest/OHHTTPStubs | 2 +- 3 files changed, 226 insertions(+), 96 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index e056f09d22..08fec00520 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -136,9 +136,6 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); -// Context for KVO observing UILayoutGuides. -static void * MGLLayoutGuidesUpdatedContext = &MGLLayoutGuidesUpdatedContext; - /// Unique identifier representing a single annotation in mbgl. typedef uint32_t MGLAnnotationTag; @@ -240,10 +237,14 @@ public: @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @property (nonatomic) UIImageView *glSnapshotView; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *scaleBarConstraints; @property (nonatomic, readwrite) MGLScaleBar *scaleBar; @property (nonatomic, readwrite) UIImageView *compassView; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *compassViewConstraints; @property (nonatomic, readwrite) UIImageView *logoView; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *logoViewConstraints; @property (nonatomic, readwrite) UIButton *attributionButton; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *attributionButtonConstraints; @property (nonatomic, readwrite) MGLStyle *style; @property (nonatomic) UITapGestureRecognizer *singleTapGestureRecognizer; @property (nonatomic) UITapGestureRecognizer *doubleTap; @@ -298,8 +299,6 @@ public: NSDate *_userLocationAnimationCompletionDate; /// True if a willChange notification has been issued for shape annotation layers and a didChange notification is pending. BOOL _isChangingAnnotationLayers; - BOOL _isObservingTopLayoutGuide; - BOOL _isObservingBottomLayoutGuide; BOOL _isWaitingForRedundantReachableNotification; BOOL _isTargetingInterfaceBuilder; @@ -478,10 +477,12 @@ public: _logoView = [[UIImageView alloc] initWithImage:logo]; _logoView.accessibilityTraits = UIAccessibilityTraitStaticText; _logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label"); + _logoView.translatesAutoresizingMaskIntoConstraints = NO; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if ([_logoView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _logoView.accessibilityIgnoresInvertColors = YES; } #endif [self addSubview:_logoView]; + _logoViewConstraints = [NSMutableArray array]; // setup attribution // @@ -492,7 +493,9 @@ public: if ([_attributionButton respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _attributionButton.accessibilityIgnoresInvertColors = YES; } #endif [_attributionButton addTarget:self action:@selector(showAttribution) forControlEvents:UIControlEventTouchUpInside]; + _attributionButton.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_attributionButton]; + _attributionButtonConstraints = [NSMutableArray array]; [_attributionButton addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionNew context:NULL]; // setup compass @@ -504,18 +507,22 @@ public: _compassView.accessibilityTraits = UIAccessibilityTraitButton; _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); _compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); + _compassView.translatesAutoresizingMaskIntoConstraints = NO; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if ([_compassView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _compassView.accessibilityIgnoresInvertColors = YES; } #endif [self addSubview:_compassView]; + _compassViewConstraints = [NSMutableArray array]; // setup scale control // _scaleBar = [[MGLScaleBar alloc] init]; + _scaleBar.translatesAutoresizingMaskIntoConstraints = NO; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if ([_scaleBar respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _scaleBar.accessibilityIgnoresInvertColors = YES; } #endif [self addSubview:_scaleBar]; + _scaleBarConstraints = [NSMutableArray array]; // setup interaction // @@ -678,48 +685,6 @@ public: _isWaitingForRedundantReachableNotification = NO; } -- (void)willMoveToWindow:(UIWindow *)newWindow -{ - [super willMoveToWindow:newWindow]; - - if (newWindow) { - [self addLayoutGuideObserversIfNeeded]; - } else { - [self removeLayoutGuideObserversIfNeeded]; - } -} - -- (void)addLayoutGuideObserversIfNeeded -{ - UIViewController *viewController = self.viewControllerForLayoutGuides; - BOOL useLayoutGuides = viewController.view && viewController.automaticallyAdjustsScrollViewInsets; - - if (useLayoutGuides && viewController.topLayoutGuide && !_isObservingTopLayoutGuide) { - [(NSObject *)viewController.topLayoutGuide addObserver:self forKeyPath:@"bounds" options:0 context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingTopLayoutGuide = YES; - } - - if (useLayoutGuides && viewController.bottomLayoutGuide && !_isObservingBottomLayoutGuide) { - [(NSObject *)viewController.bottomLayoutGuide addObserver:self forKeyPath:@"bounds" options:0 context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingBottomLayoutGuide = YES; - } -} - -- (void)removeLayoutGuideObserversIfNeeded -{ - UIViewController *viewController = self.viewControllerForLayoutGuides; - - if (_isObservingTopLayoutGuide) { - [(NSObject *)viewController.topLayoutGuide removeObserver:self forKeyPath:@"bounds" context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingTopLayoutGuide = NO; - } - - if (_isObservingBottomLayoutGuide) { - [(NSObject *)viewController.bottomLayoutGuide removeObserver:self forKeyPath:@"bounds" context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingBottomLayoutGuide = NO; - } -} - - (void)dealloc { [_reachability stopNotifier]; @@ -728,8 +693,6 @@ public: [[NSNotificationCenter defaultCenter] removeObserver:self]; [_attributionButton removeObserver:self forKeyPath:@"hidden"]; - [self removeLayoutGuideObserversIfNeeded]; - // Removing the annotations unregisters any outstanding KVO observers. NSArray *annotations = self.annotations; if (annotations) @@ -755,6 +718,18 @@ public: { [EAGLContext setCurrentContext:nil]; } + + [self.compassViewConstraints removeAllObjects]; + self.compassViewConstraints = nil; + + [self.scaleBarConstraints removeAllObjects]; + self.scaleBarConstraints = nil; + + [self.logoViewConstraints removeAllObjects]; + self.logoViewConstraints = nil; + + [self.attributionButtonConstraints removeAllObjects]; + self.attributionButtonConstraints = nil; } - (void)setDelegate:(nullable id)delegate @@ -781,6 +756,7 @@ public: - (void)setFrame:(CGRect)frame { [super setFrame:frame]; + if ( ! CGRectEqualToRect(frame, self.frame)) { [self validateTileCacheSize]; @@ -790,6 +766,7 @@ public: - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; + if ( ! CGRectEqualToRect(bounds, self.bounds)) { [self validateTileCacheSize]; @@ -837,15 +814,207 @@ public: return nil; } +- (void)updateConstraintsPreiOS11 { + // If we have a view controller reference and its automaticallyAdjustsScrollViewInsets + // is set to YES, use its view as the parent for constraints. -[MGLMapView adjustContentInset] + // already take top and bottom layout guides into account. If we don't have a reference, apply + // constraints against ourself to maintain placement of the subviews. + // + UIViewController *viewController = self.viewControllerForLayoutGuides; + BOOL useLayoutGuides = viewController.view && viewController.automaticallyAdjustsScrollViewInsets; + UIView *containerView = useLayoutGuides ? viewController.view : self; + + // compass view + // + [containerView removeConstraints:self.compassViewConstraints]; + [self.compassViewConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.compassView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:viewController.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + } + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.compassView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.compassView + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:5.0 + self.contentInset.right]]; + + [containerView addConstraints:self.compassViewConstraints]; + + // scale bar view + // + [containerView removeConstraints:self.scaleBarConstraints]; + [self.scaleBarConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:viewController.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + } + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:8.0 + self.contentInset.left]]; + + [containerView addConstraints:self.scaleBarConstraints]; + + // logo view + // + [containerView removeConstraints:self.logoViewConstraints]; + [self.logoViewConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:viewController.bottomLayoutGuide + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.logoView + attribute:NSLayoutAttributeBaseline + multiplier:1.0 + constant:8.0 + self.contentInset.bottom]]; + } + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.logoView + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.logoView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:8.0 + self.contentInset.left]]; + [containerView addConstraints:self.logoViewConstraints]; + + // attribution button + // + [containerView removeConstraints:self.attributionButtonConstraints]; + [self.attributionButtonConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:viewController.bottomLayoutGuide + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + } + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:8 + self.contentInset.right]]; + [containerView addConstraints:self.attributionButtonConstraints]; +} + - (void)updateConstraints { - [super updateConstraints]; - // If we have a view controller reference and its automaticallyAdjustsScrollViewInsets - // is set to YES, -[MGLMapView adjustContentInset] takes top and bottom layout - // guides into account. To get notified about changes to the layout guides, - // we need to observe their bounds and re-layout accordingly. - [self addLayoutGuideObserversIfNeeded]; +// If compiling with the iOS 11+ SDK +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // If safeAreaLayoutGuide API exists + if ( [self respondsToSelector:@selector(safeAreaLayoutGuide)] ) { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + UILayoutGuide *safeAreaLayoutGuide = self.safeAreaLayoutGuide; +#pragma clang diagnostic pop + // compass view + [self removeConstraints:self.compassViewConstraints]; + [self.compassViewConstraints removeAllObjects]; + [self.compassViewConstraints addObject:[self.compassView.topAnchor constraintEqualToAnchor:safeAreaLayoutGuide.topAnchor + constant:5.0 + self.contentInset.top]]; + [self.compassViewConstraints addObject:[safeAreaLayoutGuide.rightAnchor constraintEqualToAnchor:self.compassView.rightAnchor + constant:8.0 + self.contentInset.right]]; + [self addConstraints:self.compassViewConstraints]; + + // scale bar view + [self removeConstraints:self.scaleBarConstraints]; + [self.scaleBarConstraints removeAllObjects]; + [self.scaleBarConstraints addObject:[self.scaleBar.topAnchor constraintEqualToAnchor:safeAreaLayoutGuide.topAnchor + constant:5.0 + self.contentInset.top]]; + [self.scaleBarConstraints addObject:[self.scaleBar.leftAnchor constraintEqualToAnchor:safeAreaLayoutGuide.leftAnchor + constant:8.0 + self.contentInset.left]]; + [self addConstraints:self.scaleBarConstraints]; + + // logo view + [self removeConstraints:self.logoViewConstraints]; + [self.logoViewConstraints removeAllObjects]; + [self.logoViewConstraints addObject:[safeAreaLayoutGuide.bottomAnchor constraintEqualToAnchor:self.logoView.bottomAnchor + constant:8.0 + self.contentInset.bottom]]; + [self.logoViewConstraints addObject:[self.logoView.leftAnchor constraintEqualToAnchor:safeAreaLayoutGuide.leftAnchor + constant:8.0 + self.contentInset.left]]; + [self addConstraints:self.logoViewConstraints]; + + // attribution button + [self removeConstraints:self.attributionButtonConstraints]; + [self.attributionButtonConstraints removeAllObjects]; + [self.attributionButtonConstraints addObject:[safeAreaLayoutGuide.bottomAnchor constraintEqualToAnchor:self.attributionButton.bottomAnchor + constant:8.0 + self.contentInset.bottom]]; + [self.attributionButtonConstraints addObject:[safeAreaLayoutGuide.rightAnchor constraintEqualToAnchor:self.attributionButton.rightAnchor + constant:8.0 + self.contentInset.right]]; + [self addConstraints:self.attributionButtonConstraints]; + } else { + [self updateConstraintsPreiOS11]; + } +#else + [self updateConstraintsPreiOS11]; +#endif + + [super updateConstraints]; } - (BOOL)isOpaque @@ -878,8 +1047,6 @@ public: [super layoutSubviews]; [self adjustContentInset]; - - [self layoutOrnaments]; if (!_isTargetingInterfaceBuilder) { _mbglMap->setSize([self size]); @@ -898,39 +1065,6 @@ public: [self updateUserLocationAnnotationView]; } -- (void)layoutOrnaments -{ - // scale bar - self.scaleBar.frame = { - self.contentInset.left+8, - self.contentInset.top+5, - CGRectGetWidth(self.scaleBar.frame), - CGRectGetHeight(self.scaleBar.frame) - }; - - // compass - self.compassView.center = { - .x = CGRectGetWidth(self.bounds)-CGRectGetMidX(self.compassView.bounds)-self.contentInset.right-5, - .y = CGRectGetMidY(self.compassView.bounds)+self.contentInset.top+5 - }; - - // logo bug - self.logoView.frame = { - self.contentInset.left+8, - CGRectGetHeight(self.bounds)-8-self.contentInset.bottom-CGRectGetHeight(self.logoView.bounds), - CGRectGetWidth(self.logoView.bounds), - CGRectGetHeight(self.logoView.bounds) - }; - - // attribution - self.attributionButton.frame = { - CGRectGetWidth(self.bounds)-CGRectGetWidth(self.attributionButton.bounds)-self.contentInset.right-8, - CGRectGetHeight(self.bounds)-CGRectGetHeight(self.attributionButton.bounds)-self.contentInset.bottom-8, - CGRectGetWidth(self.attributionButton.bounds), - CGRectGetHeight(self.attributionButton.bounds) - }; -} - /// Updates `contentInset` to reflect the current window geometry. - (void)adjustContentInset { @@ -2090,10 +2224,6 @@ public: [self updateCalloutView]; } } - else if (context == MGLLayoutGuidesUpdatedContext && [keyPath isEqualToString:@"bounds"]) - { - [self setNeedsLayout]; - } } + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomEnabled diff --git a/platform/ios/uitest/KIF b/platform/ios/uitest/KIF index 973a4cb653..c40b1048a6 160000 --- a/platform/ios/uitest/KIF +++ b/platform/ios/uitest/KIF @@ -1 +1 @@ -Subproject commit 973a4cb653b54c3e8b2c0681f4097568ff0ac345 +Subproject commit c40b1048a6f35c6fd90376846a1a933844516b39 diff --git a/platform/ios/uitest/OHHTTPStubs b/platform/ios/uitest/OHHTTPStubs index deed01a159..4dc6f36375 160000 --- a/platform/ios/uitest/OHHTTPStubs +++ b/platform/ios/uitest/OHHTTPStubs @@ -1 +1 @@ -Subproject commit deed01a1592210a4c37f3f5c5f2b32fe0e41c605 +Subproject commit 4dc6f36375f78c0b3cfe58d90bb8a4e21df5196e -- cgit v1.2.1