diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index cf4fc08d4d..f551639afe 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -185,6 +185,24 @@ */ - (void)displayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; +/** + * @abstract Indicates that the receiver is about to display its subnodes. This method is not called if there are no + * subnodes present. + * + * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is + * about to begin. + */ +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Indicates that the receiver is finished displaying its subnodes. This method is not called if there are + * no subnodes present. + * + * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is + * about to begin. + */ +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER; + /** * @abstract Marks the receiver's bounds as needing to be redrawn, with a scale value. @@ -303,6 +321,26 @@ - (void)reclaimMemory ASDISPLAYNODE_REQUIRES_SUPER; +/** @name Placeholders */ + +/** + * @abstract Optionally provide an image to serve as the placeholder for the backing store while the contents are being + * displayed. + * + * @discussion + * Subclasses may override this method and return an image to use as the placeholder. Take caution as there may be a + * time and place where this method is called on a background thread. Note that -[UIImage imageNamed:] is not thread + * safe when using image assets. + * + * To retrieve the CGSize to do any image drawing, use the node's calculatedSize property. + * + * Defaults to nil. + * + * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) + */ +- (UIImage *)placeholderImage; + + /** @name Description */ diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index bf72daf2be..997ae4ff63 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -330,6 +330,14 @@ - (void)recursivelyReclaimMemory; +/** + * @abstract Toggle displaying a placeholder over the node that covers content until the node and all subnodes are + * displayed. + * + * @discussion Defaults to NO. + */ +@property (nonatomic, assign) BOOL placeholderEnabled; + /** @name Hit Testing */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 3880474ad5..690f6a73f9 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -184,6 +184,8 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) _replaceAsyncSentinel = nil; _displaySentinel = nil; + + _pendingDisplayNodes = nil; } #pragma mark - UIResponder overrides @@ -270,6 +272,10 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) TIME_SCOPED(_debugTimeForDidLoad); [self didLoad]; } + + if (self.placeholderEnabled) { + [self _setupPlaceholderLayer]; + } } - (UIView *)view @@ -349,6 +355,7 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) - (CGSize)measure:(CGSize)constrainedSize { ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); if (![self __shouldSize]) return CGSizeZero; @@ -365,6 +372,18 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) ASDisplayNodeAssertTrue(_size.width >= 0.0); ASDisplayNodeAssertTrue(_size.height >= 0.0); + + // we generate placeholders at measure: time so that a node is guaranteed to have a placeholder ready to go + if (self.placeholderEnabled && [self displaysAsynchronously]) { + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + + if (_placeholderLayer) { + _placeholderLayer.contents = (id)_placeholderImage.CGImage; + } + } + return _size; } @@ -448,8 +467,10 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) { ASDisplayNodeAssertMainThread(); ASDN::MutexLocker l(_propertyLock); - if (CGRectEqualToRect(_layer.bounds, CGRectZero)) + if (CGRectEqualToRect(_layer.bounds, CGRectZero)) { return; // Performing layout on a zero-bounds view often results in frame calculations with negative sizes after applying margins, which will cause measure: on subnodes to assert. + } + _placeholderLayer.frame = _layer.bounds; [self layout]; [self layoutDidFinish]; } @@ -1091,6 +1112,60 @@ static NSInteger incrementIfFound(NSInteger i) { _supernode = supernode; } +// Track that a node will be displayed as part of the current node hierarchy. +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeWillDisplay:(ASDisplayNode *)node +{ + ASDN::MutexLocker l(_propertyLock); + + [_pendingDisplayNodes addObject:node]; +} + +// Notify that a node that was pending display finished +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeDidDisplay:(ASDisplayNode *)node +{ + ASDN::MutexLocker l(_propertyLock); + + [_pendingDisplayNodes removeObject:node]; + + // only trampoline if there is a placeholder and nodes are done displaying + if ([self _pendingDisplayNodesHaveFinished] && _placeholderLayer.superlayer) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self _tearDownPlaceholderLayer]; + }); + } +} + +// Helper method to check that all nodes that the current node is waiting to display are finished +// Use this method to check to remove any placeholder layers +- (BOOL)_pendingDisplayNodesHaveFinished +{ + return _pendingDisplayNodes.count == 0; +} + +// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay +{ + return _flags.implementsDrawRect == YES || _flags.implementsImageDisplay == YES; +} + +- (void)_setupPlaceholderLayer +{ + ASDisplayNodeAssertMainThread(); + + _placeholderLayer = [CALayer layer]; + // do not set to CGFLOAT_MAX in the case that something needs to be overtop the placeholder + _placeholderLayer.zPosition = 9999.0; +} + +- (void)_tearDownPlaceholderLayer +{ + ASDisplayNodeAssertMainThread(); + + [_placeholderLayer removeFromSuperlayer]; +} + #pragma mark - For Subclasses - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize @@ -1111,6 +1186,11 @@ static NSInteger incrementIfFound(NSInteger i) { return _constrainedSize; } +- (UIImage *)placeholderImage +{ + return nil; +} + - (void)invalidateCalculatedSize { ASDisplayNodeAssertThreadAffinity(self); @@ -1142,6 +1222,7 @@ static NSInteger incrementIfFound(NSInteger i) { - (void)reclaimMemory { self.layer.contents = nil; + _placeholderLayer.contents = nil; } - (void)recursivelyReclaimMemory @@ -1159,10 +1240,36 @@ static NSInteger incrementIfFound(NSInteger i) { - (void)displayWillStart { + // in case current node takes longer to display than it's subnodes, treat it as a dependent node + [self _pendingNodeWillDisplay:self]; + + [_supernode subnodeDisplayWillStart:self]; + + if (_placeholderImage && _placeholderLayer) { + _placeholderLayer.contents = (id)_placeholderImage.CGImage; + [self.layer addSublayer:_placeholderLayer]; + } } - (void)displayDidFinish { + [self _pendingNodeDidDisplay:self]; + + [_supernode subnodeDisplayDidFinish:self]; + + if (_placeholderLayer && [self _pendingDisplayNodesHaveFinished]) { + [self _tearDownPlaceholderLayer]; + } +} + +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode +{ + [self _pendingNodeWillDisplay:subnode]; +} + +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode +{ + [self _pendingNodeDidDisplay:subnode]; } - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale @@ -1394,6 +1501,14 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, _flags.displaySuspended = flag; self.asyncLayer.displaySuspended = flag; + + if ([self _implementsDisplay]) { + if (flag) { + [_supernode subnodeDisplayDidFinish:self]; + } else { + [_supernode subnodeDisplayWillStart:self]; + } + } } - (BOOL)isInHierarchy diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 8adaf3ca63..bb042be975 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -59,6 +59,8 @@ extern id ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDi */ extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c); +extern UIColor *ASDisplayNodeDefaultPlaceholderColor(); + /** Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. */ diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 4849de7414..b8a4c1a4eb 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -122,6 +122,15 @@ extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) }); } +#pragma mark - Placeholders + +UIColor *ASDisplayNodeDefaultPlaceholderColor() +{ + return [UIColor colorWithWhite:0.95 alpha:1.0]; +} + +#pragma mark - Hierarchy Notifications + void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node) { [node __incrementVisibilityNotificationsDisabled]; diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h index d4c0ae102c..c4b6686933 100644 --- a/AsyncDisplayKit/ASImageNode.h +++ b/AsyncDisplayKit/ASImageNode.h @@ -53,6 +53,11 @@ typedef UIImage *(^asimagenode_modification_block_t)(UIImage *image); */ @property (nonatomic, assign) ASImageNodeTint tint; +/** + @abstract The placeholder color. + */ +@property (nonatomic, strong) UIColor *placeholderColor; + /** * @abstract Indicates whether efficient cropping of the receiver is enabled. * diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 7f19f17567..d8a7be58b4 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -13,6 +13,7 @@ #import #import #import +#import #import "ASImageNode+CGExtras.h" @@ -91,6 +92,7 @@ _cropEnabled = YES; _cropRect = CGRectMake(0.5, 0.5, 0, 0); _cropDisplayBounds = CGRectNull; + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); return self; } @@ -139,6 +141,14 @@ return _tint; } +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + _placeholderColor = placeholderColor; + + // prevent placeholders if we don't have a color + self.placeholderEnabled = placeholderColor != nil; +} + - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; { BOOL hasValidCropBounds = _cropEnabled && !CGRectIsNull(_cropDisplayBounds) && !CGRectIsEmpty(_cropDisplayBounds); diff --git a/AsyncDisplayKit/ASTextNode.h b/AsyncDisplayKit/ASTextNode.h index 371cc33fca..a3c2b7554a 100644 --- a/AsyncDisplayKit/ASTextNode.h +++ b/AsyncDisplayKit/ASTextNode.h @@ -69,6 +69,18 @@ typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { */ @property (nonatomic, readonly, assign) NSUInteger lineCount; +#pragma mark - Placeholders + +/** + @abstract The placeholder color. + */ +@property (nonatomic, strong) UIColor *placeholderColor; + +/** + @abstract Inset each line of the placeholder. + */ +@property (nonatomic, assign) UIEdgeInsets placeholderInsets; + #pragma mark - Shadow /** diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index c017eda023..c8170154d0 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -14,6 +14,7 @@ #import #import #import +#import #import "ASTextNodeRenderer.h" #import "ASTextNodeShadower.h" @@ -135,6 +136,11 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) self.accessibilityTraits = UIAccessibilityTraitStaticText; _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); + + // Placeholders + self.placeholderEnabled = YES; + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); + _placeholderInsets = UIEdgeInsetsMake(1.0, 0.0, 1.0, 0.0); } return self; @@ -666,6 +672,43 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) return [self.class _adjustRendererRect:frame forShadowPadding:self.shadowPadding]; } +#pragma mark - Placeholders + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + _placeholderColor = placeholderColor; + + // prevent placeholders if we don't have a color + self.placeholderEnabled = placeholderColor != nil; +} + +- (UIImage *)placeholderImage +{ + CGSize size = self.calculatedSize; + UIGraphicsBeginImageContext(size); + [self.placeholderColor setFill]; + + ASTextNodeRenderer *renderer = [self _renderer]; + NSRange textRange = [renderer visibleRange]; + + // cap height is both faster and creates less subpixel blending + NSArray *lineRects = [self _rectsForTextRange:textRange measureOption:ASTextNodeRendererMeasureOptionLineHeight]; + + // fill each line with the placeholder color + for (NSValue *rectValue in lineRects) { + CGRect lineRect = [rectValue CGRectValue]; + CGRect fillBounds = UIEdgeInsetsInsetRect(lineRect, self.placeholderInsets); + + if (fillBounds.size.width > 0.0 && fillBounds.size.height > 0.0) { + UIRectFill(fillBounds); + } + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + #pragma mark - Touch Handling - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 7081b0c07d..2995caab2b 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -53,6 +53,12 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()); UIView *_view; CALayer *_layer; + UIImage *_placeholderImage; + CALayer *_placeholderLayer; + + // keeps track of nodes/subnodes that have not finished display, used with placeholders + NSMutableSet *_pendingDisplayNodes; + _ASPendingState *_pendingViewState; struct { diff --git a/examples/Kittens/Sample/KittenNode.m b/examples/Kittens/Sample/KittenNode.m index 34d02c8bf4..27d305b00f 100644 --- a/examples/Kittens/Sample/KittenNode.m +++ b/examples/Kittens/Sample/KittenNode.m @@ -75,7 +75,7 @@ static const CGFloat kInnerPadding = 10.0f; // kitten image, with a purple background colour serving as placeholder _imageNode = [[ASNetworkImageNode alloc] init]; - _imageNode.backgroundColor = [UIColor purpleColor]; + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"http://placekitten.com/%zd/%zd", (NSInteger)roundl(_kittenSize.width), (NSInteger)roundl(_kittenSize.height)]]; diff --git a/examples/Placeholders/Default-568h@2x.png b/examples/Placeholders/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/examples/Placeholders/Default-568h@2x.png differ diff --git a/examples/Placeholders/Default-667h@2x.png b/examples/Placeholders/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/examples/Placeholders/Default-667h@2x.png differ diff --git a/examples/Placeholders/Default-736h@3x.png b/examples/Placeholders/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/examples/Placeholders/Default-736h@3x.png differ diff --git a/examples/Placeholders/Podfile b/examples/Placeholders/Podfile new file mode 100644 index 0000000000..6c012e3c04 --- /dev/null +++ b/examples/Placeholders/Podfile @@ -0,0 +1,3 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +pod 'AsyncDisplayKit', :path => '../..' diff --git a/examples/Placeholders/Sample.xcodeproj/project.pbxproj b/examples/Placeholders/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..0749dfd00c --- /dev/null +++ b/examples/Placeholders/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,372 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 29A7F3A51A3638DE00CF34F2 /* SlowpokeTextNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */; }; + 29E35E9B1A2F8DB0007B4B17 /* SlowpokeImageNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */; }; + 29E35E9E1A2F8DBC007B4B17 /* SlowpokeShareNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */; }; + 29E35EA01A2F9650007B4B17 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 29E35E9F1A2F9650007B4B17 /* logo.png */; }; + 29E35EA31A2FD0E9007B4B17 /* PostNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E35EA21A2FD0E9007B4B17 /* PostNode.m */; }; + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */; }; + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 29A7F3A31A3638DE00CF34F2 /* SlowpokeTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeTextNode.h; sourceTree = ""; }; + 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeTextNode.m; sourceTree = ""; }; + 29E35E991A2F8DB0007B4B17 /* SlowpokeImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeImageNode.h; sourceTree = ""; }; + 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeImageNode.m; sourceTree = ""; }; + 29E35E9C1A2F8DBC007B4B17 /* SlowpokeShareNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SlowpokeShareNode.h; sourceTree = ""; }; + 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SlowpokeShareNode.m; sourceTree = ""; }; + 29E35E9F1A2F9650007B4B17 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; + 29E35EA11A2FD0E9007B4B17 /* PostNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostNode.h; sourceTree = ""; }; + 29E35EA21A2FD0E9007B4B17 /* PostNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostNode.m; sourceTree = ""; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + sourceTree = ""; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 29E35E9F1A2F9650007B4B17 /* logo.png */, + 29E35EA11A2FD0E9007B4B17 /* PostNode.h */, + 29E35EA21A2FD0E9007B4B17 /* PostNode.m */, + 29E35E991A2F8DB0007B4B17 /* SlowpokeImageNode.h */, + 29E35E9A1A2F8DB0007B4B17 /* SlowpokeImageNode.m */, + 29E35E9C1A2F8DBC007B4B17 /* SlowpokeShareNode.h */, + 29E35E9D1A2F8DBC007B4B17 /* SlowpokeShareNode.m */, + 29A7F3A31A3638DE00CF34F2 /* SlowpokeTextNode.h */, + 29A7F3A41A3638DE00CF34F2 /* SlowpokeTextNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */, + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29E35EA01A2F9650007B4B17 /* logo.png in Resources */, + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E080B80F89C34A25B3488E26 /* 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; + }; + F012A6F39E0149F18F564F50 /* 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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29A7F3A51A3638DE00CF34F2 /* SlowpokeTextNode.m in Sources */, + 29E35E9B1A2F8DB0007B4B17 /* SlowpokeImageNode.m in Sources */, + 29E35E9E1A2F8DBC007B4B17 /* SlowpokeShareNode.m in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 29E35EA31A2FD0E9007B4B17 /* PostNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* 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.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* 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.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/examples/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/examples/Placeholders/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/Placeholders/Sample.xcworkspace/contents.xcworkspacedata b/examples/Placeholders/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..d98549fd35 --- /dev/null +++ b/examples/Placeholders/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/Placeholders/Sample/AppDelegate.h b/examples/Placeholders/Sample/AppDelegate.h new file mode 100644 index 0000000000..2aa29369b4 --- /dev/null +++ b/examples/Placeholders/Sample/AppDelegate.h @@ -0,0 +1,18 @@ +/* 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 AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/Placeholders/Sample/AppDelegate.m b/examples/Placeholders/Sample/AppDelegate.m new file mode 100644 index 0000000000..a8e5594780 --- /dev/null +++ b/examples/Placeholders/Sample/AppDelegate.m @@ -0,0 +1,27 @@ +/* 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 "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 = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/examples/Placeholders/Sample/Info.plist b/examples/Placeholders/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/examples/Placeholders/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/Placeholders/Sample/PostNode.h b/examples/Placeholders/Sample/PostNode.h new file mode 100644 index 0000000000..2cfa5a6048 --- /dev/null +++ b/examples/Placeholders/Sample/PostNode.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 PostNode : ASDisplayNode + +@end diff --git a/examples/Placeholders/Sample/PostNode.m b/examples/Placeholders/Sample/PostNode.m new file mode 100644 index 0000000000..b23636b17d --- /dev/null +++ b/examples/Placeholders/Sample/PostNode.m @@ -0,0 +1,88 @@ +/* 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 "PostNode.h" + +#import "SlowpokeShareNode.h" +#import "SlowpokeTextNode.h" +#import + +@interface PostNode () +{ + SlowpokeTextNode *_textNode; + SlowpokeShareNode *_needyChildNode; // this node slows down display +} + +@end + +@implementation PostNode + +// turn on to demo that the parent displays a placeholder even if it takes the longest +//+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +//{ +// usleep( (long)(1.2 * USEC_PER_SEC) ); // artificial delay of 1.2s +// +// // demonstrates that the parent node should also adhere to the placeholder +// [[UIColor colorWithWhite:0.95 alpha:1.0] setFill]; +// UIRectFill(bounds); +//} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _textNode = [[SlowpokeTextNode alloc] init]; + _textNode.placeholderInsets = UIEdgeInsetsMake(3.0, 0.0, 3.0, 0.0); + _textNode.placeholderEnabled = YES; + + NSString *text = @"Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh."; + NSDictionary *attributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:17.0] }; + _textNode.attributedString = [[NSAttributedString alloc] initWithString:text attributes:attributes]; + + _needyChildNode = [[SlowpokeShareNode alloc] init]; + _needyChildNode.opaque = NO; + + [self addSubnode:_textNode]; + [self addSubnode:_needyChildNode]; + + return self; +} + +- (UIImage *)placeholderImage +{ + CGSize size = self.calculatedSize; + UIGraphicsBeginImageContext(size); + [[UIColor colorWithWhite:0.9 alpha:1] setFill]; + UIRectFill((CGRect){CGPointZero, size}); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize textSize = [_textNode measure:constrainedSize]; + CGSize shareSize = [_needyChildNode measure:constrainedSize]; + + return CGSizeMake(constrainedSize.width, textSize.height + 10.0 + shareSize.height); +} + +- (void)layout +{ + CGSize textSize = _textNode.calculatedSize; + CGSize needyChildSize = _needyChildNode.calculatedSize; + + _textNode.frame = (CGRect){CGPointZero, textSize}; + _needyChildNode.frame = (CGRect){0.0, CGRectGetMaxY(_textNode.frame) + 10.0, needyChildSize}; +} + +@end diff --git a/examples/Placeholders/Sample/SlowpokeImageNode.h b/examples/Placeholders/Sample/SlowpokeImageNode.h new file mode 100644 index 0000000000..466590c03a --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeImageNode.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 SlowpokeImageNode : ASImageNode + +@end diff --git a/examples/Placeholders/Sample/SlowpokeImageNode.m b/examples/Placeholders/Sample/SlowpokeImageNode.m new file mode 100644 index 0000000000..07ba36cd46 --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeImageNode.m @@ -0,0 +1,65 @@ +/* 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 "SlowpokeImageNode.h" + +#import + +static CGFloat const kASDKLogoAspectRatio = 2.79; + +@implementation SlowpokeImageNode + ++ (UIImage *)displayWithParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock +{ + usleep( (long)(0.5 * USEC_PER_SEC) ); // artificial delay of 0.5s + + return [super displayWithParameters:parameters isCancelled:isCancelledBlock]; +} + +- (instancetype)init +{ + if (self = [super init]) { + self.placeholderEnabled = YES; + } + return self; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + if (constrainedSize.width > 0.0) { + return CGSizeMake(constrainedSize.width, constrainedSize.width / kASDKLogoAspectRatio); + } else if (constrainedSize.height > 0.0) { + return CGSizeMake(constrainedSize.height * kASDKLogoAspectRatio, constrainedSize.height); + } + return CGSizeZero; +} + +- (UIImage *)placeholderImage +{ + CGSize size = self.calculatedSize; + UIGraphicsBeginImageContext(size); + [[UIColor colorWithWhite:0.9 alpha:1] setStroke]; + + UIBezierPath *path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointZero]; + [path addLineToPoint:(CGPoint){size.width, size.height}]; + [path stroke]; + + [path moveToPoint:(CGPoint){size.width, 0.0}]; + [path addLineToPoint:(CGPoint){0.0, size.height}]; + [path stroke]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +@end diff --git a/examples/Placeholders/Sample/SlowpokeShareNode.h b/examples/Placeholders/Sample/SlowpokeShareNode.h new file mode 100644 index 0000000000..67d3c3b0ef --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeShareNode.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 SlowpokeShareNode : ASControlNode + +@end diff --git a/examples/Placeholders/Sample/SlowpokeShareNode.m b/examples/Placeholders/Sample/SlowpokeShareNode.m new file mode 100644 index 0000000000..e00a739d37 --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeShareNode.m @@ -0,0 +1,43 @@ +/* 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 "SlowpokeShareNode.h" + +#import + +static NSUInteger const kRingCount = 3; +static CGFloat const kRingStrokeWidth = 1.0; +static CGSize const kIconSize = (CGSize){ 60.0, 17.0 }; + +@implementation SlowpokeShareNode + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + usleep( (long)(0.8 * USEC_PER_SEC) ); // artificial delay of 0.8s + + [[UIColor colorWithRed:0.f green:122/255.f blue:1.f alpha:1.f] setStroke]; + + for (NSUInteger i = 0; i < kRingCount; i++) { + CGFloat x = i * kIconSize.width / kRingCount; + CGRect frame = CGRectMake(x, 0.f, kIconSize.height, kIconSize.height); + CGRect strokeFrame = CGRectInset(frame, kRingStrokeWidth, kRingStrokeWidth); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeFrame cornerRadius:kIconSize.height / 2.f]; + [path setLineWidth:kRingStrokeWidth]; + [path stroke]; + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + return kIconSize; +} + +@end diff --git a/examples/Placeholders/Sample/SlowpokeTextNode.h b/examples/Placeholders/Sample/SlowpokeTextNode.h new file mode 100644 index 0000000000..199430330c --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeTextNode.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 SlowpokeTextNode : ASTextNode + +@end diff --git a/examples/Placeholders/Sample/SlowpokeTextNode.m b/examples/Placeholders/Sample/SlowpokeTextNode.m new file mode 100644 index 0000000000..db309741c9 --- /dev/null +++ b/examples/Placeholders/Sample/SlowpokeTextNode.m @@ -0,0 +1,25 @@ +/* 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 "SlowpokeTextNode.h" + +#import + +@implementation SlowpokeTextNode + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + usleep( (long)(1.0 * USEC_PER_SEC) ); // artificial delay of 1.0 + + [super drawRect:bounds withParameters:parameters isCancelled:isCancelledBlock isRasterizing:isRasterizing]; +} + +@end diff --git a/examples/Placeholders/Sample/ViewController.h b/examples/Placeholders/Sample/ViewController.h new file mode 100644 index 0000000000..d0e9200d88 --- /dev/null +++ b/examples/Placeholders/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/Placeholders/Sample/ViewController.m b/examples/Placeholders/Sample/ViewController.m new file mode 100644 index 0000000000..82b140fa46 --- /dev/null +++ b/examples/Placeholders/Sample/ViewController.m @@ -0,0 +1,105 @@ +/* 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 "PostNode.h" +#import "SlowpokeImageNode.h" +#import + +@interface ViewController () +{ + PostNode *_postNode; + SlowpokeImageNode *_imageNode; + UIButton *_displayButton; +} + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _displayButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_displayButton setTitle:@"Display me!" forState:UIControlStateNormal]; + [_displayButton addTarget:self action:@selector(onDisplayButton:) forControlEvents:UIControlEventTouchUpInside]; + + UIColor *tintBlue = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0]; + [_displayButton setTitleColor:tintBlue forState:UIControlStateNormal]; + [_displayButton setTitleColor:[tintBlue colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted]; + _displayButton.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_displayButton]; +} + +- (void)viewWillLayoutSubviews +{ + CGFloat padding = 20.0; + CGRect bounds = self.view.bounds; + CGFloat constrainedWidth = CGRectGetWidth(bounds); + CGSize constrainedSize = CGSizeMake(constrainedWidth - 2 * padding, CGFLOAT_MAX); + + CGSize postSize = [_postNode measure:constrainedSize]; + CGSize imageSize = [_imageNode measure:constrainedSize]; + + _imageNode.frame = (CGRect){padding, padding, imageSize}; + _postNode.frame = (CGRect){padding, CGRectGetMaxY(_imageNode.frame) + 10.0, postSize}; + + CGFloat buttonHeight = 55.0; + _displayButton.frame = (CGRect){0.0, CGRectGetHeight(bounds) - buttonHeight, CGRectGetWidth(bounds), buttonHeight}; +} + +// this method is pretty gross and just for demonstration :] +- (void)createAndDisplayNodes +{ + [_imageNode.view removeFromSuperview]; + [_postNode.view removeFromSuperview]; + + // ASImageNode gets placeholders by default + _imageNode = [[SlowpokeImageNode alloc] init]; + _imageNode.image = [UIImage imageNamed:@"logo"]; + + _postNode = [[PostNode alloc] init]; + + // change to NO to see text placeholders, change to YES to see the parent placeholder + // this placeholder will cover all subnodes while they are displaying, just a like a stage curtain! + _postNode.placeholderEnabled = NO; + + [self.view addSubview:_imageNode.view]; + [self.view addSubview:_postNode.view]; +} + + +#pragma mark - +#pragma mark Actions + +- (void)onDisplayButton:(id)sender +{ + [self createAndDisplayNodes]; +} + +@end diff --git a/examples/Placeholders/Sample/logo.png b/examples/Placeholders/Sample/logo.png new file mode 100755 index 0000000000..dce1ebc950 Binary files /dev/null and b/examples/Placeholders/Sample/logo.png differ diff --git a/examples/Placeholders/Sample/main.m b/examples/Placeholders/Sample/main.m new file mode 100644 index 0000000000..ae9488711c --- /dev/null +++ b/examples/Placeholders/Sample/main.m @@ -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 + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +}