[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+
This commit is contained in:
Jesse Bounds
2017-09-14 13:51:05 -07:00
committed by GitHub
parent 254477ad01
commit a6224abc33
3 changed files with 226 additions and 96 deletions

View File

@@ -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<MGLMapViewDelegate>)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