From 45fa36eba57b829ca63146ec8a70c2e3e3013ef6 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Tue, 5 Jan 2016 13:44:41 +0000 Subject: [PATCH 01/18] ASMapNode now supports MKMapSnapshotOptions as opposed to just a region property. --- AsyncDisplayKit/ASMapNode.h | 4 ++-- AsyncDisplayKit/ASMapNode.mm | 33 +++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index bd2d23ad4b..524f844932 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -12,9 +12,9 @@ @interface ASMapNode : ASImageNode /** - The current region of ASMapNode. This can be set at any time and ASMapNode will animate the change. This property may be set from a background thread before the node is loaded, and will automatically be applied to define the region of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise). + The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change. This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise). */ -@property (nonatomic, assign) MKCoordinateRegion region; +@property (nonatomic, readwrite) MKMapSnapshotOptions *options; /** 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. diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index 480360a1aa..aaebabdecb 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -15,7 +15,6 @@ { ASDN::RecursiveMutex _propertyLock; MKMapSnapshotter *_snapshotter; - MKMapSnapshotOptions *_options; NSArray *_annotations; CLLocationCoordinate2D _centerCoordinateOfMap; } @@ -25,7 +24,7 @@ @synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; @synthesize mapDelegate = _mapDelegate; -@synthesize region = _region; +@synthesize options = _options; @synthesize liveMap = _liveMap; #pragma mark - Lifecycle @@ -41,11 +40,9 @@ _liveMap = NO; _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid; - //Default world-scale view - _region = MKCoordinateRegionForMapRect(MKMapRectWorld); - _options = [[MKMapSnapshotOptions alloc] init]; - _options.region = _region; + //Default world-scale view + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); return self; } @@ -116,20 +113,19 @@ _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } -- (MKCoordinateRegion)region +- (MKMapSnapshotOptions *)options { ASDN::MutexLocker l(_propertyLock); - return _region; + return _options; } -- (void)setRegion:(MKCoordinateRegion)region +- (void)setOptions:(MKMapSnapshotOptions *)options { ASDN::MutexLocker l(_propertyLock); - _region = region; + _options = options; if (self.isLiveMap) { - [_mapView setRegion:_region animated:YES]; + [self applySnapshotOptions]; } else { - _options.region = _region; [self resetSnapshotter]; [self takeSnapshot]; } @@ -190,6 +186,15 @@ _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; } +- (void)applySnapshotOptions +{ + [_mapView setCamera:_options.camera animated:YES]; + [_mapView setRegion:_options.region animated:YES]; + [_mapView setMapType:_options.mapType]; + _mapView.showsBuildings = _options.showsBuildings; + _mapView.showsPointsOfInterest = _options.showsPointsOfInterest; +} + #pragma mark - Actions - (void)addLiveMap { @@ -198,7 +203,7 @@ __weak ASMapNode *weakSelf = self; _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; _mapView.delegate = weakSelf.mapDelegate; - [_mapView setRegion:_options.region]; + [weakSelf applySnapshotOptions]; [_mapView addAnnotations:_annotations]; [weakSelf setNeedsLayout]; [weakSelf.view addSubview:_mapView]; @@ -229,7 +234,7 @@ } #pragma mark - Layout -// 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. +// Layout isn't usually needed in the box model, but since we are making use of MKMapView this is preferred. - (void)layout { [super layout]; From 239ec6feab3dc4dd1921406697cd452fd26fdd56 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Mon, 11 Jan 2016 19:30:56 +0000 Subject: [PATCH 02/18] [ASMapNode] Updated comment of options property. --- AsyncDisplayKit/ASMapNode.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index 524f844932..9d49f99a17 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -12,9 +12,9 @@ @interface ASMapNode : ASImageNode /** - The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change. This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise). + The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change.

This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise).

Changes to the region and camera options will only be animated when when the liveMap mode is enabled, otherwise these options will be applied statically to the new snapshot.

The options object is used to specify properties even when the liveMap mode is enabled, allowing seamless transitions between the snapshot and liveMap (as well as back to the snapshot). */ -@property (nonatomic, readwrite) MKMapSnapshotOptions *options; +@property (nonatomic, strong) MKMapSnapshotOptions *options; /** 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. @@ -38,7 +38,7 @@ @property (nonatomic, weak) id mapDelegate; /** - * @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. + * @discussion This method sets 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)setAnnotations:(NSArray *)annotations; From e8f5f61e3b93028237bcd4445b4f1baef0af5396 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Mon, 11 Jan 2016 19:42:34 +0000 Subject: [PATCH 03/18] [ASMapNode] Defer creation of default options till they are needed. --- AsyncDisplayKit/ASMapNode.mm | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index aaebabdecb..2f27255c8e 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -39,11 +39,6 @@ _needsMapReloadOnBoundsChange = YES; _liveMap = NO; _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid; - - _options = [[MKMapSnapshotOptions alloc] init]; - //Default world-scale view - _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); - return self; } @@ -176,7 +171,9 @@ - (void)setUpSnapshotter { 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; + if (!_options) { + [self createInitialOptions]; + } _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; } @@ -186,6 +183,14 @@ _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; } +- (void)createInitialOptions +{ + _options = [[MKMapSnapshotOptions alloc] init]; + //Default world-scale view + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + _options.size = self.calculatedSize; +} + - (void)applySnapshotOptions { [_mapView setCamera:_options.camera animated:YES]; @@ -203,6 +208,9 @@ __weak ASMapNode *weakSelf = self; _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; _mapView.delegate = weakSelf.mapDelegate; + if (!_options) { + [weakSelf createInitialOptions]; + } [weakSelf applySnapshotOptions]; [_mapView addAnnotations:_annotations]; [weakSelf setNeedsLayout]; From cc4f604ea38cb0df0a15a1b3f86c3b8a374dd587 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Tue, 12 Jan 2016 14:43:30 +0000 Subject: [PATCH 04/18] [tvOS] Initial commit to make build run. --- AsyncDisplayKit.podspec | 1 + AsyncDisplayKit/ASDisplayNode.h | 2 ++ AsyncDisplayKit/ASEditableTextNode.mm | 2 ++ AsyncDisplayKit/ASMapNode.h | 3 +++ AsyncDisplayKit/ASMapNode.mm | 4 +++- AsyncDisplayKit/ASMultiplexImageNode.h | 13 +++++++----- AsyncDisplayKit/ASMultiplexImageNode.mm | 20 ++++++++++++------- AsyncDisplayKit/ASPagerNode.m | 5 +++-- AsyncDisplayKit/ASTableViewProtocols.h | 3 ++- .../Details/ASPhotosFrameworkImageRequest.h | 3 ++- .../Details/ASPhotosFrameworkImageRequest.m | 3 ++- .../Private/ASDisplayNode+UIViewBridge.mm | 4 ++-- AsyncDisplayKit/Private/_ASPendingState.m | 8 +++++--- 13 files changed, 48 insertions(+), 23 deletions(-) diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 5755b452fa..5698d550de 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -47,4 +47,5 @@ Pod::Spec.new do |spec| } spec.ios.deployment_target = '7.0' + spec.tvos.deployment_target = '9.0' end diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index a38f093ca2..8923e41775 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -641,7 +641,9 @@ NS_ASSUME_NONNULL_END @property (atomic, assign) UIViewContentMode contentMode; // default=UIViewContentModeScaleToFill @property (atomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; // default=YES (NO for layer-backed nodes) +#if TARGET_OS_IOS @property (atomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO +#endif @property (atomic, assign, nullable) CGColorRef shadowColor; // default=opaque rgb black @property (atomic, assign) CGFloat shadowOpacity; // default=0.0 @property (atomic, assign) CGSize shadowOffset; // default=(0, -3) diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index 13554d88e5..c86aa8ccaa 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -137,7 +137,9 @@ _textKitComponents.textView = self.textView; //_textKitComponents.textView = NO; // Unfortunately there's a bug here with iOS 7 DP5 that causes the text-view to only be one line high when scrollEnabled is NO. rdar://14729288 _textKitComponents.textView.delegate = self; + #if TARGET_OS_IOS _textKitComponents.textView.editable = YES; + #endif _textKitComponents.textView.typingAttributes = _typingAttributes; _textKitComponents.textView.returnKeyType = _returnKeyType; _textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string; diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index 0a8b0ebbd7..5aa6920c50 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -7,6 +7,7 @@ */ #import +#if TARGET_OS_IOS #import NS_ASSUME_NONNULL_BEGIN @@ -48,3 +49,5 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END + +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index ce4f3fc8e4..963b9d3647 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -6,6 +6,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#if TARGET_OS_IOS #import "ASMapNode.h" #import #import @@ -246,4 +247,5 @@ } } } -@end \ No newline at end of file +@end +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index 25e463b2af..cac569d3c7 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -8,9 +8,9 @@ #import #import - +#if TARGET_OS_IOS #import - +#endif NS_ASSUME_NONNULL_BEGIN @protocol ASMultiplexImageNodeDelegate; @@ -116,13 +116,14 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { */ @property (nullable, nonatomic, readonly) ASImageIdentifier displayedImageIdentifier; +#if TARGET_OS_IOS /** * @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is `nil` (the default), then `PHImageManager.defaultManager` is used. * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. */ @property (nonatomic, strong) PHImageManager *imageManager; - +#endif @end @@ -229,6 +230,7 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier */ - (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier; +#if TARGET_OS_IOS /** * @abstract A PHAsset for the specific asset local identifier * @param imageNode The sender. @@ -240,11 +242,11 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier * @return A PHAsset corresponding to `assetLocalIdentifier`, or nil if none is available. */ - (nullable PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier; - +#endif @end #pragma mark - - +#if TARGET_OS_IOS @interface NSURL (ASPhotosFrameworkURLs) /** @@ -261,5 +263,6 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier options:(PHImageRequestOptions *)options; @end +#endif NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index c51e2bb2ba..fa58bb88f1 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -7,11 +7,11 @@ */ #import "ASMultiplexImageNode.h" - +#if TARGET_OS_IOS #import #import - +#endif #import #import "ASAvailability.h" @@ -112,6 +112,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent */ - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock; +#if TARGET_OS_IOS /** @abstract Loads the image corresponding to the given assetURL from the device's Assets Library. @param imageIdentifier The identifier for the image to be loaded. May not be nil. @@ -131,7 +132,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent @param error An error describing why the load failed, if it failed; nil otherwise. */ - (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock; - +#endif /** @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. @param imageIdentifier The identifier for the image to be downloaded. May not be nil. @@ -262,7 +263,9 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _dataSource = dataSource; _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)]; _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)]; + #if TARGET_OS_IOS _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)]; + #endif } #pragma mark - @@ -455,6 +458,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return; } + #if TARGET_OS_IOS // If it's an assets-library URL, we need to fetch it from the assets library. if ([[nextImageURL scheme] isEqualToString:kAssetsLibraryURLScheme]) { // Load the asset. @@ -470,6 +474,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent finishedLoadingBlock(image, nextImageIdentifier, error); }]; } + #endif else // Otherwise, it's a web URL that we can download. { // First, check the cache. @@ -499,7 +504,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } } - +#if TARGET_OS_IOS - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock { ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); @@ -609,7 +614,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _phImageRequestOperation = newImageRequestOp; [phImageRequestQueue addOperation:newImageRequestOp]; } - +#endif - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock { ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); @@ -708,7 +713,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } @end - +#if TARGET_OS_IOS @implementation NSURL (ASPhotosFrameworkURLs) + (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options @@ -720,4 +725,5 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return request.url; } -@end \ No newline at end of file +@end +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/ASPagerNode.m b/AsyncDisplayKit/ASPagerNode.m index a69a963d4d..d3bbcddb46 100644 --- a/AsyncDisplayKit/ASPagerNode.m +++ b/AsyncDisplayKit/ASPagerNode.m @@ -48,12 +48,13 @@ [super didLoad]; ASCollectionView *cv = self.view; - +#if TARGET_OS_IOS cv.pagingEnabled = YES; + cv.scrollsToTop = NO; +#endif cv.allowsSelection = NO; cv.showsVerticalScrollIndicator = NO; cv.showsHorizontalScrollIndicator = NO; - cv.scrollsToTop = NO; // Zeroing contentInset is important, as UIKit will set the top inset for the navigation bar even though // our view is only horizontally scrollable. This causes UICollectionViewFlowLayout to log a warning. diff --git a/AsyncDisplayKit/ASTableViewProtocols.h b/AsyncDisplayKit/ASTableViewProtocols.h index df8bb811ca..ff2c441388 100644 --- a/AsyncDisplayKit/ASTableViewProtocols.h +++ b/AsyncDisplayKit/ASTableViewProtocols.h @@ -73,8 +73,9 @@ NS_ASSUME_NONNULL_BEGIN - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath; - (nullable NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath; +#if TARGET_OS_IOS - (nullable NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath; - +#endif - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath; diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h index 7630fe2e84..96c2f4b2ea 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h @@ -5,7 +5,7 @@ // Created by Adlai Holler on 9/25/15. // Copyright © 2015 Facebook. All rights reserved. // - +#if TARGET_OS_IOS #import #import @@ -64,3 +64,4 @@ extern NSString *const ASPhotosURLScheme; @end // NS_ASSUME_NONNULL_END +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m index d46b3791c1..1245e32547 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m @@ -5,7 +5,7 @@ // Created by Adlai Holler on 9/25/15. // Copyright © 2015 Facebook. All rights reserved. // - +#if TARGET_OS_IOS #import "ASPhotosFrameworkImageRequest.h" #import "ASBaseDefines.h" #import "ASAvailability.h" @@ -159,3 +159,4 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; } @end +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 7dd7275090..ec7a10b354 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -298,7 +298,7 @@ _bridge_prologue; _setToViewOnly(userInteractionEnabled, enabled); } - +#if TARGET_OS_IOS - (BOOL)isExclusiveTouch { _bridge_prologue; @@ -310,7 +310,7 @@ _bridge_prologue; _setToViewOnly(exclusiveTouch, exclusiveTouch); } - +#endif - (BOOL)clipsToBounds { _bridge_prologue; diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m index 37918e197b..b1ab0c5d39 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.m +++ b/AsyncDisplayKit/Private/_ASPendingState.m @@ -716,9 +716,11 @@ static UIColor *defaultTintColor = nil; if (_flags.setUserInteractionEnabled) view.userInteractionEnabled = userInteractionEnabled; + #if TARGET_OS_IOS if (_flags.setExclusiveTouch) view.exclusiveTouch = exclusiveTouch; - + #endif + if (_flags.setShadowColor) layer.shadowColor = shadowColor; @@ -943,10 +945,10 @@ static UIColor *defaultTintColor = nil; pendingState.userInteractionEnabled = view.userInteractionEnabled; (pendingState->_flags).setUserInteractionEnabled = YES; - +#if TARGET_OS_IOS pendingState.exclusiveTouch = view.exclusiveTouch; (pendingState->_flags).setExclusiveTouch = YES; - +#endif pendingState.shadowColor = layer.shadowColor; (pendingState->_flags).setShadowColor = YES; From 28b03e3a28fe37a58b3011b23c77c5bc8541a717 Mon Sep 17 00:00:00 2001 From: Aaron Schubert Date: Wed, 13 Jan 2016 10:09:44 +0000 Subject: [PATCH 05/18] [tvOS] Expose UIFocusEnvironment Protocol methods to ASDisplayNode --- AsyncDisplayKit/ASDisplayNode.h | 10 +++++ AsyncDisplayKit/ASDisplayNode.mm | 32 +++++++++++++++ AsyncDisplayKit/Details/_ASDisplayView.mm | 32 +++++++++++++++ .../Private/ASDisplayNode+UIViewBridge.mm | 40 +++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 8923e41775..685ed93aef 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -660,6 +660,16 @@ NS_ASSUME_NONNULL_END - (BOOL)isFirstResponder; - (BOOL)canPerformAction:(nonnull SEL)action withSender:(nonnull id)sender; +#if TARGET_OS_TV +//Focus Engine +- (void)setNeedsFocusUpdate; +- (BOOL)canBecomeFocused; +- (void)updateFocusIfNeeded; +- (void)didUpdateFocusInContext:(nonnull UIFocusUpdateContext *)context withAnimationCoordinator:(nonnull UIFocusAnimationCoordinator *)coordinator; +- (BOOL)shouldUpdateFocusInContext:(nonnull UIFocusUpdateContext *)context; +- (nullable UIView *)preferredFocusedView; +#endif + // Accessibility support @property (atomic, assign) BOOL isAccessibilityElement; @property (nullable, atomic, copy) NSString *accessibilityLabel; diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 1346f7e910..e6e435ff5e 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -2312,6 +2312,38 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, return self; } +#if TARGET_OS_TV +#pragma mark - UIFocusEnvironment Protocol (tvOS) + +- (void)setNeedsFocusUpdate +{ + +} + +- (void)updateFocusIfNeeded +{ + +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + return YES; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + +} + +- (UIView *)preferredFocusedView +{ + if (self.nodeLoaded) { + return self.view; + } else { + return nil; + } +} +#endif @end @implementation ASDisplayNode (Debugging) diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index f542b29db6..25f92dc332 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -331,4 +331,36 @@ return _node; } +#if TARGET_OS_TV +#pragma mark - tvOS +- (BOOL)canBecomeFocused +{ + return [_node canBecomeFocused]; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + return [_node didUpdateFocusInContext:context withAnimationCoordinator:coordinator]; +} + +- (void)setNeedsFocusUpdate +{ + return [_node setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + return [_node updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + return [_node shouldUpdateFocusInContext:context]; +} + +- (UIView *)preferredFocusedView +{ + return [_node preferredFocusedView]; +} +#endif @end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index ec7a10b354..29e9b64af8 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -81,6 +81,46 @@ return YES; } +#if TARGET_OS_TV +// Focus Engine +- (BOOL)canBecomeFocused +{ + return YES; +} + +- (void)setNeedsFocusUpdate +{ + ASDisplayNodeAssertMainThread(); + [_view setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + ASDisplayNodeAssertMainThread(); + [_view updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + return YES; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + +} + +- (UIView *)preferredFocusedView +{ + if (self.nodeLoaded) { + return _view; + } + else { + return nil; + } +} +#endif + - (BOOL)isFirstResponder { ASDisplayNodeAssertMainThread(); From 6c56a6046b0a10d65abd75ecd737e57f03cb5ac9 Mon Sep 17 00:00:00 2001 From: Rajinder Ramgarhia Date: Sat, 16 Jan 2016 00:23:03 -0500 Subject: [PATCH 06/18] Simpler ASButtonNode API to set title and set background image --- AsyncDisplayKit/ASButtonNode.h | 68 ++++++++++++++++++-- AsyncDisplayKit/ASButtonNode.mm | 109 ++++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 11 deletions(-) diff --git a/AsyncDisplayKit/ASButtonNode.h b/AsyncDisplayKit/ASButtonNode.h index 588319b4b6..5ce7d966cc 100644 --- a/AsyncDisplayKit/ASButtonNode.h +++ b/AsyncDisplayKit/ASButtonNode.h @@ -11,8 +11,9 @@ @interface ASButtonNode : ASControlNode -@property (nonatomic, readonly) ASTextNode *titleNode; -@property (nonatomic, readonly) ASImageNode *imageNode; +@property (nonatomic, readonly) ASTextNode * _Nonnull titleNode; +@property (nonatomic, readonly) ASImageNode * _Nonnull imageNode; +@property (nonatomic, readonly) ASImageNode * _Nonnull backgroundImageNode; /** Spacing between image and title. Defaults to 8.0. @@ -36,11 +37,66 @@ */ @property (nonatomic, assign) ASVerticalAlignment contentVerticalAlignment; +/** + * Returns the styled title associated with the specified state. + * + * @param state The state that uses the styled title. The possible values are described in ASControlState. + * + * @return The title for the specified state. + */ +- (NSAttributedString * _Nullable)attributedTitleForState:(ASControlState)state; -- (NSAttributedString *)attributedTitleForState:(ASControlState)state; -- (void)setAttributedTitle:(NSAttributedString *)title forState:(ASControlState)state; +/** + * Sets the styled title to use for the specified state. + * + * @param title The styled text string to use for the title. + * @param state The state that uses the specified title. The possible values are described in ASControlState. + */ +- (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(ASControlState)state; -- (UIImage *)imageForState:(ASControlState)state; -- (void)setImage:(UIImage *)image forState:(ASControlState)state; +/** + * Sets the title to use for the specified state. + * + * @param title The styled text string to use for the title. + * @param font The font to use for the title. + * @param color The color to use for the title. + * @param state The state that uses the specified title. The possible values are described in ASControlState. + */ +- (void)setTitle:(nonnull NSString *)title withFont:(nullable UIFont *)font withColor:(nullable UIColor *)color forState:(ASControlState)state; + +/** + * Returns the image used for a button state. + * + * @param state The state that uses the image. Possible values are described in ASControlState. + * + * @return The image used for the specified state. + */ +- (UIImage * _Nullable)imageForState:(ASControlState)state; + +/** + * Sets the image to use for the specified state. + * + * @param image The image to use for the specified state. + * @param state The state that uses the specified title. The values are described in ASControlState. + */ +- (void)setImage:(nullable UIImage *)image forState:(ASControlState)state; + +/** + * Sets the background image to use for the specified state. + * + * @param image The image to use for the specified state. + * @param state The state that uses the specified title. The values are described in ASControlState. + */ +- (void)setBackgroundImage:(nullable UIImage *)image forState:(ASControlState)state; + + +/** + * Returns the background image used for a button state. + * + * @param state The state that uses the image. Possible values are described in ASControlState. + * + * @return The background image used for the specified state. + */ +- (UIImage * _Nullable)backgroundImageForState:(ASControlState)state; @end diff --git a/AsyncDisplayKit/ASButtonNode.mm b/AsyncDisplayKit/ASButtonNode.mm index 500d86271d..0dc3812f7a 100644 --- a/AsyncDisplayKit/ASButtonNode.mm +++ b/AsyncDisplayKit/ASButtonNode.mm @@ -10,6 +10,7 @@ #import "ASStackLayoutSpec.h" #import "ASThread.h" #import "ASDisplayNode+Subclasses.h" +#import "ASBackgroundLayoutSpec.h" @interface ASButtonNode () { @@ -24,6 +25,11 @@ UIImage *_highlightedImage; UIImage *_selectedImage; UIImage *_disabledImage; + + UIImage *_normalBackgroundImage; + UIImage *_highlightedBackgroundImage; + UIImage *_selectedBackgroundImage; + UIImage *_disabledBackgroundImage; } @end @@ -41,10 +47,12 @@ _titleNode = [[ASTextNode alloc] init]; _imageNode = [[ASImageNode alloc] init]; + _backgroundImageNode = [[ASImageNode alloc] init]; _contentHorizontalAlignment = ASAlignmentMiddle; _contentVerticalAlignment = ASAlignmentCenter; + [self addSubnode:_backgroundImageNode]; [self addSubnode:_titleNode]; [self addSubnode:_imageNode]; } @@ -54,20 +62,24 @@ - (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled]; - [self updateImage]; - [self updateTitle]; + [self updateButtonContent]; } - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; - [self updateImage]; - [self updateTitle]; + [self updateButtonContent]; } - (void)setSelected:(BOOL)selected { [super setSelected:selected]; + [self updateButtonContent]; +} + +- (void)updateButtonContent +{ + [self updateBackgroundImage]; [self updateImage]; [self updateTitle]; } @@ -113,6 +125,27 @@ } } +- (void)updateBackgroundImage +{ + ASDN::MutexLocker l(_propertyLock); + + UIImage *newImage; + if (self.enabled == NO && _disabledBackgroundImage) { + newImage = _disabledBackgroundImage; + } else if (self.highlighted && _highlightedBackgroundImage) { + newImage = _highlightedBackgroundImage; + } else if (self.selected && _selectedBackgroundImage) { + newImage = _selectedBackgroundImage; + } else { + newImage = _normalBackgroundImage; + } + + if (newImage != self.backgroundImageNode.image) { + self.backgroundImageNode.image = newImage; + [self setNeedsLayout]; + } +} + - (CGFloat)contentSpacing { ASDN::MutexLocker l(_propertyLock); @@ -145,6 +178,18 @@ [self setNeedsLayout]; } +- (void)setTitle:(NSString *)title withFont:(UIFont *)font withColor:(UIColor *)color forState:(ASControlState)state +{ + NSDictionary *attributes = @{ + NSFontAttributeName: font ? font :[UIFont systemFontOfSize:[UIFont buttonFontSize]], + NSForegroundColorAttributeName : color ? color : [UIColor blackColor] + }; + + NSAttributedString *string = [[NSAttributedString alloc] initWithString:title + attributes:attributes]; + [self setAttributedTitle:string forState:state]; +} + - (NSAttributedString *)attributedTitleForState:(ASControlState)state { ASDN::MutexLocker l(_propertyLock); @@ -239,6 +284,54 @@ [self updateImage]; } +- (void)setBackgroundImage:(UIImage *)image forState:(ASControlState)state +{ + ASDN::MutexLocker l(_propertyLock); + switch (state) { + case ASControlStateNormal: + _normalBackgroundImage = image; + break; + + case ASControlStateHighlighted: + _highlightedBackgroundImage = image; + break; + + case ASControlStateSelected: + _selectedBackgroundImage = image; + break; + + case ASControlStateDisabled: + _disabledBackgroundImage = image; + break; + + default: + break; + } + [self updateBackgroundImage]; +} + +- (UIImage *)backgroundImageForState:(ASControlState)state +{ + ASDN::MutexLocker l(_propertyLock); + switch (state) { + case ASControlStateNormal: + return _normalBackgroundImage; + + case ASControlStateHighlighted: + return _highlightedBackgroundImage; + + case ASControlStateSelected: + return _selectedBackgroundImage; + + case ASControlStateDisabled: + return _disabledBackgroundImage; + + default: + return _normalBackgroundImage; + } + +} + - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init]; @@ -258,12 +351,18 @@ stack.children = children; - return stack; + if (self.backgroundImageNode.image) { + return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:stack + background:self.backgroundImageNode]; + } else { + return stack; + } } - (void)layout { [super layout]; + self.backgroundImageNode.hidden = self.backgroundImageNode.image == nil; self.imageNode.hidden = self.imageNode.image == nil; self.titleNode.hidden = self.titleNode.attributedString.length > 0 == NO; } From 389945d69d4f8e3f18557322e28fa82338ca8e05 Mon Sep 17 00:00:00 2001 From: Rajinder Ramgarhia Date: Wed, 20 Jan 2016 14:29:34 -0500 Subject: [PATCH 07/18] ASButtonNode sets its subnodes as layer backed, but itelf should not be layer backed --- AsyncDisplayKit/ASButtonNode.h | 4 ++-- AsyncDisplayKit/ASButtonNode.mm | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASButtonNode.h b/AsyncDisplayKit/ASButtonNode.h index 5ce7d966cc..305121b3a6 100644 --- a/AsyncDisplayKit/ASButtonNode.h +++ b/AsyncDisplayKit/ASButtonNode.h @@ -47,7 +47,7 @@ - (NSAttributedString * _Nullable)attributedTitleForState:(ASControlState)state; /** - * Sets the styled title to use for the specified state. + * Sets the styled title to use for the specified state. This will reset styled title previously set with -setTitle:withFont:withColor:forState. * * @param title The styled text string to use for the title. * @param state The state that uses the specified title. The possible values are described in ASControlState. @@ -55,7 +55,7 @@ - (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(ASControlState)state; /** - * Sets the title to use for the specified state. + * Sets the title to use for the specified state. This will reset styled title previously set with -setAttributedTitle:forState. * * @param title The styled text string to use for the title. * @param font The font to use for the title. diff --git a/AsyncDisplayKit/ASButtonNode.mm b/AsyncDisplayKit/ASButtonNode.mm index 0dc3812f7a..c0abfe11ca 100644 --- a/AsyncDisplayKit/ASButtonNode.mm +++ b/AsyncDisplayKit/ASButtonNode.mm @@ -48,7 +48,12 @@ _titleNode = [[ASTextNode alloc] init]; _imageNode = [[ASImageNode alloc] init]; _backgroundImageNode = [[ASImageNode alloc] init]; + [_backgroundImageNode setContentMode:UIViewContentModeScaleToFill]; + [_titleNode setLayerBacked:YES]; + [_imageNode setLayerBacked:YES]; + [_backgroundImageNode setLayerBacked:YES]; + _contentHorizontalAlignment = ASAlignmentMiddle; _contentVerticalAlignment = ASAlignmentCenter; @@ -59,6 +64,12 @@ return self; } +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!layerBacked, @"ASButtonNode must not be layer backed!"); + [super setLayerBacked:layerBacked]; +} + - (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled]; From fe572c23fb0e6b9dfac3c7edca9b92066516ce18 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 23 Jan 2016 11:45:09 -0800 Subject: [PATCH 08/18] Exclude all of ASMultiplexImageNode from tvOS - for now. --- AsyncDisplayKit/ASMultiplexImageNode.h | 9 ++++++--- AsyncDisplayKit/ASMultiplexImageNode.mm | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index cac569d3c7..5c02ed6048 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -6,11 +6,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#if TARGET_OS_IOS + #import #import -#if TARGET_OS_IOS #import -#endif + NS_ASSUME_NONNULL_BEGIN @protocol ASMultiplexImageNodeDelegate; @@ -265,4 +266,6 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier @end #endif -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END + +#endif diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index fa58bb88f1..ff35ed3aec 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -6,12 +6,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "ASMultiplexImageNode.h" #if TARGET_OS_IOS + +#import "ASMultiplexImageNode.h" #import #import -#endif #import #import "ASAvailability.h" @@ -726,4 +726,6 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } @end -#endif \ No newline at end of file +#endif + +#endif From 2c5db2e335b09a08d990ed1c0786ec1e23c1b782 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 23 Jan 2016 13:05:27 -0800 Subject: [PATCH 09/18] Podfile bump to 1.9.6; minor preparations for release. --- AsyncDisplayKit.podspec | 7 ++++--- AsyncDisplayKit.xcodeproj/project.pbxproj | 17 ----------------- AsyncDisplayKit/ASVideoNode.h | 11 +++++++++++ AsyncDisplayKit/ASVideoNode.mm | 17 +++++++++++++---- .../Sample.xcodeproj/project.pbxproj | 16 ---------------- 5 files changed, 28 insertions(+), 40 deletions(-) diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 5698d550de..dfc8aad047 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '1.9.5' + spec.version = '1.9.6' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com', 'Ryan Nystrom' => 'rnystrom@fb.com' } spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' - spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.9.5' } + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.9.6' } spec.documentation_url = 'http://asyncdisplaykit.org/appledoc/' @@ -47,5 +47,6 @@ Pod::Spec.new do |spec| } spec.ios.deployment_target = '7.0' - spec.tvos.deployment_target = '9.0' + # tvOS not recognized by older versions of Cocoapods - add this only after tvOS support complete. + # spec.tvos.deployment_target = '9.0' end diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 861b362e96..04868896b6 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -1587,8 +1587,6 @@ 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, 3B9D88CDF51B429C8409E4B6 /* Copy Pods Resources */, - 527A806066E1F4E2795090DF /* Embed Pods Frameworks */, - 1B86F48711505F91D5FEF571 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -1718,21 +1716,6 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 527A806066E1F4E2795090DF /* 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-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index a01624f7d1..31b63545e2 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -1,3 +1,10 @@ +/* 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 @@ -9,6 +16,10 @@ typedef NS_ENUM(NSUInteger, ASVideoGravity) { @protocol ASVideoNodeDelegate; +// If you need ASVideoNode, please use AsyncDisplayKit master until this comment is removed. +// As of 1.9.6, ASVideoNode accidentally triggers creating the AVPlayerLayer even before playing +// the video. Using a lot of them intended to show static frame placeholders will be slow. + @interface ASVideoNode : ASControlNode @property (atomic, strong, readwrite) AVAsset *asset; @property (atomic, strong, readonly) AVPlayer *player; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 42d9d5ce36..e883ad4e52 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -1,7 +1,12 @@ - +/* 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 "ASVideoNode.h" -#import "ASDisplayNode+Beta.h" @interface ASVideoNode () { @@ -31,9 +36,13 @@ - (instancetype)init { - if (!(self = [super init])) { return nil; } + if (!(self = [super init])) { + return nil; + } - [ASDisplayNode setShouldUseNewRenderingRange:YES]; +#if DEBUG + NSLog(@"*** Warning: ASVideoNode is a new component - the 1.9.6 version may cause performance hiccups."); +#endif self.gravity = AVLayerVideoGravityResizeAspect; diff --git a/examples/VideoTableView/Sample.xcodeproj/project.pbxproj b/examples/VideoTableView/Sample.xcodeproj/project.pbxproj index c198b60ae4..f2ba462aca 100644 --- a/examples/VideoTableView/Sample.xcodeproj/project.pbxproj +++ b/examples/VideoTableView/Sample.xcodeproj/project.pbxproj @@ -128,7 +128,6 @@ 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - A5C135CBCFD74D965DE0D799 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -185,21 +184,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - A5C135CBCFD74D965DE0D799 /* 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; From 264887413e982c4094d8b1360cab4a97e91b6fa7 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 23 Jan 2016 13:35:04 -0800 Subject: [PATCH 10/18] Add @synchronized around UIImage draw to prevent concurrently decoding the same instance. This is a workaround necessary due to an iOS 9 bug. I'm not gating it to iOS 9 only so as to not exaggerate iOS version-specific behavior differences in the framework, since a majority are on iOS 9 anyway. If we can confirm the bug is fixed in a later iOS version, then it will be gated. Issue tracked in greater detail here: https://github.com/facebook/AsyncDisplayKit/issues/1068 --- AsyncDisplayKit/ASImageNode.mm | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 3191b53ab7..c9024c7510 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -235,7 +235,21 @@ UIRectFill({ .size = backingSize }); } - [image drawInRect:imageDrawRect]; + // iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on + // multiple threads concurrently. In fact, instead of crashing, it appears to deadlock. + // The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premier, + // as well as iOS games, and a small number of ASDK apps that provide the same image reference + // to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes + // that may get the same pointer for a given UI asset image, etc. + // FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and + // only if the object already exists in the set we should create a semaphore to signal waiting threads + // upon removal of the object from the set when the operation completes. + // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer. + // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068 + + @synchronized(image) { + [image drawInRect:imageDrawRect]; + } if (isCancelled()) { UIGraphicsEndImageContext(); From 3b4785cf24287b7b8226fe5c03c24654c650e526 Mon Sep 17 00:00:00 2001 From: Sam Stow Date: Sat, 23 Jan 2016 14:28:37 -0800 Subject: [PATCH 11/18] Added sample project using networking and infinite scrolling and random cats --- examples/CatDealsCollectionView/Podfile | 3 + .../Sample.xcodeproj/project.pbxproj | 405 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Sample.xcscheme | 88 ++++ .../contents.xcworkspacedata | 10 + .../Sample/AppDelegate.h | 20 + .../Sample/AppDelegate.m | 51 +++ .../CatDealsCollectionView/Sample/BlurbNode.h | 21 + .../CatDealsCollectionView/Sample/BlurbNode.m | 112 +++++ .../LaunchImage.launchimage/Contents.json | 39 ++ .../Default-568h@2x.png | Bin 0 -> 17520 bytes .../cat_face.imageset/Contents.json | 21 + .../cat_face.imageset/cat_face.png | Bin 0 -> 60445 bytes .../CatDealsCollectionView/Sample/Info.plist | 59 +++ .../CatDealsCollectionView/Sample/ItemNode.h | 21 + .../CatDealsCollectionView/Sample/ItemNode.m | 361 ++++++++++++++++ .../Sample/ItemStyles.h | 23 + .../Sample/ItemStyles.m | 93 ++++ .../Sample/ItemViewModel.h | 27 ++ .../Sample/ItemViewModel.m | 99 +++++ .../Sample/Launchboard.storyboard | 7 + .../Sample/LoadingNode.h | 15 + .../Sample/LoadingNode.m | 68 +++ .../Sample/PlaceholderNetworkImageNode.h | 15 + .../Sample/PlaceholderNetworkImageNode.m | 18 + .../Sample/PresentingViewController.h | 13 + .../Sample/PresentingViewController.m | 30 ++ .../Sample/ViewController.h | 16 + .../Sample/ViewController.m | 244 +++++++++++ examples/CatDealsCollectionView/Sample/main.m | 19 + .../contents.xcworkspacedata | 10 + 31 files changed, 1915 insertions(+) create mode 100644 examples/CatDealsCollectionView/Podfile create mode 100644 examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj create mode 100644 examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme create mode 100644 examples/CatDealsCollectionView/Sample.xcworkspace/contents.xcworkspacedata create mode 100644 examples/CatDealsCollectionView/Sample/AppDelegate.h create mode 100644 examples/CatDealsCollectionView/Sample/AppDelegate.m create mode 100644 examples/CatDealsCollectionView/Sample/BlurbNode.h create mode 100644 examples/CatDealsCollectionView/Sample/BlurbNode.m create mode 100644 examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json create mode 100644 examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png create mode 100644 examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json create mode 100644 examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png create mode 100644 examples/CatDealsCollectionView/Sample/Info.plist create mode 100644 examples/CatDealsCollectionView/Sample/ItemNode.h create mode 100644 examples/CatDealsCollectionView/Sample/ItemNode.m create mode 100644 examples/CatDealsCollectionView/Sample/ItemStyles.h create mode 100644 examples/CatDealsCollectionView/Sample/ItemStyles.m create mode 100644 examples/CatDealsCollectionView/Sample/ItemViewModel.h create mode 100644 examples/CatDealsCollectionView/Sample/ItemViewModel.m create mode 100644 examples/CatDealsCollectionView/Sample/Launchboard.storyboard create mode 100644 examples/CatDealsCollectionView/Sample/LoadingNode.h create mode 100644 examples/CatDealsCollectionView/Sample/LoadingNode.m create mode 100644 examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h create mode 100644 examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m create mode 100644 examples/CatDealsCollectionView/Sample/PresentingViewController.h create mode 100644 examples/CatDealsCollectionView/Sample/PresentingViewController.m create mode 100644 examples/CatDealsCollectionView/Sample/ViewController.h create mode 100644 examples/CatDealsCollectionView/Sample/ViewController.m create mode 100644 examples/CatDealsCollectionView/Sample/main.m create mode 100644 examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata diff --git a/examples/CatDealsCollectionView/Podfile b/examples/CatDealsCollectionView/Podfile new file mode 100644 index 0000000000..6c012e3c04 --- /dev/null +++ b/examples/CatDealsCollectionView/Podfile @@ -0,0 +1,3 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +pod 'AsyncDisplayKit', :path => '../..' diff --git a/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj b/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1810a8ebbf --- /dev/null +++ b/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,405 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FDEC911BF31EE700CEB123 /* ItemNode.m */; }; + 7A83848E1C34359D002CDD08 /* ItemViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A83848D1C34359D002CDD08 /* ItemViewModel.m */; }; + 7A8384941C343680002CDD08 /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A8384921C343680002CDD08 /* BlurbNode.m */; }; + 7A8384971C344057002CDD08 /* ItemStyles.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A8384961C344057002CDD08 /* ItemStyles.m */; }; + 7ACD5F891C415B7500E7BE16 /* LoadingNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */; }; + 7ACD5F961C4847C000E7BE16 /* PlaceholderNetworkImageNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; + AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + FABD6D156A3EB118497E5CE6 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F02BAF78E68BC56FD8C161B7 /* libPods.a */; }; + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 25FDEC901BF31EE700CEB123 /* ItemNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemNode.h; sourceTree = ""; }; + 25FDEC911BF31EE700CEB123 /* ItemNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemNode.m; sourceTree = ""; }; + 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 7A83848C1C34359D002CDD08 /* ItemViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemViewModel.h; sourceTree = ""; }; + 7A83848D1C34359D002CDD08 /* ItemViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemViewModel.m; sourceTree = ""; }; + 7A8384911C343680002CDD08 /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 7A8384921C343680002CDD08 /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 7A8384951C344057002CDD08 /* ItemStyles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemStyles.h; sourceTree = ""; }; + 7A8384961C344057002CDD08 /* ItemStyles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemStyles.m; sourceTree = ""; }; + 7ACD5F871C415B7500E7BE16 /* LoadingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoadingNode.h; sourceTree = ""; }; + 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoadingNode.m; sourceTree = ""; }; + 7ACD5F941C4847C000E7BE16 /* PlaceholderNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaceholderNetworkImageNode.h; sourceTree = ""; }; + 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaceholderNetworkImageNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; + AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AC3C4A651A11F47200143C57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AC3C4A661A11F47200143C57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AC3C4A681A11F47200143C57 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + CD1ABB23007FEDB31D8C1978 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + F02BAF78E68BC56FD8C161B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PresentingViewController.h; sourceTree = ""; }; + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PresentingViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC3C4A5B1A11F47200143C57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FABD6D156A3EB118497E5CE6 /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 90A2B9C5397C46134C8A793B /* Pods */ = { + isa = PBXGroup; + children = ( + 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */, + CD1ABB23007FEDB31D8C1978 /* Pods.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AC3C4A551A11F47200143C57 = { + isa = PBXGroup; + children = ( + AC3C4A601A11F47200143C57 /* Sample */, + AC3C4A5F1A11F47200143C57 /* Products */, + 90A2B9C5397C46134C8A793B /* Pods */, + D6E38FF0CB18E3F55CF06437 /* Frameworks */, + ); + sourceTree = ""; + }; + AC3C4A5F1A11F47200143C57 /* Products */ = { + isa = PBXGroup; + children = ( + AC3C4A5E1A11F47200143C57 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + AC3C4A601A11F47200143C57 /* Sample */ = { + isa = PBXGroup; + children = ( + AC3C4A651A11F47200143C57 /* AppDelegate.h */, + AC3C4A661A11F47200143C57 /* AppDelegate.m */, + AC3C4A681A11F47200143C57 /* ViewController.h */, + AC3C4A691A11F47200143C57 /* ViewController.m */, + 7ACD5F941C4847C000E7BE16 /* PlaceholderNetworkImageNode.h */, + 7ACD5F951C4847C000E7BE16 /* PlaceholderNetworkImageNode.m */, + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */, + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */, + AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, + AC3C4A611A11F47200143C57 /* Supporting Files */, + 25FDEC901BF31EE700CEB123 /* ItemNode.h */, + 25FDEC911BF31EE700CEB123 /* ItemNode.m */, + 7A8384951C344057002CDD08 /* ItemStyles.h */, + 7A8384961C344057002CDD08 /* ItemStyles.m */, + 7A83848C1C34359D002CDD08 /* ItemViewModel.h */, + 7A83848D1C34359D002CDD08 /* ItemViewModel.m */, + 7A8384911C343680002CDD08 /* BlurbNode.h */, + 7A8384921C343680002CDD08 /* BlurbNode.m */, + 7ACD5F871C415B7500E7BE16 /* LoadingNode.h */, + 7ACD5F881C415B7500E7BE16 /* LoadingNode.m */, + ); + indentWidth = 2; + path = Sample; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + AC3C4A611A11F47200143C57 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + AC3C4A621A11F47200143C57 /* Info.plist */, + AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D6E38FF0CB18E3F55CF06437 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F02BAF78E68BC56FD8C161B7 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC3C4A5D1A11F47200143C57 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + F868CFBB21824CC9521B6588 /* Check Pods Manifest.lock */, + AC3C4A5A1A11F47200143C57 /* Sources */, + AC3C4A5B1A11F47200143C57 /* Frameworks */, + AC3C4A5C1A11F47200143C57 /* Resources */, + A6902C454C7661D0D277AC62 /* Copy Pods Resources */, + B4CD33E927E6F4EE5DD6CCF0 /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = AC3C4A5E1A11F47200143C57 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC3C4A561A11F47200143C57 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + AC3C4A5D1A11F47200143C57 = { + CreatedOnToolsVersion = 6.1; + }; + }; + }; + buildConfigurationList = AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC3C4A551A11F47200143C57; + productRefGroup = AC3C4A5F1A11F47200143C57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC3C4A5D1A11F47200143C57 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC3C4A5C1A11F47200143C57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, + AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A6902C454C7661D0D277AC62 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + B4CD33E927E6F4EE5DD6CCF0 /* 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; + }; + F868CFBB21824CC9521B6588 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC3C4A5A1A11F47200143C57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */, + 7ACD5F891C415B7500E7BE16 /* LoadingNode.m in Sources */, + AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 7A8384971C344057002CDD08 /* ItemStyles.m in Sources */, + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */, + 7A8384941C343680002CDD08 /* BlurbNode.m in Sources */, + 7A83848E1C34359D002CDD08 /* ItemViewModel.m in Sources */, + AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, + 7ACD5F961C4847C000E7BE16 /* PlaceholderNetworkImageNode.m in Sources */, + AC3C4A641A11F47200143C57 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AC3C4A7F1A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A801A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC3C4A821A11F47200143C57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC3C4A831A11F47200143C57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CD1ABB23007FEDB31D8C1978 /* Pods.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC3C4A591A11F47200143C57 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A7F1A11F47200143C57 /* Debug */, + AC3C4A801A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC3C4A811A11F47200143C57 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC3C4A821A11F47200143C57 /* Debug */, + AC3C4A831A11F47200143C57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AC3C4A561A11F47200143C57 /* Project object */; +} diff --git a/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..f49edc75d6 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CatDealsCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/CatDealsCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/CatDealsCollectionView/Sample/AppDelegate.h b/examples/CatDealsCollectionView/Sample/AppDelegate.h new file mode 100644 index 0000000000..80b7fb3d50 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/AppDelegate.h @@ -0,0 +1,20 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +#define SIMULATE_WEB_RESPONSE 0 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/CatDealsCollectionView/Sample/AppDelegate.m b/examples/CatDealsCollectionView/Sample/AppDelegate.m new file mode 100644 index 0000000000..adab692baf --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/AppDelegate.m @@ -0,0 +1,51 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "AppDelegate.h" + +#import "PresentingViewController.h" +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] init]; + + [self pushNewViewControllerAnimated:NO]; + + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)pushNewViewControllerAnimated:(BOOL)animated +{ + UINavigationController *navController = (UINavigationController *)self.window.rootViewController; + +#if SIMULATE_WEB_RESPONSE + UIViewController *viewController = [[PresentingViewController alloc] init]; +#else + UIViewController *viewController = [[ViewController alloc] init]; + viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Another Copy" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +#endif + + [navController pushViewController:viewController animated:animated]; +} + +- (void)pushNewViewController +{ + [self pushNewViewControllerAnimated:YES]; +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/BlurbNode.h b/examples/CatDealsCollectionView/Sample/BlurbNode.h new file mode 100644 index 0000000000..efe58505e1 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/BlurbNode.h @@ -0,0 +1,21 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + ++ (CGFloat)desiredHeightForWidth:(CGFloat)width; + +@end diff --git a/examples/CatDealsCollectionView/Sample/BlurbNode.m b/examples/CatDealsCollectionView/Sample/BlurbNode.m new file mode 100644 index 0000000000..68dcf866bc --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/BlurbNode.m @@ -0,0 +1,112 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BlurbNode.h" + +#import +#import + +#import +#import + +static CGFloat kFixedHeight = 75.0f; +static CGFloat kTextPadding = 10.0f; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + ++ (CGFloat)desiredHeightForWidth:(CGFloat)width { + return kFixedHeight; +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + self.backgroundColor = [UIColor lightGrayColor]; + // create a text node + _textNode = [[ASTextNode alloc] init]; + _textNode.maximumNumberOfLines = 2; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"Kittens courtesy lorempixel.com \U0001F638 \nTitles courtesy of catipsum.com"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + NSLinkAttributeName: [NSURL URLWithString:@"http://lorempixel.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } + range:[blurb rangeOfString:@"lorempixel.com"]]; + [string addAttributes:@{ + NSLinkAttributeName: [NSURL URLWithString:@"http://www.catipsum.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } range:[blurb rangeOfString:@"catipsum.com"]]; + _textNode.attributedString = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} + + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json b/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..f0fce54771 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,39 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "scale" : "1x", + "orientation" : "portrait" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "orientation" : "portrait" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1547a98454a45fe958e45470a1e9594e3a8906fd GIT binary patch literal 17520 zcmeI3&u`;I6vw9vhb5>d4j{zA$_fDzj(<0aNm5GErIkpxYKyiNhuw)iPOK)jv7ILE zX$2=vNSqMjuzvyn01^l!&fGb11%VKUJ#d1togeN?GIZ5NXr+nN#&6zy-^_g8%(HXw zC;NNvt`}Y|AcWR;cUlJs-FSe|v%$?9wB^z7;a&RmT(ENzA@uwU$=@3K;>(v1dh1j7 z=r}%Zzh_$hpoDF|LrUX8Kzk!p+Z+ejIwi5tAzjz27ytPB+oIsw_2ONlEw_Uv>A5=> zAvwI*JF+fLt*TwzY!qr^lLi=&7z^V;-;2y~y_m)|>2=a96@|1*d|EGVB?Ah_?R}x? zheR++hG@x(E|jY!#ZdH$@}{85iYCdLq-dg|nsU`t3}NyqHVX8r7TS(^(7HVtj#~9% zFOCCKl18IZX{43>uq!FmYE_a|NmWJKLyXS77>`9Sx|Ic)#%Ynr3f&-feNRZ@;*LLz z>&0R+(Cjl=*C3b;&fF^pra5W1XJap&?_jWW=qTPkqX;HQmnx&9^MFVPB=U!$MYhlAQf@66Bj z|2*0AP^5)p(x97B7Zq7l^dnU^wW=wXMY(LsaxO^L6U1oyw(FcPiJ@0aSV{SoJJ@kVxmu}Iuwgikil?D-@`ccMLYI~~+|Tt%X8*WD zED5tP)Q%g`&3T^Nv~=sHh3t@gc)4}z=(}k_3UC;a_F@aa3oW;+{SC2935s!7CvQTNS)j=ZPiAOjjTV1**{`H_| zAunrS?3$F+{l5DjWFf+5*UNz+E2@mEy0~GKv8b12B32ERh~*YY# zOjVy2r2i=g^Zje}u-7GaLz*sY+VE7R?Mk)pnrX}5N1pbyWoPhmDV$dD#nM?z9v)0u zc4-N~X}MEs(n8yO!({!Blk(Xgrv1yROh1y6{GUkkZYVurOY?3hoh#q>+_*t+dFez! zb54~!;beEB3SG^ek1L%$16wZRDjn z*F>6SEBEA_md>n|K9|#<1k&s`!9rD?-Fcqm#e7XqGAfA02LV9XCdb9*1H2p;0Kzso zE;b+F<+uP4w#jj^`2a7+1%R+kj*HC)csVWrgl%$MY(ButaRDG~ljCCZ0bY&^0AZUP z7n=|8a$Eoi+vK>|e1MnZ0zlX%$HnFYyc`z*!ZtZBHXq>SxBw8g$#Jpy058V{fUr%D zi_HgkIW7Q%ZE{>}KETUy0U&IX<6`pxUXBX@VVfKmn-B1ETmT5$HaRXfAK>M<01&pxak2RTFUJLduuUee!u%~;;?Y-YNAw-q_19i`o4$W5 zSUU%8gf3o1=)uPb{q;Bf{sp0-jL@(55i&nV=tcjtdq3Pp=;jBzt*xW+PrrSqtWo9n z4mH~~(86W|0m0Z!mC3H;oB2JPKLYv26X|1**9Ztu0edguN}{{Z>Z BpTqzF literal 0 HcmV?d00001 diff --git a/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json b/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json new file mode 100644 index 0000000000..a9e3a5b11b --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "cat_face.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png b/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4407212c3dcd43ee231d134f2005073ebe0cd5 GIT binary patch literal 60445 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 z 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 EKc}javj6}9 literal 0 HcmV?d00001 diff --git a/examples/CatDealsCollectionView/Sample/Info.plist b/examples/CatDealsCollectionView/Sample/Info.plist new file mode 100644 index 0000000000..8c57e7a83d --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/Info.plist @@ -0,0 +1,59 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + http://lorempixel.com + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Launchboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/CatDealsCollectionView/Sample/ItemNode.h b/examples/CatDealsCollectionView/Sample/ItemNode.h new file mode 100644 index 0000000000..eee5dc5dc9 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemNode.h @@ -0,0 +1,21 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import "ItemViewModel.h" + +@interface ItemNode : ASCellNode + +- initWithViewModel:(ItemViewModel *)viewModel; ++ (CGSize)sizeForWidth:(CGFloat)width; ++ (CGSize)preferredViewSize; + +@end diff --git a/examples/CatDealsCollectionView/Sample/ItemNode.m b/examples/CatDealsCollectionView/Sample/ItemNode.m new file mode 100644 index 0000000000..15608a7620 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemNode.m @@ -0,0 +1,361 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "ItemNode.h" +#import "ItemStyles.h" +#import "PlaceholderNetworkImageNode.h" + +const CGFloat kFixedLabelsAreaHeight = 96.0; +const CGFloat kDesignWidth = 320.0; +const CGFloat kDesignHeight = 299.0; +const CGFloat kBadgeHeight = 34.0; +const CGFloat kSoldOutGBHeight = 50.0; + +@interface ItemNode() + +@property (nonatomic, strong) ItemViewModel *viewModel; + +@property (nonatomic, strong) PlaceholderNetworkImageNode *dealImageView; + +@property (nonatomic, strong) ASTextNode *titleLabel; +@property (nonatomic, strong) ASTextNode *firstInfoLabel; +@property (nonatomic, strong) ASTextNode *distanceLabel; +@property (nonatomic, strong) ASTextNode *secondInfoLabel; +@property (nonatomic, strong) ASTextNode *originalPriceLabel; +@property (nonatomic, strong) ASTextNode *finalPriceLabel; +@property (nonatomic, strong) ASTextNode *soldOutLabelFlat; +@property (nonatomic, strong) ASDisplayNode *soldOutLabelBackground; +@property (nonatomic, strong) ASDisplayNode *soldOutOverlay; +@property (nonatomic, strong) ASTextNode *badge; + +@end + +@implementation ItemNode + +- (instancetype)initWithViewModel:(ItemViewModel *)viewModel +{ + self = [super init]; + if (self != nil) { + _viewModel = viewModel; + [self setup]; + [self updateLabels]; + [self updateBackgroundColor]; + + } + return self; +} + ++ (BOOL)isRTL { + return [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; +} + +- (void)setup { + self.dealImageView = [[PlaceholderNetworkImageNode alloc] init]; + self.dealImageView.delegate = self; + self.dealImageView.placeholderEnabled = YES; + self.dealImageView.placeholderImageOverride = [ItemStyles placeholderImage]; + self.dealImageView.defaultImage = [ItemStyles placeholderImage]; + self.dealImageView.contentMode = UIViewContentModeScaleToFill; + self.dealImageView.placeholderFadeDuration = 0.0; + self.dealImageView.layerBacked = YES; + + self.titleLabel = [[ASTextNode alloc] init]; + self.titleLabel.maximumNumberOfLines = 2; + self.titleLabel.alignSelf = ASStackLayoutAlignSelfStart; + self.titleLabel.flexGrow = YES; + self.titleLabel.layerBacked = YES; + + self.firstInfoLabel = [[ASTextNode alloc] init]; + self.firstInfoLabel.maximumNumberOfLines = 1; + self.firstInfoLabel.layerBacked = YES; + + self.secondInfoLabel = [[ASTextNode alloc] init]; + self.secondInfoLabel.maximumNumberOfLines = 1; + self.secondInfoLabel.layerBacked = YES; + + self.distanceLabel = [[ASTextNode alloc] init]; + self.distanceLabel.maximumNumberOfLines = 1; + self.distanceLabel.layerBacked = YES; + + self.originalPriceLabel = [[ASTextNode alloc] init]; + self.originalPriceLabel.maximumNumberOfLines = 1; + self.originalPriceLabel.layerBacked = YES; + + self.finalPriceLabel = [[ASTextNode alloc] init]; + self.finalPriceLabel.maximumNumberOfLines = 1; + self.finalPriceLabel.layerBacked = YES; + + self.badge = [[ASTextNode alloc] init]; + self.badge.hidden = YES; + self.badge.layerBacked = YES; + + self.soldOutLabelFlat = [[ASTextNode alloc] init]; + self.soldOutLabelFlat.layerBacked = YES; + + self.soldOutLabelBackground = [[ASDisplayNode alloc] init]; + self.soldOutLabelBackground.sizeRange = ASRelativeSizeRangeMake(ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(1), ASRelativeDimensionMakeWithPoints(kSoldOutGBHeight)), ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(1), ASRelativeDimensionMakeWithPoints(kSoldOutGBHeight))); + self.soldOutLabelBackground.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; + self.soldOutLabelBackground.flexGrow = YES; + self.soldOutLabelBackground.layerBacked = YES; + + self.soldOutOverlay = [[ASDisplayNode alloc] init]; + self.soldOutOverlay.flexGrow = YES; + self.soldOutOverlay.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5]; + self.soldOutOverlay.layerBacked = YES; + + [self addSubnode:self.dealImageView]; + [self addSubnode:self.titleLabel]; + [self addSubnode:self.firstInfoLabel]; + [self addSubnode:self.secondInfoLabel]; + [self addSubnode:self.originalPriceLabel]; + [self addSubnode:self.finalPriceLabel]; + [self addSubnode:self.distanceLabel]; + [self addSubnode:self.badge]; + + [self addSubnode:self.soldOutLabelBackground]; + [self addSubnode:self.soldOutLabelFlat]; + [self addSubnode:self.soldOutOverlay]; + self.soldOutOverlay.hidden = YES; + self.soldOutLabelBackground.hidden = YES; + self.soldOutLabelFlat.hidden = YES; + + [self addSubnode:self.soldOutOverlay]; + + if ([ItemNode isRTL]) { + self.titleLabel.alignSelf = ASStackLayoutAlignSelfEnd; + self.firstInfoLabel.alignSelf = ASStackLayoutAlignSelfEnd; + self.distanceLabel.alignSelf = ASStackLayoutAlignSelfEnd; + self.secondInfoLabel.alignSelf = ASStackLayoutAlignSelfEnd; + self.originalPriceLabel.alignSelf = ASStackLayoutAlignSelfStart; + self.finalPriceLabel.alignSelf = ASStackLayoutAlignSelfStart; + } else { + self.firstInfoLabel.alignSelf = ASStackLayoutAlignSelfStart; + self.distanceLabel.alignSelf = ASStackLayoutAlignSelfStart; + self.secondInfoLabel.alignSelf = ASStackLayoutAlignSelfStart; + self.originalPriceLabel.alignSelf = ASStackLayoutAlignSelfEnd; + self.finalPriceLabel.alignSelf = ASStackLayoutAlignSelfEnd; + } +} + +- (void)updateLabels { + // Set Title text + if (self.viewModel.titleText) { + self.titleLabel.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.titleText attributes:[ItemStyles titleStyle]]; + } + if (self.viewModel.firstInfoText) { + self.firstInfoLabel.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.firstInfoText attributes:[ItemStyles subtitleStyle]]; + } + + if (self.viewModel.secondInfoText) { + self.secondInfoLabel.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.secondInfoText attributes:[ItemStyles secondInfoStyle]]; + } + if (self.viewModel.originalPriceText) { + self.originalPriceLabel.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.originalPriceText attributes:[ItemStyles originalPriceStyle]]; + } + if (self.viewModel.finalPriceText) { + self.finalPriceLabel.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.finalPriceText attributes:[ItemStyles finalPriceStyle]]; + } + if (self.viewModel.distanceLabelText) { + NSString *format = [ItemNode isRTL] ? @"%@ •" : @"• %@"; + NSString *distanceText = [NSString stringWithFormat:format, self.viewModel.distanceLabelText]; + + self.distanceLabel.attributedString = [[NSAttributedString alloc] initWithString:distanceText attributes:[ItemStyles distanceStyle]]; + } + + BOOL isSoldOut = self.viewModel.soldOutText != nil; + + if (isSoldOut) { + NSString *soldOutText = self.viewModel.soldOutText; + self.soldOutLabelFlat.attributedString = [[NSAttributedString alloc] initWithString:soldOutText attributes:[ItemStyles soldOutStyle]]; + } + self.soldOutOverlay.hidden = !isSoldOut; + self.soldOutLabelFlat.hidden = !isSoldOut; + self.soldOutLabelBackground.hidden = !isSoldOut; + + BOOL hasBadge = self.viewModel.badgeText != nil; + if (hasBadge) { + self.badge.attributedString = [[NSAttributedString alloc] initWithString:self.viewModel.badgeText attributes:[ItemStyles badgeStyle]]; + self.badge.backgroundColor = [ItemStyles badgeColor]; + } + self.badge.hidden = !hasBadge; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.3]; + } else if (self.selected) { + self.backgroundColor = [UIColor lightGrayColor]; + } else { + self.backgroundColor = [UIColor whiteColor]; + } +} + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image { +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + +#pragma mark - superclass + +- (void)displayWillStart { + [super displayWillStart]; + [self fetchData]; +} + +- (void)fetchData { + [super fetchData]; + if (self.viewModel) { + [self loadImage]; + } +} + + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { + + ASLayoutSpec *textSpec = [self textSpec]; + ASLayoutSpec *imageSpec = [self imageSpecWithSize:constrainedSize]; + ASOverlayLayoutSpec *soldOutOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imageSpec overlay:[self soldOutLabelSpec]]; + + NSArray *stackChildren = @[soldOutOverImage, textSpec]; + + ASStackLayoutSpec *mainStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:stackChildren]; + + ASOverlayLayoutSpec *soldOutOverlay = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:mainStack overlay:self.soldOutOverlay]; + + return soldOutOverlay; +} + +- (ASLayoutSpec *)textSpec { + CGFloat kInsetHorizontal = 16.0; + CGFloat kInsetTop = 6.0; + CGFloat kInsetBottom = 0.0; + + UIEdgeInsets textInsets = UIEdgeInsetsMake(kInsetTop, kInsetHorizontal, kInsetBottom, kInsetHorizontal); + + ASLayoutSpec *verticalSpacer = [[ASLayoutSpec alloc] init]; + verticalSpacer.flexGrow = YES; + + ASLayoutSpec *horizontalSpacer1 = [[ASLayoutSpec alloc] init]; + horizontalSpacer1.flexGrow = YES; + + ASLayoutSpec *horizontalSpacer2 = [[ASLayoutSpec alloc] init]; + horizontalSpacer2.flexGrow = YES; + + NSArray *info1Children = @[self.firstInfoLabel, self.distanceLabel, horizontalSpacer1, self.originalPriceLabel]; + NSArray *info2Children = @[self.secondInfoLabel, horizontalSpacer2, self.finalPriceLabel]; + if ([ItemNode isRTL]) { + info1Children = [[info1Children reverseObjectEnumerator] allObjects]; + info2Children = [[info2Children reverseObjectEnumerator] allObjects]; + } + + ASStackLayoutSpec *info1Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:1.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsBaselineLast children:info1Children]; + + ASStackLayoutSpec *info2Stack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentCenter alignItems:ASStackLayoutAlignItemsBaselineLast children:info2Children]; + + ASStackLayoutSpec *textStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0.0 justifyContent:ASStackLayoutJustifyContentEnd alignItems:ASStackLayoutAlignItemsStretch children:@[self.titleLabel, verticalSpacer, info1Stack, info2Stack]]; + + ASInsetLayoutSpec *textWrapper = [ASInsetLayoutSpec insetLayoutSpecWithInsets:textInsets child:textStack]; + textWrapper.flexGrow = YES; + + return textWrapper; +} + +- (ASLayoutSpec *)imageSpecWithSize:(ASSizeRange)constrainedSize { + CGFloat imageRatio = [self imageRatioFromSize:constrainedSize.max]; + + ASRatioLayoutSpec *imagePlace = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:imageRatio child:self.dealImageView]; + + self.badge.layoutPosition = CGPointMake(0, constrainedSize.max.height - kFixedLabelsAreaHeight - kBadgeHeight); + self.badge.sizeRange = ASRelativeSizeRangeMake(ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(0), ASRelativeDimensionMakeWithPoints(kBadgeHeight)), ASRelativeSizeMake(ASRelativeDimensionMakeWithPercent(1), ASRelativeDimensionMakeWithPoints(kBadgeHeight))); + ASStaticLayoutSpec *badgePosition = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[self.badge]]; + + ASOverlayLayoutSpec *badgeOverImage = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:imagePlace overlay:badgePosition]; + badgeOverImage.flexGrow = YES; + + return badgeOverImage; +} + +- (ASLayoutSpec *)soldOutLabelSpec { + ASCenterLayoutSpec *centerSoldOutLabel = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:self.soldOutLabelFlat]; + ASStaticLayoutSpec *soldOutBG = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[self.soldOutLabelBackground]]; + ASCenterLayoutSpec *centerSoldOut = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:soldOutBG]; + ASBackgroundLayoutSpec *soldOutLabelOverBackground = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:centerSoldOutLabel background:centerSoldOut]; + return soldOutLabelOverBackground; +} + + ++ (CGSize)sizeForWidth:(CGFloat)width { + CGFloat height = [self scaledHeightForPreferredSize:[self preferredViewSize] scaledWidth:width]; + return CGSizeMake(width, height); +} + + ++ (CGSize)preferredViewSize { + return CGSizeMake(kDesignWidth, kDesignHeight); +} + ++ (CGFloat)scaledHeightForPreferredSize:(CGSize)preferredSize scaledWidth:(CGFloat)scaledWidth { + CGFloat scale = scaledWidth / kDesignWidth; + CGFloat scaledHeight = ceilf(scale * (kDesignHeight - kFixedLabelsAreaHeight)) + kFixedLabelsAreaHeight; + + return scaledHeight; +} + +#pragma mark - view operations + +- (CGFloat)imageRatioFromSize:(CGSize)size { + CGFloat imageHeight = size.height - kFixedLabelsAreaHeight; + CGFloat imageRatio = imageHeight / size.width; + + return imageRatio; +} + +- (CGSize)imageSize { + if (!CGSizeEqualToSize(self.dealImageView.frame.size, CGSizeZero)) { + return self.dealImageView.frame.size; + } else if (!CGSizeEqualToSize(self.calculatedSize, CGSizeZero)) { + CGFloat imageRatio = [self imageRatioFromSize:self.calculatedSize]; + CGFloat imageWidth = self.calculatedSize.width; + return CGSizeMake(imageWidth, imageRatio * imageWidth); + } else { + return CGSizeZero; + } +} + +- (void)loadImage { + CGSize imageSize = [self imageSize]; + if (CGSizeEqualToSize(CGSizeZero, imageSize)) { + return; + } + + NSURL *url = [self.viewModel imageURLWithSize:imageSize]; + + // if we're trying to set the deal image to what it already was, skip the work + if ([[url absoluteString] isEqualToString:[self.dealImageView.URL absoluteString]]) { + return; + } + + // Clear the flag that says we've loaded our image + [self.dealImageView setURL:url]; +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/ItemStyles.h b/examples/CatDealsCollectionView/Sample/ItemStyles.h new file mode 100644 index 0000000000..67879f3925 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemStyles.h @@ -0,0 +1,23 @@ +// +// ItemStyles.h +// Sample +// +// Created by Samuel Stow on 12/30/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import +#import + +@interface ItemStyles : NSObject ++ (NSDictionary *)titleStyle; ++ (NSDictionary *)subtitleStyle; ++ (NSDictionary *)distanceStyle; ++ (NSDictionary *)secondInfoStyle; ++ (NSDictionary *)originalPriceStyle; ++ (NSDictionary *)finalPriceStyle; ++ (NSDictionary *)soldOutStyle; ++ (NSDictionary *)badgeStyle; ++ (UIColor *)badgeColor; ++ (UIImage *)placeholderImage; +@end diff --git a/examples/CatDealsCollectionView/Sample/ItemStyles.m b/examples/CatDealsCollectionView/Sample/ItemStyles.m new file mode 100644 index 0000000000..3b39068452 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemStyles.m @@ -0,0 +1,93 @@ +// +// ItemStyles.m +// Sample +// +// Created by Samuel Stow on 12/30/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "ItemStyles.h" + +const CGFloat kTitleFontSize = 20.0; +const CGFloat kInfoFontSize = 14.0; + +UIColor *kTitleColor; +UIColor *kInfoColor; +UIColor *kFinalPriceColor; +UIFont *kTitleFont; +UIFont *kInfoFont; + +@implementation ItemStyles + ++ (void)initialize { + if (self == [ItemStyles class]) { + kTitleColor = [UIColor darkGrayColor]; + kInfoColor = [UIColor grayColor]; + kFinalPriceColor = [UIColor greenColor]; + kTitleFont = [UIFont boldSystemFontOfSize:kTitleFontSize]; + kInfoFont = [UIFont systemFontOfSize:kInfoFontSize]; + } +} + ++ (NSDictionary *)titleStyle { + // Title Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kTitleColor }; +} + ++ (NSDictionary *)subtitleStyle { + // First Subtitle + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor }; +} + ++ (NSDictionary *)distanceStyle { + // Distance Label + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor}; +} + ++ (NSDictionary *)secondInfoStyle { + // Second Subtitle + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor}; +} + ++ (NSDictionary *)originalPriceStyle { + // Original price + return @{ NSFontAttributeName:kInfoFont, + NSForegroundColorAttributeName:kInfoColor, + NSStrikethroughStyleAttributeName:@(NSUnderlineStyleSingle)}; +} + ++ (NSDictionary *)finalPriceStyle { + // Discounted / Claimable price label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kFinalPriceColor}; +} + ++ (NSDictionary *)soldOutStyle { + // Setup Sold Out Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:kTitleColor}; +} + ++ (NSDictionary *)badgeStyle { + // Setup Sold Out Label + return @{ NSFontAttributeName:kTitleFont, + NSForegroundColorAttributeName:[UIColor whiteColor]}; +} + ++ (UIColor *)badgeColor { + return [[UIColor purpleColor] colorWithAlphaComponent:0.4]; +} + ++ (UIImage *)placeholderImage { + static UIImage *__catFace = nil; + if (!__catFace) { + __catFace = [UIImage imageNamed:@"cat_face"]; + } + return __catFace; +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/ItemViewModel.h b/examples/CatDealsCollectionView/Sample/ItemViewModel.h new file mode 100644 index 0000000000..1d0403331c --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemViewModel.h @@ -0,0 +1,27 @@ +// +// GPDealViewModel.h +// Groupon +// +// Created by Samuel Stow on 12/29/15. +// Copyright © 2015 Groupon Inc. All rights reserved. +// + +#import +#import + +@interface ItemViewModel : NSObject + ++ (instancetype)randomItem; + +@property (nonatomic, copy) NSString *titleText; +@property (nonatomic, copy) NSString *firstInfoText; +@property (nonatomic, copy) NSString *secondInfoText; +@property (nonatomic, copy) NSString *originalPriceText; +@property (nonatomic, copy) NSString *finalPriceText; +@property (nonatomic, copy) NSString *soldOutText; +@property (nonatomic, copy) NSString *distanceLabelText; +@property (nonatomic, copy) NSString *badgeText; + +- (NSURL *)imageURLWithSize:(CGSize)size; + +@end diff --git a/examples/CatDealsCollectionView/Sample/ItemViewModel.m b/examples/CatDealsCollectionView/Sample/ItemViewModel.m new file mode 100644 index 0000000000..6ad4c1d13e --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ItemViewModel.m @@ -0,0 +1,99 @@ +// +// GPDealViewModel.m +// Groupon +// +// Created by Samuel Stow on 12/29/15. +// Copyright © 2015 Groupon Inc. All rights reserved. +// + +#import "ItemViewModel.h" + +NSArray *titles; +NSArray *firstInfos; +NSArray *badges; + +@interface ItemViewModel() + +@property (nonatomic, assign) NSInteger catNumber; +@property (nonatomic, assign) NSInteger labelNumber; + +@end + +@implementation ItemViewModel + ++ (instancetype)randomItem { + return [[ItemViewModel alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _titleText = [self randomObjectFromArray:titles]; + _firstInfoText = [self randomObjectFromArray:firstInfos]; + _secondInfoText = [NSString stringWithFormat:@"%zd+ bought", [self randomNumberInRange:5 to:6000]]; + _originalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:40 to:90]]; + _finalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:5 to:30]]; + BOOL isSoldOut = arc4random() % 5 == 0; + _soldOutText = isSoldOut ? @"SOLD OUT" : nil; + _distanceLabelText = [NSString stringWithFormat:@"%zd mi", [self randomNumberInRange:1 to:20]]; + BOOL isBadged = arc4random() % 2 == 0; + if (isBadged) { + _badgeText = [self randomObjectFromArray:badges]; + } + _catNumber = [self randomNumberInRange:1 to:10]; + _labelNumber = [self randomNumberInRange:1 to:10000]; + + } + return self; +} + +- (NSURL *)imageURLWithSize:(CGSize)size { + NSString *imageText = [NSString stringWithFormat:@"Fun cat pic %zd", self.labelNumber]; + NSString *urlString = [NSString stringWithFormat:@"http://lorempixel.com/%zd/%zd/cats/%zd/%@", + (NSInteger)roundl(size.width), + (NSInteger)roundl(size.height), self.catNumber, imageText]; + urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + return [NSURL URLWithString:urlString]; +} + +// titles courtesy of http://www.catipsum.com/ ++ (void)initialize { + titles = @[@"Leave fur on owners clothes intrigued by the shower", + @"Meowwww", + @"Immediately regret falling into bathtub stare out the window", + @"Jump launch to pounce upon little yarn mouse, bare fangs at toy run hide in litter box until treats are fed", + @"Sleep nap", + @"Lick butt", + @"Chase laser lick arm hair present belly, scratch hand when stroked"]; + firstInfos = @[@"Kitty Shop", + @"Cat's r us", + @"Fantastic Felines", + @"The Cat Shop", + @"Cat in a hat", + @"Cat-tastic" + ]; + + badges = @[@"ADORABLE", + @"BOUNCES", + @"HATES CUCUMBERS", + @"SCRATCHY" + ]; +} + + +- (id)randomObjectFromArray:(NSArray *)strings +{ + u_int32_t ipsumCount = (u_int32_t)[strings count]; + u_int32_t location = arc4random_uniform(ipsumCount); + + return strings[location]; +} + +- (uint32_t)randomNumberInRange:(uint32_t)start to:(uint32_t)end { + + return start + arc4random_uniform(end - start); +} + + +@end diff --git a/examples/CatDealsCollectionView/Sample/Launchboard.storyboard b/examples/CatDealsCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/CatDealsCollectionView/Sample/LoadingNode.h b/examples/CatDealsCollectionView/Sample/LoadingNode.h new file mode 100644 index 0000000000..87c182c7a9 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/LoadingNode.h @@ -0,0 +1,15 @@ +// +// LoadingNode.h +// Sample +// +// Created by Samuel Stow on 1/9/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface LoadingNode : ASCellNode + ++ (CGFloat)desiredHeightForWidth:(CGFloat)width; + +@end diff --git a/examples/CatDealsCollectionView/Sample/LoadingNode.m b/examples/CatDealsCollectionView/Sample/LoadingNode.m new file mode 100644 index 0000000000..1bb3977b9a --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/LoadingNode.m @@ -0,0 +1,68 @@ +// +// LoadingNode.m +// Sample +// +// Created by Samuel Stow on 1/9/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "LoadingNode.h" +#import +#import + +#import +#import + +static CGFloat kFixedHeight = 200.0f; + +@interface LoadingNode () +{ + ASDisplayNode *_loadingSpinner; +} + +@end + +@implementation LoadingNode + + +#pragma mark - +#pragma mark ASCellNode. + ++ (CGFloat)desiredHeightForWidth:(CGFloat)width { + return kFixedHeight; +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _loadingSpinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [spinner startAnimating]; + return spinner; + }]; + _loadingSpinner.preferredFrameSize = CGSizeMake(50, 50); + + + // add it as a subnode, and we're done + [self addSubnode:_loadingSpinner]; + + return self; +} + +- (void)layout { + [super layout]; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringXY; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionDefault; + centerSpec.child = _loadingSpinner; + + return centerSpec; +} + +@end \ No newline at end of file diff --git a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h new file mode 100644 index 0000000000..e9018a3784 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h @@ -0,0 +1,15 @@ +// +// PlacholderNetworkImageNode.h +// Sample +// +// Created by Samuel Stow on 1/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface PlaceholderNetworkImageNode : ASNetworkImageNode + +@property (nonatomic, strong) UIImage *placeholderImageOverride; + +@end diff --git a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m new file mode 100644 index 0000000000..c68607f8bb --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m @@ -0,0 +1,18 @@ +// +// PlacholderNetworkImageNode.m +// Sample +// +// Created by Samuel Stow on 1/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "PlaceholderNetworkImageNode.h" + +@implementation PlaceholderNetworkImageNode + +- (UIImage *)placeholderImage { + return self.placeholderImageOverride; +} + + +@end diff --git a/examples/CatDealsCollectionView/Sample/PresentingViewController.h b/examples/CatDealsCollectionView/Sample/PresentingViewController.h new file mode 100644 index 0000000000..bd2308fab3 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/PresentingViewController.h @@ -0,0 +1,13 @@ +// +// PresentingViewController.h +// Sample +// +// Created by Tom King on 12/23/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import + +@interface PresentingViewController : UIViewController + +@end diff --git a/examples/CatDealsCollectionView/Sample/PresentingViewController.m b/examples/CatDealsCollectionView/Sample/PresentingViewController.m new file mode 100644 index 0000000000..49c65e6906 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/PresentingViewController.m @@ -0,0 +1,30 @@ +// +// PresentingViewController.m +// Sample +// +// Created by Tom King on 12/23/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "PresentingViewController.h" +#import "ViewController.h" + +@interface PresentingViewController () + +@end + +@implementation PresentingViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Details" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +} + +- (void)pushNewViewController +{ + ViewController *controller = [[ViewController alloc] init]; + [self.navigationController pushViewController:controller animated:true]; +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/ViewController.h b/examples/CatDealsCollectionView/Sample/ViewController.h new file mode 100644 index 0000000000..d0e9200d88 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ViewController.h @@ -0,0 +1,16 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface ViewController : UIViewController + +@end diff --git a/examples/CatDealsCollectionView/Sample/ViewController.m b/examples/CatDealsCollectionView/Sample/ViewController.m new file mode 100644 index 0000000000..31a1e35435 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/ViewController.m @@ -0,0 +1,244 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "ViewController.h" + +#import +#import "ItemNode.h" +#import "BlurbNode.h" +#import "LoadingNode.h" + +static const NSTimeInterval kWebResponseDelay = 1.0; +static const BOOL kSimulateWebResponse = YES; +static const NSInteger kBatchSize = 20; + +static const CGFloat kHorizontalSectionPadding = 10.0f; +static const CGFloat kVerticalSectionPadding = 20.0f; + +@interface ViewController () +{ + ASCollectionView *_collectionView; + NSMutableArray *_data; +} + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + self = [super init]; + + if (self) { + + self.title = @"Cat Deals"; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + + _collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + _collectionView.asyncDataSource = self; + _collectionView.asyncDelegate = self; + _collectionView.backgroundColor = [UIColor grayColor]; + _collectionView.leadingScreensForBatching = 2; + + ASRangeTuningParameters fetchDataTuning; + fetchDataTuning.leadingBufferScreenfuls = 2; + fetchDataTuning.trailingBufferScreenfuls = 1; + [_collectionView setTuningParameters:fetchDataTuning forRangeType:ASLayoutRangeTypeFetchData]; + + ASRangeTuningParameters preRenderTuning; + preRenderTuning.leadingBufferScreenfuls = 1; + preRenderTuning.trailingBufferScreenfuls = 0.5; + [_collectionView setTuningParameters:preRenderTuning forRangeType:ASLayoutRangeTypeDisplay]; + + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; + + _data = [[NSMutableArray alloc] init]; + + self.navigationItem.leftItemsSupplementBackButton = YES; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadTapped)]; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_collectionView]; + [self fetchMoreCatsWithCompletion:nil]; +} + +- (void)fetchMoreCatsWithCompletion:(void (^)(BOOL))completion { + if (kSimulateWebResponse) { + __weak typeof(self) weakSelf = self; + void(^mockWebService)() = ^{ + NSLog(@"ViewController \"got data from a web service\""); + ViewController *strongSelf = weakSelf; + if (strongSelf != nil) + { + NSLog(@"ViewController is not nil"); + [strongSelf appendMoreItems:kBatchSize completion:completion]; + NSLog(@"ViewController finished updating collectionView"); + } + else { + NSLog(@"ViewController is nil - won't update collectionView"); + } + }; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kWebResponseDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), mockWebService); + } else { + [self appendMoreItems:kBatchSize completion:completion]; + } +} + +- (void)appendMoreItems:(NSInteger)numberOfNewItems completion:(void (^)(BOOL))completion { + NSArray *newData = [self getMoreData:numberOfNewItems]; + dispatch_async(dispatch_get_main_queue(), ^{ + [_collectionView performBatchUpdates:^{ + [_data addObjectsFromArray:newData]; + NSArray *addedIndexPaths = [self indexPathsForObjects:newData]; + [_collectionView insertItemsAtIndexPaths:addedIndexPaths]; + } completion:completion]; + }); +} + +- (NSArray *)getMoreData:(NSInteger)count { + NSMutableArray *data = [NSMutableArray array]; + for (int i = 0; i < count; i++) { + [data addObject:[ItemViewModel randomItem]]; + } + return data; +} + +- (NSArray *)indexPathsForObjects:(NSArray *)data { + NSMutableArray *indexPaths = [NSMutableArray array]; + NSInteger section = 0; + for (ItemViewModel *viewModel in data) { + NSInteger item = [_data indexOfObject:viewModel]; + NSAssert(item < [_data count] && item != NSNotFound, @"Item should be in _data"); + [indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]]; + } + return indexPaths; +} + +- (void)viewWillLayoutSubviews +{ + _collectionView.frame = self.view.bounds; +} + + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { + [_collectionView.collectionViewLayout invalidateLayout]; +} + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +- (void)reloadTapped +{ + [_collectionView reloadData]; +} + +#pragma mark - +#pragma mark ASCollectionView data source. + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ItemViewModel *viewModel = _data[indexPath.item]; + return [[ItemNode alloc] initWithViewModel:viewModel]; +} + +- (ASCellNode *)collectionView:(UICollectionView *)collectionView nodeForSupplementaryElementOfKind:(nonnull NSString *)kind atIndexPath:(nonnull NSIndexPath *)indexPath { + if ([kind isEqualToString:UICollectionElementKindSectionHeader] && indexPath.section == 0) { + return [[BlurbNode alloc] init]; + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter] && indexPath.section == 0) { + return [[LoadingNode alloc] init]; + } + return nil; +} + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { + if (section == 0) { + CGFloat width = CGRectGetWidth(self.view.frame) - 2 * kHorizontalSectionPadding; + return CGSizeMake(width, [BlurbNode desiredHeightForWidth:width]); + } + return CGSizeZero; +} + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { + if (section == 0) { + CGFloat width = CGRectGetWidth(self.view.frame); + return CGSizeMake(width, [LoadingNode desiredHeightForWidth:width]); + } + return CGSizeZero; +} + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { + CGFloat collectionViewWidth = CGRectGetWidth(self.view.frame) - 2 * kHorizontalSectionPadding; + CGFloat oneItemWidth = [ItemNode preferredViewSize].width; + NSInteger numColumns = floor(collectionViewWidth / oneItemWidth); + // Number of columns should be at least 1 + numColumns = MAX(1, numColumns); + + CGFloat totalSpaceBetweenColumns = (numColumns - 1) * kHorizontalSectionPadding; + CGFloat itemWidth = ((collectionViewWidth - totalSpaceBetweenColumns) / numColumns); + CGSize itemSize = [ItemNode sizeForWidth:itemWidth]; + return ASSizeRangeMake(itemSize, itemSize); +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [_data count]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 1; +} + +- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView +{ + // lock the data source + // The data source should not be change until it is unlocked. +} + +- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView +{ + // unlock the data source to enable data source updating. +} + +- (void)collectionView:(UICollectionView *)collectionView willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + NSLog(@"fetch additional content"); + [self fetchMoreCatsWithCompletion:^(BOOL finished){ + [context completeBatchFetching:YES]; + }]; +} + +- (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + return UIEdgeInsetsMake(kVerticalSectionPadding, kHorizontalSectionPadding, kVerticalSectionPadding, kHorizontalSectionPadding); +} + +-(void)dealloc +{ + NSLog(@"ViewController is deallocing"); +} + +@end diff --git a/examples/CatDealsCollectionView/Sample/main.m b/examples/CatDealsCollectionView/Sample/main.m new file mode 100644 index 0000000000..592423d8f6 --- /dev/null +++ b/examples/CatDealsCollectionView/Sample/main.m @@ -0,0 +1,19 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata b/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + From 28618f723822dfb1c47665deca360817ffe2d60d Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 23 Jan 2016 16:52:39 -0800 Subject: [PATCH 12/18] Re-add .region property for API compatibility and convenience, using options object internally. --- AsyncDisplayKit/ASMapNode.h | 6 +++++ AsyncDisplayKit/ASMapNode.mm | 47 ++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index ccf55584fb..301859d9bb 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -19,6 +19,12 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, strong) MKMapSnapshotOptions *options; +/** The region is simply the sub-field on the options object. If the objects object is reset, + this will in effect be overwritten and become the value of the .region property on that object. + Defaults to MKCoordinateRegionForMapRect(MKMapRectWorld). + */ +@property (nonatomic, assign) MKCoordinateRegion region; + /** 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. */ diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index c641a17af0..714df66d57 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -114,6 +114,11 @@ - (MKMapSnapshotOptions *)options { ASDN::MutexLocker l(_propertyLock); + if (!_options) { + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + _options.size = self.calculatedSize; + } return _options; } @@ -129,6 +134,16 @@ } } +- (MKCoordinateRegion)region +{ + return self.options.region; +} + +- (void)setRegion:(MKCoordinateRegion)region +{ + self.options.region = region; +} + #pragma mark - Snapshotter - (void)takeSnapshot @@ -174,33 +189,25 @@ - (void)setUpSnapshotter { 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."); - if (!_options) { - [self createInitialOptions]; - } - _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } - (void)resetSnapshotter { + // FIXME: The semantics of this method / name would suggest that we cancel + destroy the snapshotter, + // but not that we create a new one. We should probably only create the new one in -takeSnapshot or something. [_snapshotter cancel]; - _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:_options]; -} - -- (void)createInitialOptions -{ - _options = [[MKMapSnapshotOptions alloc] init]; - //Default world-scale view - _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); - _options.size = self.calculatedSize; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } - (void)applySnapshotOptions { - [_mapView setCamera:_options.camera animated:YES]; - [_mapView setRegion:_options.region animated:YES]; - [_mapView setMapType:_options.mapType]; - _mapView.showsBuildings = _options.showsBuildings; - _mapView.showsPointsOfInterest = _options.showsPointsOfInterest; + MKMapSnapshotOptions *options = self.options; + [_mapView setCamera:options.camera animated:YES]; + [_mapView setRegion:options.region animated:YES]; + [_mapView setMapType:options.mapType]; + _mapView.showsBuildings = options.showsBuildings; + _mapView.showsPointsOfInterest = options.showsPointsOfInterest; } #pragma mark - Actions @@ -211,9 +218,6 @@ __weak ASMapNode *weakSelf = self; _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; _mapView.delegate = weakSelf.mapDelegate; - if (!_options) { - [weakSelf createInitialOptions]; - } [weakSelf applySnapshotOptions]; [_mapView addAnnotations:_annotations]; [weakSelf setNeedsLayout]; @@ -227,6 +231,7 @@ - (void)removeLiveMap { + // FIXME: With MKCoordinateRegion, isn't the center coordinate fully specified? Do we need this? _centerCoordinateOfMap = _mapView.centerCoordinate; [_mapView removeFromSuperview]; _mapView = nil; From 82f7956bf91656f530d8e1568dfbdb41e02e280a Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 23 Jan 2016 20:12:45 -0800 Subject: [PATCH 13/18] [ASMapNode] Some improvements to layout logic and snapshot triggering. --- AsyncDisplayKit/ASMapNode.h | 3 ++- AsyncDisplayKit/ASMapNode.mm | 30 ++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index 301859d9bb..8703ac2c1a 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -36,7 +36,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, getter=isLiveMap) BOOL liveMap; /** - @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. Defaults to YES. + @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. + @default Default value is 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 needsMapReloadOnBoundsChange; diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index 714df66d57..fcfd821487 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -117,7 +117,10 @@ if (!_options) { _options = [[MKMapSnapshotOptions alloc] init]; _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); - _options.size = self.calculatedSize; + CGSize calculatedSize = self.calculatedSize; + if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { + _options.size = calculatedSize; + } } return _options; } @@ -250,6 +253,24 @@ } #pragma mark - Layout +- (void)setSnapshotSizeIfNeeded:(CGSize)snapshotSize +{ + if (!CGSizeEqualToSize(self.options.size, snapshotSize)) { + _options.size = snapshotSize; + [self resetSnapshotter]; + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize size = self.preferredFrameSize; + if (CGSizeEqualToSize(size, CGSizeZero)) { + size = constrainedSize; + } + [self setSnapshotSizeIfNeeded:size]; + return constrainedSize; +} + // Layout isn't usually needed in the box model, but since we are making use of MKMapView this is preferred. - (void)layout { @@ -258,9 +279,10 @@ _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); } else { // 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) && _needsMapReloadOnBoundsChange) { - _options.size = self.bounds.size; - [self resetSnapshotter]; + if (_needsMapReloadOnBoundsChange) { + [self setSnapshotSizeIfNeeded:self.bounds.size]; + // FIXME: Adding a check for FetchData here seems to cause intermittent map load failures, but shouldn't. + // if (ASInterfaceStateIncludesFetchData(self.interfaceState)) { [self takeSnapshot]; } } From 9ddf68fa96586b771fc7558205ce782cb2f146dd Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 24 Jan 2016 00:50:43 -0800 Subject: [PATCH 14/18] [ASTextNode] Optimize handling of constrained size to almost never recreate NSLayoutManager This also fixes two fairly subtle but serious bugs, #1076 and #1046. --- AsyncDisplayKit/ASCollectionNode+Beta.h | 1 + AsyncDisplayKit/ASTextNode.mm | 50 ++++++++++--------- AsyncDisplayKit/TextKit/ASTextKitContext.h | 2 + AsyncDisplayKit/TextKit/ASTextKitContext.mm | 10 ++++ AsyncDisplayKit/TextKit/ASTextKitRenderer.h | 3 +- AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 49 ++++++++++++------ .../TextKit/ASTextKitTailTruncater.mm | 3 -- AsyncDisplayKit/TextKit/ASTextKitTruncating.h | 3 +- .../ASTextKitTruncationTests.mm | 15 ++---- 9 files changed, 81 insertions(+), 55 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionNode+Beta.h b/AsyncDisplayKit/ASCollectionNode+Beta.h index eeac22b3d1..11ea3ac2fd 100644 --- a/AsyncDisplayKit/ASCollectionNode+Beta.h +++ b/AsyncDisplayKit/ASCollectionNode+Beta.h @@ -6,6 +6,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import "ASCollectionNode.h" @protocol ASCollectionViewLayoutFacilitatorProtocol; NS_ASSUME_NONNULL_BEGIN diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 7eac0593dc..f1fc6f7eda 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -35,13 +35,13 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation - (instancetype)initWithRenderer:(ASTextKitRenderer *)renderer textOrigin:(CGPoint)textOrigin - backgroundColor:(CGColorRef)backgroundColor; + backgroundColor:(UIColor *)backgroundColor; @property (nonatomic, strong, readonly) ASTextKitRenderer *renderer; @property (nonatomic, assign, readonly) CGPoint textOrigin; -@property (nonatomic, assign, readonly) CGColorRef backgroundColor; +@property (nonatomic, strong, readonly) UIColor *backgroundColor; @end @@ -49,20 +49,18 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation - (instancetype)initWithRenderer:(ASTextKitRenderer *)renderer textOrigin:(CGPoint)textOrigin - backgroundColor:(CGColorRef)backgroundColor + backgroundColor:(UIColor *)backgroundColor { if (self = [super init]) { _renderer = renderer; _textOrigin = textOrigin; - _backgroundColor = CGColorRetain(backgroundColor); + _backgroundColor = backgroundColor; } return self; } - (void)dealloc { - CGColorRelease(_backgroundColor); - // Destruction of the layout managers/containers/text storage is quite // expensive, and can take some time, so we dispatch onto a bg queue to // actually dealloc. @@ -182,7 +180,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; NSString *truncationString = [_composedTruncationString string]; if (plainString.length > 50) plainString = [[plainString substringToIndex:50] stringByAppendingString:@"\u2026"]; - return [NSString stringWithFormat:@"<%@: %p; text = \"%@\"; truncation string = \"%@\"; frame = %@>", self.class, self, plainString, truncationString, self.nodeLoaded ? NSStringFromCGRect(self.layer.frame) : nil]; + return [NSString stringWithFormat:@"<%@: %p; text = \"%@\"; truncation string = \"%@\"; frame = %@; renderer = %p>", self.class, self, plainString, truncationString, self.nodeLoaded ? NSStringFromCGRect(self.layer.frame) : nil, _renderer]; } #pragma mark - ASDisplayNode @@ -240,13 +238,13 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setFrame:(CGRect)frame { [super setFrame:frame]; - [self _invalidateRendererIfNeeded:frame.size]; + [self _invalidateRendererIfNeededForBoundsSize:frame.size]; } - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; - [self _invalidateRendererIfNeeded:bounds.size]; + [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; } #pragma mark - Renderer Management @@ -291,12 +289,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)_invalidateRendererIfNeeded { - [self _invalidateRendererIfNeeded:self.bounds.size]; + [self _invalidateRendererIfNeededForBoundsSize:self.bounds.size]; } -- (void)_invalidateRendererIfNeeded:(CGSize)newSize +- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize { - if ([self _needInvalidateRenderer:newSize]) { + if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { // Our bounds of frame have changed to a size that is not identical to our constraining size, // so our previous layout information is invalid, and TextKit may draw at the // incorrect origin. @@ -305,7 +303,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (BOOL)_needInvalidateRenderer:(CGSize)newSize +- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { if (!_renderer) { return YES; @@ -313,9 +311,9 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // If the size is not the same as the constraint we provided to the renderer, start out assuming we need // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. - CGSize oldSize = _renderer.constrainedSize; + CGSize rendererConstrainedSize = _renderer.constrainedSize; - if (CGSizeEqualToSize(newSize, oldSize)) { + if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { return NO; } else { // It is very common to have a constrainedSize with a concrete, specific width but +Inf height. @@ -324,7 +322,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // experience truncation and don't need to recreate the renderer with the size it already calculated, // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). ASLayout *layout = self.calculatedLayout; - if (layout != nil && CGSizeEqualToSize(newSize, layout.size)) { + if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { + if (!CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { + // Don't bother changing _constrainedSize, as ASDisplayNode's -measure: method would have a cache miss + // and ask us to recalculate layout if it were called with the same calculatedSize that got us to this point! + _renderer.constrainedSize = boundsSize; + } return NO; } else { return YES; @@ -409,12 +412,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Fill background if (!isRasterizing) { - CGColorRef backgroundColor = parameters.backgroundColor; + UIColor *backgroundColor = parameters.backgroundColor; if (backgroundColor) { - CGContextSetFillColorWithColor(context, backgroundColor); - CGContextSetBlendMode(context, kCGBlendModeCopy); - CGContextFillRect(context, CGContextGetClipBoundingBox(context)); - CGContextSetBlendMode(context, kCGBlendModeNormal); + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); } } @@ -430,14 +431,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - [self _invalidateRendererIfNeeded]; + CGRect bounds = self.bounds; + [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; // Offset the text origin by any shadow padding UIEdgeInsets shadowPadding = [self shadowPadding]; - CGPoint textOrigin = CGPointMake(self.bounds.origin.x - shadowPadding.left, self.bounds.origin.y - shadowPadding.top); + CGPoint textOrigin = CGPointMake(bounds.origin.x - shadowPadding.left, bounds.origin.y - shadowPadding.top); return [[ASTextNodeDrawParameters alloc] initWithRenderer:[self _renderer] textOrigin:textOrigin - backgroundColor:self.backgroundColor.CGColor]; + backgroundColor:self.backgroundColor]; } #pragma mark - Attributes diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 994082a2cf..d9e6642f61 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.h +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.h @@ -30,6 +30,8 @@ constrainedSize:(CGSize)constrainedSize layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory; +@property (nonatomic, assign, readwrite) CGSize constrainedSize; + /** All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to TextKit components may cause crashes. diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index 5982006008..2b682f9f26 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -49,6 +49,16 @@ return self; } +- (CGSize)constrainedSize +{ + return _textContainer.size; +} + +- (void)setConstrainedSize:(CGSize)constrainedSize +{ + _textContainer.size = constrainedSize; +} + - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, NSTextStorage *, NSTextContainer *))block diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h index 969fd9494a..62d9388a17 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h @@ -37,7 +37,6 @@ /** Designated Initializer -dvlkferufedgjnhjjfhldjedlunvtdtv @discussion Sizing will occur as a result of initialization, so be careful when/where you use this. */ - (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)textComponentAttributes @@ -51,7 +50,7 @@ dvlkferufedgjnhjjfhldjedlunvtdtv @property (nonatomic, assign, readonly) ASTextKitAttributes attributes; -@property (nonatomic, assign, readonly) CGSize constrainedSize; +@property (nonatomic, assign, readwrite) CGSize constrainedSize; #pragma mark - Drawing /* diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index a978b0fcf1..d7922e19b5 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -17,6 +17,9 @@ #import "ASTextKitTailTruncater.h" #import "ASTextKitTruncating.h" +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { static NSCharacterSet *truncationCharacterSet; @@ -65,12 +68,10 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { if (!_truncater) { ASTextKitAttributes attributes = _attributes; - // We must inset the constrained size by the size of the shadower. - CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; + NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet(); _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] truncationAttributedString:attributes.truncationAttributedString - avoidTailTruncationSet:attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet() - constrainedSize:shadowConstrainedSize]; + avoidTailTruncationSet:avoidTailTruncationSet]; } return _truncater; } @@ -79,6 +80,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { if (!_context) { ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; _context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString lineBreakMode:attributes.lineBreakMode @@ -92,6 +94,30 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() #pragma mark - Sizing +- (CGSize)size +{ + if (!_sizeIsCalculated) { + [self _calculateSize]; + _sizeIsCalculated = YES; + } + return _calculatedSize; +} + +- (void)setConstrainedSize:(CGSize)constrainedSize +{ + if (!CGSizeEqualToSize(constrainedSize, _constrainedSize)) { + _sizeIsCalculated = NO; + _constrainedSize = constrainedSize; + // If the context isn't created yet, it will be initialized with the appropriate size when next accessed. + if (_context) { + // If we're updating an existing context, make sure to use the same inset logic used during initialization. + // This codepath allows us to reuse the + CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:constrainedSize]; + _context.constrainedSize = shadowConstrainedSize; + } + } +} + - (void)_calculateSize { // Force glyph generation and layout, which may not have happened yet (and isn't triggered by @@ -111,16 +137,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() // to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect. boundingRect = CGRectIntersection(boundingRect, {.size = constrainedRect.size}); - _calculatedSize = [_shadower outsetSizeWithInsetSize:CGSizeMake(boundingRect.size.width + boundingRect.origin.x, boundingRect.size.height + boundingRect.origin.y)]; -} - -- (CGSize)size -{ - if (!_sizeIsCalculated) { - [self _calculateSize]; - _sizeIsCalculated = YES; - } - return _calculatedSize; + _calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; } #pragma mark - Drawing @@ -136,8 +153,12 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() [[self shadower] setShadowInContext:context]; UIGraphicsPushContext(context); + LOG(@"%@, shadowInsetBounds = %@",self, NSStringFromCGRect(shadowInsetBounds)); + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer])); NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + LOG(@"boundingRect: %@", NSStringFromCGRect([layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer])); [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; }]; diff --git a/AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm b/AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm index 4988d286d6..7617bfe82e 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm @@ -18,7 +18,6 @@ __weak ASTextKitContext *_context; NSAttributedString *_truncationAttributedString; NSCharacterSet *_avoidTailTruncationSet; - CGSize _constrainedSize; } @synthesize visibleRanges = _visibleRanges; @synthesize truncationStringRect = _truncationStringRect; @@ -26,13 +25,11 @@ - (instancetype)initWithContext:(ASTextKitContext *)context truncationAttributedString:(NSAttributedString *)truncationAttributedString avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet - constrainedSize:(CGSize)constrainedSize { if (self = [super init]) { _context = context; _truncationAttributedString = truncationAttributedString; _avoidTailTruncationSet = avoidTailTruncationSet; - _constrainedSize = constrainedSize; [self _truncate]; } diff --git a/AsyncDisplayKit/TextKit/ASTextKitTruncating.h b/AsyncDisplayKit/TextKit/ASTextKitTruncating.h index f3f276ba00..946c378f36 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitTruncating.h +++ b/AsyncDisplayKit/TextKit/ASTextKitTruncating.h @@ -31,7 +31,6 @@ */ - (instancetype)initWithContext:(ASTextKitContext *)context truncationAttributedString:(NSAttributedString *)truncationAttributedString - avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet - constrainedSize:(CGSize)constrainedSize; + avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet; @end diff --git a/AsyncDisplayKitTests/ASTextKitTruncationTests.mm b/AsyncDisplayKitTests/ASTextKitTruncationTests.mm index 123ef417d3..fc7e47f31f 100644 --- a/AsyncDisplayKitTests/ASTextKitTruncationTests.mm +++ b/AsyncDisplayKitTests/ASTextKitTruncationTests.mm @@ -50,8 +50,7 @@ }]; ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context truncationAttributedString:nil - avoidTailTruncationSet:nil - constrainedSize:constrainedSize]; + avoidTailTruncationSet:nil]; XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.visibleRanges[0])); } @@ -67,8 +66,7 @@ layoutManagerFactory:nil]; ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context truncationAttributedString:[self _simpleTruncationAttributedString] - avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@""] - constrainedSize:constrainedSize]; + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@""]]; __block NSString *drawnString; [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { drawnString = textStorage.string; @@ -90,8 +88,7 @@ layoutManagerFactory:nil]; ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context truncationAttributedString:[self _simpleTruncationAttributedString] - avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."] - constrainedSize:constrainedSize]; + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; (void)tailTruncater; __block NSString *drawnString; [context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { @@ -114,8 +111,7 @@ layoutManagerFactory:nil]; ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context truncationAttributedString:[self _simpleTruncationAttributedString] - avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."] - constrainedSize:constrainedSize]; + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]; // So Xcode doesn't yell at me for an unused var... (void)tailTruncater; __block NSString *drawnString; @@ -139,8 +135,7 @@ layoutManagerFactory:nil]; XCTAssertNoThrow([[ASTextKitTailTruncater alloc] initWithContext:context truncationAttributedString:[self _simpleTruncationAttributedString] - avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."] - constrainedSize:constrainedSize]); + avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]); } @end From 351f4a9afc013abe50550f1205daedf0628f4cc9 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 24 Jan 2016 13:11:15 -0800 Subject: [PATCH 15/18] Utilize NSMutableOrderedSet in ASRangeControllerBeta to ensure visible items are prioritized. This also adopts Objective-C generics for the various collections in this class. --- .../Details/ASRangeControllerBeta.mm | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm index e53000d9d2..9ed2beba94 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm +++ b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm @@ -22,7 +22,7 @@ BOOL _rangeIsValid; BOOL _queuedRangeUpdate; ASScrollDirection _scrollDirection; - NSSet *_allPreviousIndexPaths; + NSSet *_allPreviousIndexPaths; } @end @@ -65,7 +65,7 @@ } // FIXME: Consider if we need to check this separately from the range calculation below. - NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; + NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; if (visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... _queuedRangeUpdate = NO; @@ -80,19 +80,22 @@ [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; } - NSArray *allNodes = [_dataSource completedNodes]; - NSArray *currentSectionNodes = nil; + NSArray *allNodes = [_dataSource completedNodes]; // 2D array: section arrays, each containing nodes. + NSArray *currentSectionNodes = nil; NSInteger currentSectionIndex = -1; // Will be unequal to any indexPath.section, so we set currentSectionNodes. NSUInteger numberOfSections = [allNodes count]; NSUInteger numberOfNodesInSection = 0; - NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; + NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; // = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; - NSSet *displayIndexPaths = nil; - NSSet *fetchDataIndexPaths = nil; - NSMutableSet *allIndexPaths = nil; - NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil); + NSSet *displayIndexPaths = nil; + NSSet *fetchDataIndexPaths = nil; + NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil); + + // Prioritize the order in which we visit each. Visible nodes should be updated first so they are enqueued on + // the network or display queues before offscreen, preloading nodes are. + NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; @@ -102,20 +105,22 @@ displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay]; // Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. - allIndexPaths = [fetchDataIndexPaths mutableCopy]; + // Because allIndexPaths is an NSMutableOrderedSet, this adds the non-duplicate items /after/ the existing items. + // This means that during iteration, we will first visit visible, then display, then fetch data nodes. + // Nodes within the visible range may be getting their first display and fetch data call too, so enqueue them first. [allIndexPaths unionSet:displayIndexPaths]; - [allIndexPaths unionSet:visibleIndexPaths]; - } else { - allIndexPaths = [visibleIndexPaths mutableCopy]; + [allIndexPaths unionSet:fetchDataIndexPaths]; } // Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any // range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic // scroll or major main thread stall could cause entirely disjoint sets, but we must visit all. - NSSet *allCurrentIndexPaths = [allIndexPaths copy]; + + // Calling set on NSMutableOrderedSet just references the underlying data store, so we must copy it. + NSSet *allCurrentIndexPaths = [[allIndexPaths set] copy]; [allIndexPaths unionSet:_allPreviousIndexPaths]; _allPreviousIndexPaths = allCurrentIndexPaths; - + for (NSIndexPath *indexPath in allIndexPaths) { // Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it. // For consistency, make sure each node knows that it should measure itself if something changes. From 92ce6ce4d349ec6418e22668f923fbab1c800649 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 24 Jan 2016 14:07:39 -0800 Subject: [PATCH 16/18] [ASRangeControllerBeta] Improve comments, code clarity, cache respondsToSelector: --- .../Details/ASRangeControllerBeta.mm | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm index 9ed2beba94..99c8323675 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm +++ b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm @@ -21,6 +21,7 @@ { BOOL _rangeIsValid; BOOL _queuedRangeUpdate; + BOOL _layoutControllerImplementsSetVisibleIndexPaths; ASScrollDirection _scrollDirection; NSSet *_allPreviousIndexPaths; } @@ -58,13 +59,21 @@ }); } +- (void)setLayoutController:(id)layoutController +{ + _layoutController = layoutController; + _layoutControllerImplementsSetVisibleIndexPaths = [_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)]; +} + - (void)_updateVisibleNodeIndexPaths { - if (!_queuedRangeUpdate) { + ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); + if (!_queuedRangeUpdate || !_layoutController) { return; } - // FIXME: Consider if we need to check this separately from the range calculation below. + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges + // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; if (visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... @@ -72,29 +81,27 @@ return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later } - CGSize viewportSize = [_dataSource viewportSizeForRangeController:self]; - [_layoutController setViewportSize:viewportSize]; + [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]]; // the layout controller needs to know what the current visible indices are to calculate range offsets - if ([_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)]) { + if (_layoutControllerImplementsSetVisibleIndexPaths) { [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; } - NSArray *allNodes = [_dataSource completedNodes]; // 2D array: section arrays, each containing nodes. - NSArray *currentSectionNodes = nil; - NSInteger currentSectionIndex = -1; // Will be unequal to any indexPath.section, so we set currentSectionNodes. - + // allNodes is a 2D array: it contains arrays for each section, each containing nodes. + NSArray *allNodes = [_dataSource completedNodes]; NSUInteger numberOfSections = [allNodes count]; + + NSArray *currentSectionNodes = nil; + NSInteger currentSectionIndex = -1; // Set to -1 so we don't match any indexPath.section on the first iteration. NSUInteger numberOfNodesInSection = 0; - NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; - // = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; + NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; NSSet *displayIndexPaths = nil; NSSet *fetchDataIndexPaths = nil; - NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil); // Prioritize the order in which we visit each. Visible nodes should be updated first so they are enqueued on - // the network or display queues before offscreen, preloading nodes are. + // the network or display queues before preloading (offscreen) nodes are enqueued. NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; @@ -107,20 +114,21 @@ // Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. // Because allIndexPaths is an NSMutableOrderedSet, this adds the non-duplicate items /after/ the existing items. // This means that during iteration, we will first visit visible, then display, then fetch data nodes. - // Nodes within the visible range may be getting their first display and fetch data call too, so enqueue them first. [allIndexPaths unionSet:displayIndexPaths]; [allIndexPaths unionSet:fetchDataIndexPaths]; } - // Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any + // Add anything we had applied interfaceState to in the last update, but is no longer in range, so we can clear any // range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic - // scroll or major main thread stall could cause entirely disjoint sets, but we must visit all. - - // Calling set on NSMutableOrderedSet just references the underlying data store, so we must copy it. + // scroll or major main thread stall could cause entirely disjoint sets. In either case we must visit all. + // Calling "-set" on NSMutableOrderedSet just references the underlying mutable data store, so we must copy it. NSSet *allCurrentIndexPaths = [[allIndexPaths set] copy]; [allIndexPaths unionSet:_allPreviousIndexPaths]; _allPreviousIndexPaths = allCurrentIndexPaths; - + + // This array is only used if logging is enabled. + NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil); + for (NSIndexPath *indexPath in allIndexPaths) { // Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it. // For consistency, make sure each node knows that it should measure itself if something changes. From 51e44760689d8e3bbb4c1077d6b8a229c438582d Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 24 Jan 2016 23:41:46 +0100 Subject: [PATCH 17/18] [ASRangeController] don't get index paths twice if tuning parameters are same Signed-off-by: Matej Knopp --- AsyncDisplayKit/Details/ASRangeControllerBeta.mm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm index 99c8323675..dd755d5ca2 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm +++ b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm @@ -109,7 +109,15 @@ if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { // If we are already visible, get busy! Better get started on preloading before the user scrolls more... fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData]; - displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay]; + + ASRangeTuningParameters parametersFetchData = [_layoutController tuningParametersForRangeType:ASLayoutRangeTypeFetchData]; + ASRangeTuningParameters parametersDisplay = [_layoutController tuningParametersForRangeType:ASLayoutRangeTypeDisplay]; + if (parametersDisplay.leadingBufferScreenfuls == parametersFetchData.leadingBufferScreenfuls && + parametersDisplay.trailingBufferScreenfuls == parametersFetchData.trailingBufferScreenfuls) { + displayIndexPaths = fetchDataIndexPaths; + } else { + displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay]; + } // Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. // Because allIndexPaths is an NSMutableOrderedSet, this adds the non-duplicate items /after/ the existing items. From 2713bdd72e2749917719500f188c46ca56d76252 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 24 Jan 2016 17:14:14 -0800 Subject: [PATCH 18/18] [ASTextNode, ASDisplayNode] Create -calculatedLayoutDidChange and use it in text node. This allows the change in size for the NSTextContainer to occur off the main thread, whenever that size change is necessary. Then the text relayout can occur earlier, during the process of computing ASLayoutSpecs. --- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 7 ++++ AsyncDisplayKit/ASDisplayNode.mm | 5 +++ AsyncDisplayKit/ASTextNode.mm | 43 +++++++++++++++------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 7d002934b5..699b6213ed 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -91,6 +91,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)layoutDidFinish; +/** + * @abstract Called on a background thread if !isNodeLoaded - called on the main thread if isNodeLoaded. + * + * @discussion When the .calculatedLayout property is set to a new ASLayout (directly from -calculateLayoutThatFits: or + * calculated via use of -layoutSpecThatFits:), subclasses may inspect it here. + */ +- (void)calculatedLayoutDidChange; /** @name Layout calculation */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index e6e435ff5e..7c88376cf8 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -594,6 +594,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _layout = [self calculateLayoutThatFits:constrainedSize]; _constrainedSize = constrainedSize; _flags.isMeasured = YES; + [self calculatedLayoutDidChange]; } ASDisplayNodeAssertTrue(_layout.layoutableObject == self); @@ -615,6 +616,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return _layout; } +- (void)calculatedLayoutDidChange +{ +} + - (BOOL)displaysAsynchronously { ASDN::MutexLocker l(_propertyLock); diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index f1fc6f7eda..f4dbea3800 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -185,19 +185,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode -- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize -{ - ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); - ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); - - _constrainedSize = constrainedSize; - [self _invalidateRenderer]; - ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ - [self setNeedsDisplay]; - }); - return [[self _renderer] size]; -} - // FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage. // This code was written before TextKit and when 512MB devices were still the overwhelming majority. - (void)displayDidFinish @@ -303,6 +290,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } +#pragma mark - Layout and Sizing + - (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { if (!_renderer) { @@ -323,7 +312,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). ASLayout *layout = self.calculatedLayout; if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { - if (!CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { + if (boundsSize.width != rendererConstrainedSize.width) { // Don't bother changing _constrainedSize, as ASDisplayNode's -measure: method would have a cache miss // and ask us to recalculate layout if it were called with the same calculatedSize that got us to this point! _renderer.constrainedSize = boundsSize; @@ -335,6 +324,32 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } +- (void)calculatedLayoutDidChange +{ + ASLayout *layout = self.calculatedLayout; + if (layout != nil) { + _renderer.constrainedSize = layout.size; + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); + ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); + + _constrainedSize = constrainedSize; + + // Instead of invalidating the renderer, in case this is a new call with a different constrained size, + // just update the size of the NSTextContainer that is owned by the renderer's internal context object. + [self _renderer].constrainedSize = _constrainedSize; + + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); + + return [[self _renderer] size]; +} + #pragma mark - Modifying User Text - (void)setAttributedString:(NSAttributedString *)attributedString