1/** 2 * Copyright (c) 2015-present, Facebook, Inc. 3 * All rights reserved. 4 * 5 * This source code is licensed under the BSD-style license found in the 6 * LICENSE file in the root directory of this source tree. An additional grant 7 * of patent rights can be found in the PATENTS file in the same directory. 8 */ 9 10#import "AIRMap.h" 11 12#import <React/RCTEventDispatcher.h> 13#import <React/UIView+React.h> 14#import "AIRMapMarker.h" 15#import "AIRMapPolyline.h" 16#import "AIRMapPolygon.h" 17#import "AIRMapCircle.h" 18#import <QuartzCore/QuartzCore.h> 19#import "AIRMapUrlTile.h" 20#import "AIRMapWMSTile.h" 21#import "AIRMapLocalTile.h" 22#import "AIRMapOverlay.h" 23 24const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1; 25const CGFloat AIRMapZoomBoundBuffer = 0.01; 26const NSInteger AIRMapMaxZoomLevel = 20; 27 28 29@interface MKMapView (UIGestureRecognizer) 30 31// this tells the compiler that MKMapView actually implements this method 32- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch; 33 34@end 35 36@interface AIRMap () 37 38@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; 39@property (nonatomic, assign) NSNumber *shouldZoomEnabled; 40@property (nonatomic, assign) NSNumber *shouldScrollEnabled; 41 42- (void)updateScrollEnabled; 43- (void)updateZoomEnabled; 44 45@end 46 47@implementation AIRMap 48{ 49 UIView *_legalLabel; 50 BOOL _initialRegionSet; 51 BOOL _initialCameraSet; 52 53 // Array to manually track RN subviews 54 // 55 // AIRMap implicitly creates subviews that aren't regular RN children 56 // (SMCalloutView injects an overlay subview), which otherwise confuses RN 57 // during component re-renders: 58 // https://github.com/facebook/react-native/blob/v0.16.0/React/Modules/RCTUIManager.m#L657 59 // 60 // Implementation based on RCTTextField, another component with indirect children 61 // https://github.com/facebook/react-native/blob/v0.16.0/Libraries/Text/RCTTextField.m#L20 62 NSMutableArray<UIView *> *_reactSubviews; 63} 64 65- (instancetype)init 66{ 67 if ((self = [super init])) { 68 _hasStartedRendering = NO; 69 _reactSubviews = [NSMutableArray new]; 70 71 // Find Apple link label 72 for (UIView *subview in self.subviews) { 73 if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) { 74 // This check is super hacky, but the whole premise of moving around 75 // Apple's internal subviews is super hacky 76 _legalLabel = subview; 77 break; 78 } 79 } 80 81 // 3rd-party callout view for MapKit that has more options than the built-in. It's painstakingly built to 82 // be identical to the built-in callout view (which has a private API) 83 self.calloutView = [SMCalloutView platformCalloutView]; 84 self.calloutView.delegate = self; 85 86 self.minZoomLevel = 0; 87 self.maxZoomLevel = AIRMapMaxZoomLevel; 88 self.compassOffset = CGPointMake(0, 0); 89 } 90 return self; 91} 92 93-(void)addSubview:(UIView *)view { 94 if([view isKindOfClass:[AIRMapMarker class]]) { 95 [self addAnnotation:(id <MKAnnotation>)view]; 96 } else { 97 [super addSubview:view]; 98 } 99} 100 101#pragma clang diagnostic push 102#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" 103- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex { 104 // Our desired API is to pass up markers/overlays as children to the mapview component. 105 // This is where we intercept them and do the appropriate underlying mapview action. 106 if ([subview isKindOfClass:[AIRMapMarker class]]) { 107 [self addAnnotation:(id <MKAnnotation>) subview]; 108 } else if ([subview isKindOfClass:[AIRMapPolyline class]]) { 109 ((AIRMapPolyline *)subview).map = self; 110 [self addOverlay:(id<MKOverlay>)subview]; 111 } else if ([subview isKindOfClass:[AIRMapPolygon class]]) { 112 ((AIRMapPolygon *)subview).map = self; 113 [self addOverlay:(id<MKOverlay>)subview]; 114 } else if ([subview isKindOfClass:[AIRMapCircle class]]) { 115 ((AIRMapCircle *)subview).map = self; 116 [self addOverlay:(id<MKOverlay>)subview]; 117 } else if ([subview isKindOfClass:[AIRMapUrlTile class]]) { 118 ((AIRMapUrlTile *)subview).map = self; 119 [self addOverlay:(id<MKOverlay>)subview]; 120 }else if ([subview isKindOfClass:[AIRMapWMSTile class]]) { 121 ((AIRMapWMSTile *)subview).map = self; 122 [self addOverlay:(id<MKOverlay>)subview]; 123 } else if ([subview isKindOfClass:[AIRMapLocalTile class]]) { 124 ((AIRMapLocalTile *)subview).map = self; 125 [self addOverlay:(id<MKOverlay>)subview]; 126 } else if ([subview isKindOfClass:[AIRMapOverlay class]]) { 127 ((AIRMapOverlay *)subview).map = self; 128 [self addOverlay:(id<MKOverlay>)subview]; 129 } else { 130 NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews]; 131 for (int i = 0; i < childSubviews.count; i++) { 132 [self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex]; 133 } 134 } 135 [_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex]; 136} 137#pragma clang diagnostic pop 138 139#pragma clang diagnostic push 140#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" 141- (void)removeReactSubview:(id<RCTComponent>)subview { 142 // similarly, when the children are being removed we have to do the appropriate 143 // underlying mapview action here. 144 if ([subview isKindOfClass:[AIRMapMarker class]]) { 145 [self removeAnnotation:(id<MKAnnotation>)subview]; 146 } else if ([subview isKindOfClass:[AIRMapPolyline class]]) { 147 [self removeOverlay:(id <MKOverlay>) subview]; 148 } else if ([subview isKindOfClass:[AIRMapPolygon class]]) { 149 [self removeOverlay:(id <MKOverlay>) subview]; 150 } else if ([subview isKindOfClass:[AIRMapCircle class]]) { 151 [self removeOverlay:(id <MKOverlay>) subview]; 152 } else if ([subview isKindOfClass:[AIRMapUrlTile class]]) { 153 [self removeOverlay:(id <MKOverlay>) subview]; 154 } else if ([subview isKindOfClass:[AIRMapWMSTile class]]) { 155 [self removeOverlay:(id <MKOverlay>) subview]; 156 } else if ([subview isKindOfClass:[AIRMapLocalTile class]]) { 157 [self removeOverlay:(id <MKOverlay>) subview]; 158 } else if ([subview isKindOfClass:[AIRMapOverlay class]]) { 159 [self removeOverlay:(id <MKOverlay>) subview]; 160 } else { 161 NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews]; 162 for (int i = 0; i < childSubviews.count; i++) { 163 [self removeReactSubview:(UIView *)childSubviews[i]]; 164 } 165 } 166 [_reactSubviews removeObject:(UIView *)subview]; 167} 168#pragma clang diagnostic pop 169 170#pragma clang diagnostic push 171#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" 172- (NSArray<id<RCTComponent>> *)reactSubviews { 173 return _reactSubviews; 174} 175#pragma clang diagnostic pop 176 177#pragma mark Utils 178 179- (NSArray*) markers { 180 NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 181 AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject; 182 return [marker isKindOfClass:[AIRMapMarker class]]; 183 }]; 184 NSArray *filteredMarkers = [self.annotations filteredArrayUsingPredicate:filterMarkers]; 185 return filteredMarkers; 186} 187 188- (AIRMapMarker*) markerForCallout:(AIRMapCallout*)callout { 189 AIRMapMarker* marker = nil; 190 NSArray* markers = [self markers]; 191 for (AIRMapMarker* mrk in markers) { 192 if (mrk.calloutView == callout) { 193 marker = mrk; 194 break; 195 } 196 } 197 return marker; 198} 199 200- (CGRect) frameForMarker:(AIRMapMarker*) mrkAnn { 201 MKAnnotationView* mrkView = [self viewForAnnotation: mrkAnn]; 202 CGRect mrkFrame = mrkView.frame; 203 return mrkFrame; 204} 205 206- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible { 207 NSMutableDictionary* markersFrames = [NSMutableDictionary new]; 208 for (AIRMapMarker* mrkAnn in self.markers) { 209 CGRect frame = [self frameForMarker:mrkAnn]; 210 CGPoint point = [self convertCoordinate:mrkAnn.coordinate toPointToView:self]; 211 NSDictionary* frameDict = @{ 212 @"x": @(frame.origin.x), 213 @"y": @(frame.origin.y), 214 @"width": @(frame.size.width), 215 @"height": @(frame.size.height) 216 }; 217 NSDictionary* pointDict = @{ 218 @"x": @(point.x), 219 @"y": @(point.y) 220 }; 221 NSString* k = mrkAnn.identifier; 222 BOOL isVisible = CGRectIntersectsRect(self.bounds, frame); 223 if (k != nil && (!onlyVisible || isVisible)) { 224 [markersFrames setObject:@{ @"frame": frameDict, @"point": pointDict } forKey:k]; 225 } 226 } 227 return markersFrames; 228} 229 230- (AIRMapMarker*) markerAtPoint:(CGPoint)point { 231 AIRMapMarker* mrk = nil; 232 for (AIRMapMarker* mrkAnn in self.markers) { 233 CGRect frame = [self frameForMarker:mrkAnn]; 234 if (CGRectContainsPoint(frame, point)) { 235 mrk = mrkAnn; 236 break; 237 } 238 } 239 return mrk; 240} 241 242#pragma mark Overrides for Callout behavior 243 244// override UIGestureRecognizer's delegate method so we can prevent MKMapView's recognizer from firing 245// when we interact with UIControl subclasses inside our callout view. 246- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 247 if ([touch.view isDescendantOfView:self.calloutView]) 248 return NO; 249 else 250 return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch]; 251} 252 253 254// Allow touches to be sent to our calloutview. 255// See this for some discussion of why we need to override this: https://github.com/nfarina/calloutview/pull/9 256- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { 257 258 CGPoint touchPoint = [self.calloutView convertPoint:point fromView:self]; 259 UIView *touchedView = [self.calloutView hitTest:touchPoint withEvent:event]; 260 261 if (touchedView) { 262 UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject]; 263 AIRMapCalloutSubview* calloutSubview = nil; 264 AIRMapCallout* callout = nil; 265 AIRMapMarker* marker = nil; 266 267 UIView* tmp = touchedView; 268 while (tmp && tmp != win && tmp != self.calloutView) { 269 if ([tmp respondsToSelector:@selector(onPress)]) { 270 calloutSubview = (AIRMapCalloutSubview*) tmp; 271 } 272 if ([tmp isKindOfClass:[AIRMapCallout class]]) { 273 callout = (AIRMapCallout*) tmp; 274 break; 275 } 276 tmp = tmp.superview; 277 } 278 279 if (callout) { 280 marker = [self markerForCallout:callout]; 281 if (marker) { 282 CGPoint touchPointReal = [marker.calloutView convertPoint:point fromView:self]; 283 if (![callout isPointInside:touchPointReal]) { 284 return [super hitTest:point withEvent:event]; 285 } 286 } 287 } 288 289 return calloutSubview ? calloutSubview : touchedView; 290 } 291 292 return [super hitTest:point withEvent:event]; 293} 294 295#pragma mark SMCalloutViewDelegate 296 297- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset { 298 299 // When the callout is being asked to present in a way where it or its target will be partially offscreen, it asks us 300 // if we'd like to reposition our surface first so the callout is completely visible. Here we scroll the map into view, 301 // but it takes some math because we have to deal in lon/lat instead of the given offset in pixels. 302 303 CLLocationCoordinate2D coordinate = self.region.center; 304 305 // where's the center coordinate in terms of our view? 306 CGPoint center = [self convertCoordinate:coordinate toPointToView:self]; 307 308 // move it by the requested offset 309 center.x -= offset.width; 310 center.y -= offset.height; 311 312 // and translate it back into map coordinates 313 coordinate = [self convertPoint:center toCoordinateFromView:self]; 314 315 // move the map! 316 [self setCenterCoordinate:coordinate animated:YES]; 317 318 // tell the callout to wait for a while while we scroll (we assume the scroll delay for MKMapView matches UIScrollView) 319 return kSMCalloutViewRepositionDelayForUIScrollView; 320} 321 322#pragma mark Accessors 323 324- (NSArray *)getMapBoundaries 325{ 326 MKMapRect mapRect = self.visibleMapRect; 327 328 CLLocationCoordinate2D northEast = MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMaxX(mapRect), mapRect.origin.y)); 329 CLLocationCoordinate2D southWest = MKCoordinateForMapPoint(MKMapPointMake(mapRect.origin.x, MKMapRectGetMaxY(mapRect))); 330 331 return @[ 332 @[ 333 [NSNumber numberWithDouble:northEast.longitude], 334 [NSNumber numberWithDouble:northEast.latitude] 335 ], 336 @[ 337 [NSNumber numberWithDouble:southWest.longitude], 338 [NSNumber numberWithDouble:southWest.latitude] 339 ] 340 ]; 341} 342 343- (void)setShowsUserLocation:(BOOL)showsUserLocation 344{ 345 if (self.showsUserLocation != showsUserLocation) { 346 super.showsUserLocation = showsUserLocation; 347 } 348} 349 350- (void)setUserInterfaceStyle:(NSString*)userInterfaceStyle 351{ 352 if (@available(iOS 13.0, *)) { 353 if([userInterfaceStyle isEqualToString:@"light"]) { 354 self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 355 } else if([userInterfaceStyle isEqualToString:@"dark"]) { 356 self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; 357 } else { 358 self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified; 359 } 360 } else { 361 NSLog(@"UserInterfaceStyle not supported below iOS 13"); 362 } 363} 364 365- (void)setTintColor:(UIColor *)tintColor 366{ 367 super.tintColor = tintColor; 368} 369 370- (void)setFollowsUserLocation:(BOOL)followsUserLocation 371{ 372 _followUserLocation = followsUserLocation; 373} 374 375- (void)setUserLocationCalloutEnabled:(BOOL)calloutEnabled 376{ 377 _userLocationCalloutEnabled = calloutEnabled; 378} 379 380- (void)setHandlePanDrag:(BOOL)handleMapDrag { 381 for (UIGestureRecognizer *recognizer in [self gestureRecognizers]) { 382 if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]]) { 383 recognizer.enabled = handleMapDrag; 384 break; 385 } 386 } 387} 388 389- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated 390{ 391 // If location is invalid, abort 392 if (!CLLocationCoordinate2DIsValid(region.center)) { 393 return; 394 } 395 396 // If new span values are nil, use old values instead 397 if (!region.span.latitudeDelta) { 398 region.span.latitudeDelta = self.region.span.latitudeDelta; 399 } 400 if (!region.span.longitudeDelta) { 401 region.span.longitudeDelta = self.region.span.longitudeDelta; 402 } 403 404 // Animate/move to new position 405 [super setRegion:region animated:animated]; 406} 407 408- (void)setInitialRegion:(MKCoordinateRegion)initialRegion { 409 if (!_initialRegionSet) { 410 _initialRegionSet = YES; 411 [self setRegion:initialRegion animated:NO]; 412 } 413} 414 415- (void)setCamera:(MKMapCamera*)camera animated:(BOOL)animated 416{ 417 [super setCamera:camera animated:animated]; 418} 419 420 421- (void)setInitialCamera:(MKMapCamera*)initialCamera { 422 if (!_initialCameraSet) { 423 _initialCameraSet = YES; 424 [self setCamera:initialCamera animated:NO]; 425 } 426} 427 428- (void)setCacheEnabled:(BOOL)cacheEnabled { 429 _cacheEnabled = cacheEnabled; 430 if (self.cacheEnabled && self.cacheImageView.image == nil) { 431 self.loadingView.hidden = NO; 432 [self.activityIndicatorView startAnimating]; 433 } 434 else { 435 if (_loadingView != nil) { 436 self.loadingView.hidden = YES; 437 } 438 } 439} 440 441- (void)setLoadingEnabled:(BOOL)loadingEnabled { 442 _loadingEnabled = loadingEnabled; 443 if (!self.hasShownInitialLoading) { 444 self.loadingView.hidden = !self.loadingEnabled; 445 } 446 else { 447 if (_loadingView != nil) { 448 self.loadingView.hidden = YES; 449 } 450 } 451} 452 453- (UIColor *)loadingBackgroundColor { 454 return self.loadingView.backgroundColor; 455} 456 457- (void)setLoadingBackgroundColor:(UIColor *)loadingBackgroundColor { 458 self.loadingView.backgroundColor = loadingBackgroundColor; 459} 460 461- (UIColor *)loadingIndicatorColor { 462 return self.activityIndicatorView.color; 463} 464 465- (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor { 466 self.activityIndicatorView.color = loadingIndicatorColor; 467} 468 469// Include properties of MKMapView which are only available on iOS 9+ 470// and check if their selector is available before calling super method. 471 472- (void)setShowsCompass:(BOOL)showsCompass { 473 if ([MKMapView instancesRespondToSelector:@selector(setShowsCompass:)]) { 474 [super setShowsCompass:showsCompass]; 475 } 476} 477 478- (BOOL)showsCompass { 479 if ([MKMapView instancesRespondToSelector:@selector(showsCompass)]) { 480 return [super showsCompass]; 481 } else { 482 return NO; 483 } 484} 485 486- (void)setShowsScale:(BOOL)showsScale { 487 if ([MKMapView instancesRespondToSelector:@selector(setShowsScale:)]) { 488 [super setShowsScale:showsScale]; 489 } 490} 491 492- (BOOL)showsScale { 493 if ([MKMapView instancesRespondToSelector:@selector(showsScale)]) { 494 return [super showsScale]; 495 } else { 496 return NO; 497 } 498} 499 500- (void)setShowsTraffic:(BOOL)showsTraffic { 501 if ([MKMapView instancesRespondToSelector:@selector(setShowsTraffic:)]) { 502 [super setShowsTraffic:showsTraffic]; 503 } 504} 505 506- (BOOL)showsTraffic { 507 if ([MKMapView instancesRespondToSelector:@selector(showsTraffic)]) { 508 return [super showsTraffic]; 509 } else { 510 return NO; 511 } 512} 513 514- (void)setScrollEnabled:(BOOL)scrollEnabled { 515 self.shouldScrollEnabled = [NSNumber numberWithBool:scrollEnabled]; 516 [self updateScrollEnabled]; 517} 518 519- (void)updateScrollEnabled { 520 if (self.cacheEnabled) { 521 [super setScrollEnabled:NO]; 522 } 523 else if (self.shouldScrollEnabled != nil) { 524 [super setScrollEnabled:[self.shouldScrollEnabled boolValue]]; 525 } 526} 527 528- (void)setZoomEnabled:(BOOL)zoomEnabled { 529 self.shouldZoomEnabled = [NSNumber numberWithBool:zoomEnabled]; 530 [self updateZoomEnabled]; 531} 532 533- (void)updateZoomEnabled { 534 if (self.cacheEnabled) { 535 [super setZoomEnabled: NO]; 536 } 537 else if (self.shouldZoomEnabled != nil) { 538 [super setZoomEnabled:[self.shouldZoomEnabled boolValue]]; 539 } 540} 541 542- (void)cacheViewIfNeeded { 543 // https://github.com/react-native-maps/react-native-maps/issues/3100 544 // Do nothing if app is not active 545 if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { 546 return; 547 } 548 if (self.hasShownInitialLoading) { 549 if (!self.cacheEnabled) { 550 if (_cacheImageView != nil) { 551 self.cacheImageView.hidden = YES; 552 self.cacheImageView.image = nil; 553 } 554 } 555 else { 556 self.cacheImageView.image = nil; 557 self.cacheImageView.hidden = YES; 558 559 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 560 self.cacheImageView.image = nil; 561 self.cacheImageView.hidden = YES; 562 UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0); 563 [self.layer renderInContext:UIGraphicsGetCurrentContext()]; 564 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 565 UIGraphicsEndImageContext(); 566 567 self.cacheImageView.image = image; 568 self.cacheImageView.hidden = NO; 569 }); 570 } 571 572 [self updateScrollEnabled]; 573 [self updateZoomEnabled]; 574 [self updateLegalLabelInsets]; 575 } 576} 577 578- (void)updateLegalLabelInsets { 579 if (_legalLabel) { 580 dispatch_async(dispatch_get_main_queue(), ^{ 581 CGRect frame = self->_legalLabel.frame; 582 if (self->_legalLabelInsets.left) { 583 frame.origin.x = self->_legalLabelInsets.left; 584 } else if (self->_legalLabelInsets.right) { 585 frame.origin.x = self.frame.size.width - self->_legalLabelInsets.right - frame.size.width; 586 } 587 if (self->_legalLabelInsets.top) { 588 frame.origin.y = self->_legalLabelInsets.top; 589 } else if (self->_legalLabelInsets.bottom) { 590 frame.origin.y = self.frame.size.height - self->_legalLabelInsets.bottom - frame.size.height; 591 } 592 self->_legalLabel.frame = frame; 593 }); 594 } 595} 596 597 598- (void)setLegalLabelInsets:(UIEdgeInsets)legalLabelInsets { 599 _legalLabelInsets = legalLabelInsets; 600 [self updateLegalLabelInsets]; 601} 602 603- (void)setMapPadding:(UIEdgeInsets)mapPadding { 604 self.layoutMargins = mapPadding; 605} 606 607- (UIEdgeInsets)mapPadding { 608 return self.layoutMargins; 609} 610 611- (void)beginLoading { 612 if ((!self.hasShownInitialLoading && self.loadingEnabled) || (self.cacheEnabled && self.cacheImageView.image == nil)) { 613 self.loadingView.hidden = NO; 614 [self.activityIndicatorView startAnimating]; 615 } 616 else { 617 if (_loadingView != nil) { 618 self.loadingView.hidden = YES; 619 } 620 } 621} 622 623- (void)finishLoading { 624 self.hasShownInitialLoading = YES; 625 if (_loadingView != nil) { 626 self.loadingView.hidden = YES; 627 } 628 [self cacheViewIfNeeded]; 629} 630 631- (UIActivityIndicatorView *)activityIndicatorView { 632 if (_activityIndicatorView == nil) { 633 _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 634 _activityIndicatorView.center = self.loadingView.center; 635 _activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 636 _activityIndicatorView.color = [UIColor colorWithRed:96.f/255.f green:96.f/255.f blue:96.f/255.f alpha:1.f]; // defaults to #606060 637 } 638 [self.loadingView addSubview:_activityIndicatorView]; 639 return _activityIndicatorView; 640} 641 642- (UIView *)loadingView { 643 if (_loadingView == nil) { 644 _loadingView = [[UIView alloc] initWithFrame:self.bounds]; 645 _loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 646 _loadingView.backgroundColor = [UIColor whiteColor]; // defaults to #FFFFFF 647 [self addSubview:_loadingView]; 648 _loadingView.hidden = NO; 649 } 650 return _loadingView; 651} 652 653- (UIImageView *)cacheImageView { 654 if (_cacheImageView == nil) { 655 _cacheImageView = [[UIImageView alloc] initWithFrame:self.bounds]; 656 _cacheImageView.contentMode = UIViewContentModeCenter; 657 _cacheImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 658 [self addSubview:self.cacheImageView]; 659 _cacheImageView.hidden = YES; 660 } 661 return _cacheImageView; 662} 663 664- (void)layoutSubviews { 665 [super layoutSubviews]; 666 [self cacheViewIfNeeded]; 667 NSUInteger index = [[self subviews] indexOfObjectPassingTest:^BOOL(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 668 NSString *str = NSStringFromClass([obj class]); 669 return [str containsString:@"MKCompassView"]; 670 }]; 671 if (index != NSNotFound) { 672 UIView* compassButton; 673 compassButton = [self.subviews objectAtIndex:index]; 674 compassButton.frame = CGRectMake(compassButton.frame.origin.x + _compassOffset.x, compassButton.frame.origin.y + _compassOffset.y, compassButton.frame.size.width, compassButton.frame.size.height); 675 } 676} 677 678@end 679