diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 5755b452..dfc8aad0 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,4 +47,6 @@ Pod::Spec.new do |spec| } spec.ios.deployment_target = '7.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 861b362e..04868896 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.mm.un~ b/AsyncDisplayKit/.ASVideoNode.mm.un~ new file mode 100644 index 00000000..c3a40805 Binary files /dev/null and b/AsyncDisplayKit/.ASVideoNode.mm.un~ differ diff --git a/AsyncDisplayKit/ASButtonNode.h b/AsyncDisplayKit/ASButtonNode.h index 588319b4..305121b3 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. 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. + */ +- (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. 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. + * @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 ae101c97..7ed676e7 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,33 +47,50 @@ _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; + [self addSubnode:_backgroundImageNode]; [self addSubnode:_titleNode]; [self addSubnode:_imageNode]; } return self; } +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!layerBacked, @"ASButtonNode must not be layer backed!"); + [super setLayerBacked:layerBacked]; +} + - (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]; } @@ -120,6 +143,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); @@ -152,6 +196,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); @@ -246,6 +302,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]; @@ -265,12 +369,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; } diff --git a/AsyncDisplayKit/ASCollectionNode+Beta.h b/AsyncDisplayKit/ASCollectionNode+Beta.h index eeac22b3..11ea3ac2 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/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 7d002934..699b6213 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.h b/AsyncDisplayKit/ASDisplayNode.h index a38f093c..685ed93a 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) @@ -658,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 1346f7e9..7c88376c 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); @@ -2312,6 +2317,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/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index 06accae2..c098e872 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -163,7 +163,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/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 3191b53a..c9024c75 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(); diff --git a/AsyncDisplayKit/ASMapNode.h b/AsyncDisplayKit/ASMapNode.h index 0a8b0ebb..8703ac2c 100644 --- a/AsyncDisplayKit/ASMapNode.h +++ b/AsyncDisplayKit/ASMapNode.h @@ -7,6 +7,7 @@ */ #import +#if TARGET_OS_IOS #import NS_ASSUME_NONNULL_BEGIN @@ -14,7 +15,13 @@ NS_ASSUME_NONNULL_BEGIN @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).

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, 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; @@ -29,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; @@ -40,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN @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; @@ -48,3 +56,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 ce4f3fc8..fcfd8214 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 @@ -17,7 +18,6 @@ { ASDN::RecursiveMutex _propertyLock; MKMapSnapshotter *_snapshotter; - MKMapSnapshotOptions *_options; NSArray *_annotations; CLLocationCoordinate2D _centerCoordinateOfMap; } @@ -27,7 +27,7 @@ @synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; @synthesize mapDelegate = _mapDelegate; -@synthesize region = _region; +@synthesize options = _options; @synthesize liveMap = _liveMap; #pragma mark - Lifecycle @@ -42,13 +42,6 @@ _needsMapReloadOnBoundsChange = YES; _liveMap = NO; _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid; - - //Default world-scale view - _region = MKCoordinateRegionForMapRect(MKMapRectWorld); - - _options = [[MKMapSnapshotOptions alloc] init]; - _options.region = _region; - return self; } @@ -118,23 +111,40 @@ _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } -- (MKCoordinateRegion)region +- (MKMapSnapshotOptions *)options { ASDN::MutexLocker l(_propertyLock); - return _region; + if (!_options) { + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + CGSize calculatedSize = self.calculatedSize; + if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { + _options.size = calculatedSize; + } + } + return _options; +} + +- (void)setOptions:(MKMapSnapshotOptions *)options +{ + ASDN::MutexLocker l(_propertyLock); + _options = options; + if (self.isLiveMap) { + [self applySnapshotOptions]; + } else { + [self resetSnapshotter]; + [self takeSnapshot]; + } +} + +- (MKCoordinateRegion)region +{ + return self.options.region; } - (void)setRegion:(MKCoordinateRegion)region { - ASDN::MutexLocker l(_propertyLock); - _region = region; - if (self.isLiveMap) { - [_mapView setRegion:_region animated:YES]; - } else { - _options.region = _region; - [self resetSnapshotter]; - [self takeSnapshot]; - } + self.options.region = region; } #pragma mark - Snapshotter @@ -182,14 +192,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."); - _options.size = self.calculatedSize; - _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]; + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; +} + +- (void)applySnapshotOptions +{ + 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 @@ -200,7 +221,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]; @@ -213,6 +234,7 @@ - (void)removeLiveMap { + // FIXME: With MKCoordinateRegion, isn't the center coordinate fully specified? Do we need this? _centerCoordinateOfMap = _mapView.centerCoordinate; [_mapView removeFromSuperview]; _mapView = nil; @@ -231,7 +253,25 @@ } #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. +- (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 { [super layout]; @@ -239,11 +279,13 @@ _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]; } } } -@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 25e463b2..5c02ed60 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -6,9 +6,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#if TARGET_OS_IOS + #import #import - #import NS_ASSUME_NONNULL_BEGIN @@ -116,13 +117,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 +231,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 +243,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 +264,8 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier options:(PHImageRequestOptions *)options; @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 c51e2bb2..ff35ed3a 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 - #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,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return request.url; } -@end \ No newline at end of file +@end +#endif + +#endif diff --git a/AsyncDisplayKit/ASPagerNode.m b/AsyncDisplayKit/ASPagerNode.m index 075d0f07..c7db4850 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 df8bb811..ff2c4413 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/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 7eac0593..f4dbea38 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,24 +180,11 @@ 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 -- (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 @@ -240,13 +225,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 +276,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 +290,9 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (BOOL)_needInvalidateRenderer:(CGSize)newSize +#pragma mark - Layout and Sizing + +- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { if (!_renderer) { return YES; @@ -313,9 +300,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 +311,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 (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; + } return NO; } else { return YES; @@ -332,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 @@ -409,12 +427,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 +446,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/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index a01624f7..31b63545 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 af612f02..cd5e8fbd 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 () { @@ -34,11 +39,17 @@ - (instancetype)init { - if (!(self = [super init])) { return nil; } + if (!(self = [super init])) { + return nil; + } _previewQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); [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/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h index 7630fe2e..96c2f4b2 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 d46b3791..1245e325 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/Details/ASRangeControllerBeta.mm b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm index e53000d9..dd755d5c 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm +++ b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm @@ -21,8 +21,9 @@ { BOOL _rangeIsValid; BOOL _queuedRangeUpdate; + BOOL _layoutControllerImplementsSetVisibleIndexPaths; ASScrollDirection _scrollDirection; - NSSet *_allPreviousIndexPaths; + NSSet *_allPreviousIndexPaths; } @end @@ -58,64 +59,84 @@ }); } +- (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. - NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; + // 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)... _queuedRangeUpdate = NO; 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]; - 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 *displayIndexPaths = nil; - NSSet *fetchDataIndexPaths = nil; - NSMutableSet *allIndexPaths = nil; - NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil); + NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; + NSSet *displayIndexPaths = nil; + NSSet *fetchDataIndexPaths = 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 preloading (offscreen) nodes are enqueued. + NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; 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. - 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. [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 + // 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. - NSSet *allCurrentIndexPaths = [allIndexPaths copy]; + // 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. diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index f542b29d..25f92dc3 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 7dd72750..29e9b64a 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(); @@ -298,7 +338,7 @@ _bridge_prologue; _setToViewOnly(userInteractionEnabled, enabled); } - +#if TARGET_OS_IOS - (BOOL)isExclusiveTouch { _bridge_prologue; @@ -310,7 +350,7 @@ _bridge_prologue; _setToViewOnly(exclusiveTouch, exclusiveTouch); } - +#endif - (BOOL)clipsToBounds { _bridge_prologue; diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m index 37918e19..b1ab0c5d 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; diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 994082a2..d9e6642f 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 59820060..2b682f9f 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 969fd949..62d9388a 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 a978b0fc..d7922e19 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 4988d286..7617bfe8 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 f3f276ba..946c378f 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 123ef417..fc7e47f3 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 diff --git a/examples/CatDealsCollectionView/Podfile b/examples/CatDealsCollectionView/Podfile new file mode 100644 index 00000000..6c012e3c --- /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 00000000..1810a8eb --- /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 00000000..a80c0382 --- /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 00000000..f49edc75 --- /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 00000000..7b5a2f30 --- /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 00000000..80b7fb3d --- /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 00000000..adab692b --- /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 00000000..efe58505 --- /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 00000000..68dcf866 --- /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 00000000..f0fce547 --- /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 00000000..1547a984 Binary files /dev/null and b/examples/CatDealsCollectionView/Sample/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ 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 00000000..a9e3a5b1 --- /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 00000000..ee440721 Binary files /dev/null and b/examples/CatDealsCollectionView/Sample/Images.xcassets/cat_face.imageset/cat_face.png differ diff --git a/examples/CatDealsCollectionView/Sample/Info.plist b/examples/CatDealsCollectionView/Sample/Info.plist new file mode 100644 index 00000000..8c57e7a8 --- /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 00000000..eee5dc5d --- /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 00000000..15608a76 --- /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 00000000..67879f39 --- /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 00000000..3b390684 --- /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 00000000..1d040333 --- /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 00000000..6ad4c1d1 --- /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 00000000..673e0f7e --- /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 00000000..87c182c7 --- /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 00000000..1bb3977b --- /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 00000000..e9018a37 --- /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 00000000..c68607f8 --- /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 00000000..bd2308fa --- /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 00000000..49c65e69 --- /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 00000000..d0e9200d --- /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 00000000..31a1e354 --- /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 00000000..592423d8 --- /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 00000000..7b5a2f30 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/VideoTableView/Sample.xcodeproj/project.pbxproj b/examples/VideoTableView/Sample.xcodeproj/project.pbxproj index c198b60a..f2ba462a 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;