diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index cfb767f482..d6257c18a2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -211,6 +211,8 @@ 509E68651B3AEDC5009B9150 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 92DD2FE31BF4B97E0074C9DD /* ASMapNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */; }; + 92DD2FE41BF4B97E0074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */; }; 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; }; 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -599,6 +601,8 @@ 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASFlowLayoutController.mm; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; + 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; + 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutable.h; path = AsyncDisplayKit/Layout/ASStackLayoutable.h; sourceTree = ""; }; 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASAsciiArtBoxCreator.h; path = AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.h; sourceTree = ""; }; 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASAsciiArtBoxCreator.m; path = AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m; sourceTree = ""; }; @@ -787,6 +791,8 @@ 058D09B1195D04C000B7D73C /* AsyncDisplayKit */ = { isa = PBXGroup; children = ( + 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */, + 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */, 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */, AC6456071B0A335000CF11B8 /* ASCellNode.m */, 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */, @@ -1194,6 +1200,7 @@ ACF6ED4F1B17847A00DA7C62 /* ASStackPositionedLayout.h in Headers */, ACF6ED511B17847A00DA7C62 /* ASStackUnpositionedLayout.h in Headers */, 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, + 92DD2FE31BF4B97E0074C9DD /* ASMapNode.h in Headers */, ACF6ED311B17843500DA7C62 /* ASStaticLayoutSpec.h in Headers */, 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */, 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */, @@ -1547,6 +1554,7 @@ AC6456091B0A335000CF11B8 /* ASCellNode.m in Sources */, ACF6ED1D1B17843500DA7C62 /* ASCenterLayoutSpec.mm in Sources */, 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.m in Sources */, + 92DD2FE41BF4B97E0074C9DD /* ASMapNode.mm in Sources */, AC3C4A521A1139C100143C57 /* ASCollectionView.mm in Sources */, 205F0E1E1B373A2C007741D0 /* ASCollectionViewLayoutController.mm in Sources */, 058D0A13195D050800B7D73C /* ASControlNode.m in Sources */, diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h new file mode 100644 index 0000000000..566e6dc0f3 --- /dev/null +++ b/AsyncDisplayKit/ASMapNode.h @@ -0,0 +1,39 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#import +#import +@interface ASMapNode : ASControlNode +- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) ASImageNode *mapImage; +@property (nonatomic, readonly) ASDisplayNode *liveMap; +/** + Whether the map snapshot should turn into a MKMapView when tapped on. Defaults to YES. + */ +@property (nonatomic, assign) BOOL hasLiveMap; +/** + @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; +/** + Set the delegate of the MKMapView. + */ +@property (nonatomic, weak) id mapDelegate; +/** + * @discussion This method adds annotations to the static map view and also to the live map view. + * @param annotations An array of objects that conform to the MKAnnotation protocol + */ +- (void)addAnnotations:(NSArray *)annotations; +@end diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm new file mode 100644 index 0000000000..869cf0ccfa --- /dev/null +++ b/AsyncDisplayKit/ASMapNode.mm @@ -0,0 +1,235 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASMapNode.h" +#import +#import +#import + +@interface ASMapNode() +{ + ASDN::RecursiveMutex _propertyLock; + CGSize _nodeSize; + MKMapSnapshotter *_snapshotter; + MKMapSnapshotOptions *_options; + CGSize _maxSize; + NSArray *_annotations; +} +@end + +@implementation ASMapNode + +@synthesize hasLiveMap = _hasLiveMap; +@synthesize mapSize = _mapSize; +@synthesize automaticallyReloadsMapImageOnOrientationChange = _automaticallyReloadsMapImageOnOrientationChange; +@synthesize mapDelegate = _mapDelegate; + +- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate +{ + if (!(self = [super init])) { + return nil; + } + self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _hasLiveMap = YES; + _automaticallyReloadsMapImageOnOrientationChange = YES; + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionMakeWithDistance(coordinate, 1000, 1000);; + + _mapImage = [[ASImageNode alloc]init]; + _mapImage.clipsToBounds = YES; + [self addSubnode:_mapImage]; + [self updateGesture]; + _maxSize = self.bounds.size; + return self; +} + +- (void)addAnnotations:(NSArray *)annotations +{ + ASDN::MutexLocker l(_propertyLock); + if (annotations.count == 0) { + return; + } + _annotations = [annotations copy]; + if (annotations.count != _annotations.count && _mapImage.image) { + // Redraw + [self setNeedsDisplay]; + } +} + +- (void)setUpSnapshotter +{ + if (!_snapshotter) { + _options.size = _nodeSize; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; + } +} + +- (BOOL)hasLiveMap +{ + ASDN::MutexLocker l(_propertyLock); + return _hasLiveMap; +} + +- (void)setHasLiveMap:(BOOL)hasLiveMap +{ + ASDN::MutexLocker l(_propertyLock); + if (hasLiveMap == _hasLiveMap) + return; + + _hasLiveMap = hasLiveMap; + [self updateGesture]; +} + +- (CGSize)mapSize +{ + ASDN::MutexLocker l(_propertyLock); + return _mapSize; +} + +- (void)setMapSize:(CGSize)mapSize +{ + 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; + +} + +- (void)updateGesture +{ + _hasLiveMap ? [self addTarget:self action:@selector(showLiveMap) forControlEvents:ASControlNodeEventTouchUpInside] : [self removeTarget:self action:@selector(showLiveMap) forControlEvents:ASControlNodeEventTouchUpInside]; +} + +- (void)fetchData +{ + [super fetchData]; + [self setUpSnapshotter]; + [self takeSnapshot]; +} + +- (void)clearFetchedData +{ + [super clearFetchedData]; + if (_liveMap) { + [_liveMap removeFromSupernode]; + _liveMap = nil; + } + _mapImage.image = nil; +} + +- (void)takeSnapshot +{ + if (!_snapshotter.isLoading) { + [_snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) { + if (!error) { + 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)) + { + 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; + } + }]; + } +} + +- (void)resetSnapshotter +{ + if (!_snapshotter.isLoading) { + _options.size = _nodeSize; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; + } +} + +#pragma mark - Action +- (void)showLiveMap +{ + if (self.isNodeLoaded && !_liveMap) { + _liveMap = [[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; + }]; + [self addSubnode:_liveMap]; + _mapImage.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 (_liveMap) { + MKMapView *mapView = (MKMapView *)_liveMap.view; + 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) { + [self resetSnapshotter]; + [self takeSnapshot]; + } + } + } +} + +@end