From 79b4c9574933c5a5c46563db2ac55d894bded7e2 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Thu, 3 Dec 2015 11:22:05 +0000 Subject: [PATCH] Substantially improved ASMapNode and made it a lot clearer and less complex internally. --- AsyncDisplayKit/ASMapNode.h | 32 ++-- AsyncDisplayKit/ASMapNode.mm | 162 ++++++++---------- .../Kittens/Sample.xcodeproj/project.pbxproj | 16 ++ examples/Kittens/Sample/KittenNode.mm | 30 +++- 4 files changed, 127 insertions(+), 113 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index 35d63bf228..e5fcbd4921 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -8,38 +8,36 @@ #import #import -@interface ASMapNode : ASControlNode + +@interface ASMapNode : ASImageNode + - (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate NS_DESIGNATED_INITIALIZER; + /** - This is the snapshot shot image node, this will be hidden (but not nil) when .liveMap = YES + This is the MKMapView that is the live map part of ASMapNode. This will be nil if .liveMap = NO. Note, MKMapView is *not* thread-safe. */ -@property (nonatomic, readonly) ASImageNode *mapImage; -/** - This is the ASDisplayNode that backs the MKMapView. This will be nil if .liveMap = NO. To access the underlying MKMapView, in order to set a delegate for example, use (MKMapView *)mapView.view; - */ -@property (nonatomic, readonly) ASDisplayNode *mapView; +@property (nonatomic, readonly) MKMapView *mapView; + /** Set this to YES to turn the snapshot into an interactive MKMapView and vice versa. Defaults to NO. */ @property (nonatomic, assign, getter=isLiveMap) BOOL liveMap; -/** - @abstract Explicitly set the size of the map and therefore the size of ASMapNode. Defaults to CGSizeMake(constrainedSize.max.width, 256). - @discussion If the mapSize width or height is greater than the available space, then ASMapNode will take the maximum space available. - @result The current size of the ASMapNode. - */ -@property (nonatomic, assign) CGSize mapSize; + /** @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. Defaults to YES. @discussion If mapSize is set then this will be set to NO, since the size will be the same in all orientations. */ -@property (nonatomic, assign) BOOL automaticallyReloadsMapImageOnOrientationChange; +@property (nonatomic, assign) BOOL needsMapReloadOnBoundsChange; + /** - Set the delegate of the MKMapView. + Set the delegate of the MKMapView. This can be set even before mapView is created and will be set on the map in the case that the liveMap mode is engaged. */ @property (nonatomic, weak) id mapDelegate; + /** - * @discussion This method adds annotations to the static map view and also to the live map view. + * @discussion This method set the annotations of the static map view and also to the live map view. Passing an empty array clears the map of any annotations. * @param annotations An array of objects that conform to the MKAnnotation protocol */ -- (void)addAnnotations:(NSArray *)annotations; +- (void)setAnnotations:(NSArray *)annotations; + @end diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index d60171a9e4..311000362e 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -14,19 +14,18 @@ @interface ASMapNode() { ASDN::RecursiveMutex _propertyLock; - CGSize _nodeSize; MKMapSnapshotter *_snapshotter; MKMapSnapshotOptions *_options; - CGSize _maxSize; NSArray *_annotations; + ASDisplayNode *_mapNode; + CLLocationCoordinate2D _centerCoordinateOfMap; } @end @implementation ASMapNode @synthesize liveMap = _liveMap; -@synthesize mapSize = _mapSize; -@synthesize automaticallyReloadsMapImageOnOrientationChange = _automaticallyReloadsMapImageOnOrientationChange; +@synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; @synthesize mapDelegate = _mapDelegate; - (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate @@ -35,26 +34,23 @@ return nil; } self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); - _automaticallyReloadsMapImageOnOrientationChange = YES; + self.clipsToBounds = YES; + + _needsMapReloadOnBoundsChange = YES; _liveMap = NO; + _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid; _options = [[MKMapSnapshotOptions alloc] init]; _options.region = MKCoordinateRegionMakeWithDistance(coordinate, 1000, 1000);; - _mapImage = [[ASImageNode alloc]init]; - _mapImage.clipsToBounds = YES; - [self addSubnode:_mapImage]; - _maxSize = self.bounds.size; + return self; } -- (void)addAnnotations:(NSArray *)annotations +- (void)setAnnotations:(NSArray *)annotations { ASDN::MutexLocker l(_propertyLock); - if (annotations.count == 0) { - return; - } _annotations = [annotations copy]; - if (annotations.count != _annotations.count && _mapImage.image) { + if (annotations.count != _annotations.count) { // Redraw [self setNeedsDisplay]; } @@ -63,8 +59,9 @@ - (void)setUpSnapshotter { if (!_snapshotter) { - _options.size = _nodeSize; - _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; + ASDisplayNodeAssert(!CGSizeEqualToSize(CGSizeZero, self.calculatedSize), @"self.calculatedSize can not be zero. Make sure that you are setting a preferredFrameSize or wrapping ASMapNode in a ASRatioLayoutSpec or similar."); + _options.size = self.calculatedSize; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; } } @@ -77,60 +74,42 @@ - (void)setLiveMap:(BOOL)liveMap { ASDN::MutexLocker l(_propertyLock); - if (liveMap == _liveMap) return; + if (liveMap == _liveMap) { + return; + } _liveMap = liveMap; liveMap ? [self addLiveMap] : [self removeLiveMap]; } -- (CGSize)mapSize + +- (BOOL)needsMapReloadOnBoundsChange { ASDN::MutexLocker l(_propertyLock); - return _mapSize; + return _needsMapReloadOnBoundsChange; } -- (void)setMapSize:(CGSize)mapSize +- (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange { ASDN::MutexLocker l(_propertyLock); - if (CGSizeEqualToSize(mapSize,_mapSize)) { - return; - } - _mapSize = mapSize; - _nodeSize = _mapSize; - _automaticallyReloadsMapImageOnOrientationChange = NO; - [self setNeedsLayout]; -} - -- (BOOL)automaticallyReloadsMapImageOnOrientationChange -{ - ASDN::MutexLocker l(_propertyLock); - return _automaticallyReloadsMapImageOnOrientationChange; -} - -- (void)setAutomaticallyReloadsMapImageOnOrientationChange:(BOOL)automaticallyReloadsMapImageOnOrientationChange -{ - ASDN::MutexLocker l(_propertyLock); - if (_automaticallyReloadsMapImageOnOrientationChange == automaticallyReloadsMapImageOnOrientationChange) { - return; - } - _automaticallyReloadsMapImageOnOrientationChange = automaticallyReloadsMapImageOnOrientationChange; - + _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } - (void)fetchData { [super fetchData]; - [self setUpSnapshotter]; - [self takeSnapshot]; + if (_liveMap && !_mapNode) { + [self addLiveMap]; + } + else { + [self setUpSnapshotter]; + [self takeSnapshot]; + } } - (void)clearFetchedData { [super clearFetchedData]; - if (_mapView) { - [_mapView removeFromSupernode]; - _mapView = nil; - } - _mapImage.image = nil; + [self removeLiveMap]; } - (void)takeSnapshot @@ -141,29 +120,31 @@ UIImage *image = snapshot.image; CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); - // Get a standard annotation view pin. Future implementations should use a custom annotation image property. - MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; - UIImage *pinImage = pin.image; - UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); [image drawAtPoint:CGPointMake(0, 0)]; - for (idannotation in _annotations) - { - CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; - if (CGRectContainsPoint(finalImageRect, point)) + if (_annotations.count > 0 ) { + // Get a standard annotation view pin. Future implementations should use a custom annotation image property. + MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; + UIImage *pinImage = pin.image; + for (idannotation in _annotations) { - CGPoint pinCenterOffset = pin.centerOffset; - point.x -= pin.bounds.size.width / 2.0; - point.y -= pin.bounds.size.height / 2.0; - point.x += pinCenterOffset.x; - point.y += pinCenterOffset.y; - [pinImage drawAtPoint:point]; + CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; + if (CGRectContainsPoint(finalImageRect, point)) + { + CGPoint pinCenterOffset = pin.centerOffset; + point.x -= pin.bounds.size.width / 2.0; + point.y -= pin.bounds.size.height / 2.0; + point.x += pinCenterOffset.x; + point.y += pinCenterOffset.y; + [pinImage drawAtPoint:point]; + } } } + UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - _mapImage.image = finalImage; + self.image = finalImage; } }]; } @@ -172,7 +153,7 @@ - (void)resetSnapshotter { if (!_snapshotter.isLoading) { - _options.size = _nodeSize; + _options.size = self.calculatedSize; _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; } } @@ -180,52 +161,45 @@ #pragma mark - Action - (void)addLiveMap { - if (self.isNodeLoaded && !_mapView) { - _mapView = [[ASDisplayNode alloc]initWithViewBlock:^UIView *{ - MKMapView *mapView = [[MKMapView alloc]initWithFrame:CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height)]; - mapView.delegate = _mapDelegate; - [mapView setRegion:_options.region]; - [mapView addAnnotations:_annotations]; - return mapView; + if (self.isNodeLoaded && !_mapNode) { + _mapNode = [[ASDisplayNode alloc]initWithViewBlock:^UIView *{ + _mapView = [[MKMapView alloc]initWithFrame:CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height)]; + _mapView.delegate = _mapDelegate; + [_mapView setRegion:_options.region]; + [_mapView addAnnotations:_annotations]; + return _mapView; }]; - [self addSubnode:_mapView]; - _mapImage.hidden = YES; + [self addSubnode:_mapNode]; + + if (CLLocationCoordinate2DIsValid(_centerCoordinateOfMap)) { + [_mapView setCenterCoordinate:_centerCoordinateOfMap]; + } } } - (void)removeLiveMap { - if (_mapView) { - [_mapView removeFromSupernode]; + if (_mapNode) { + _centerCoordinateOfMap = _mapView.centerCoordinate; + [_mapNode removeFromSupernode]; _mapView = nil; - _mapImage.hidden = NO; + _mapNode = nil; } + self.image = nil; } #pragma mark - Layout -- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize -{ - _nodeSize = CGSizeEqualToSize(CGSizeZero, _mapSize) ? CGSizeMake(constrainedSize.width, _options.size.height) : _mapSize; - if (_mapImage) { - [_mapImage calculateSizeThatFits:_nodeSize]; - } - return _nodeSize; -} - // Layout isn't usually needed in the box model, but since we are making use of MKMapView which is hidden in an ASDisplayNode this is preferred. - (void)layout { [super layout]; if (_mapView) { - MKMapView *mapView = (MKMapView *)_mapView.view; - mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); + _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); } else { - _mapImage.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); - if (!CGSizeEqualToSize(_maxSize, self.bounds.size)) { - _mapImage.preferredFrameSize = self.bounds.size; - _maxSize = self.bounds.size; - if (_automaticallyReloadsMapImageOnOrientationChange && _mapImage.image) { + // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter. + if (!CGSizeEqualToSize(_options.size, self.bounds.size)) { + if (_needsMapReloadOnBoundsChange && self.image) { [self resetSnapshotter]; [self takeSnapshot]; } diff --git a/examples/Kittens/Sample.xcodeproj/project.pbxproj b/examples/Kittens/Sample.xcodeproj/project.pbxproj index ddfd884074..e70aa77813 100644 --- a/examples/Kittens/Sample.xcodeproj/project.pbxproj +++ b/examples/Kittens/Sample.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + 860D1494A00C2E990C93A4D9 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -184,6 +185,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 860D1494A00C2E990C93A4D9 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/examples/Kittens/Sample/KittenNode.mm b/examples/Kittens/Sample/KittenNode.mm index adc656aa22..144313959d 100644 --- a/examples/Kittens/Sample/KittenNode.mm +++ b/examples/Kittens/Sample/KittenNode.mm @@ -29,6 +29,7 @@ static const CGFloat kInnerPadding = 10.0f; ASNetworkImageNode *_imageNode; ASTextNode *_textNode; ASDisplayNode *_divider; + ASMapNode *_map; BOOL _isImageEnlarged; BOOL _swappedTextAndImage; } @@ -88,6 +89,22 @@ static const CGFloat kInnerPadding = 10.0f; // _imageNode.contentMode = UIViewContentModeCenter; [_imageNode addTarget:self action:@selector(toggleNodesSwap) forControlEvents:ASControlNodeEventTouchUpInside]; [self addSubnode:_imageNode]; + + MKPointAnnotation *point1 = [[MKPointAnnotation alloc]init]; + point1.coordinate = CLLocationCoordinate2DMake(55.864237, -4.251806); + point1.title = @"Best fish & chip shop"; + point1.subtitle = @"Everrrrrr"; + + + MKPointAnnotation *point2 = [[MKPointAnnotation alloc]init]; + point2.coordinate = CLLocationCoordinate2DMake(55.861, -4.251806); + point2.title = @"The 2nd Best fish & chip shop"; + point2.subtitle = @"Everrrrrr"; + + _map = [[ASMapNode alloc]initWithCoordinate:CLLocationCoordinate2DMake(55.864237, -4.251806)]; + [_map addTarget:self action:@selector(makeMapInteractive) forControlEvents:ASControlNodeEventTouchUpInside]; + [_map setAnnotations:@[point1,point2]]; + [self addSubnode:_map]; // lorem ipsum text, plus some nice styling _textNode = [[ASTextNode alloc] init]; @@ -131,16 +148,25 @@ static const CGFloat kInnerPadding = 10.0f; NSParagraphStyleAttributeName: style }; } +- (void)makeMapInteractive +{ + [_map setLiveMap:YES]; +} + #if UseAutomaticLayout - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { _imageNode.preferredFrameSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) : CGSizeMake(kImageSize, kImageSize); _textNode.flexShrink = YES; + + ASRatioLayoutSpec *ratioSpec = [[ASRatioLayoutSpec alloc]init]; + ratioSpec.ratio = 0.5; + ratioSpec.child = _map; ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; - stackSpec.direction = ASStackLayoutDirectionHorizontal; + stackSpec.direction = ASStackLayoutDirectionVertical; stackSpec.spacing = kInnerPadding; - [stackSpec setChildren:!_swappedTextAndImage ? @[_imageNode, _textNode] : @[_textNode, _imageNode]]; + [stackSpec setChildren:!_swappedTextAndImage ? @[ratioSpec,_imageNode, _textNode] : @[ratioSpec,_textNode, _imageNode]]; ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding);