-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add ASViewController #660
Add ASViewController #660
Changes from all commits
07c0d78
72d108c
fcd76db
8ebc3bd
1ef1b64
a864341
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // | ||
| // ASViewController.h | ||
| // AsyncDisplayKit | ||
| // | ||
| // Created by Huy Nguyen on 16/09/15. | ||
| // Copyright (c) 2015 Facebook. All rights reserved. | ||
| // | ||
|
|
||
| #import <UIKit/UIKit.h> | ||
| #import <AsyncDisplayKit/ASDisplayNode.h> | ||
|
|
||
| @interface ASViewController : UIViewController | ||
|
|
||
| @property (nonatomic, strong, readonly) ASDisplayNode *node; | ||
|
|
||
| //TODO Use nonnull annotation late on. Travis doesn't recognize it (yet). | ||
| - (instancetype)initWithNode:(ASDisplayNode *)node; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // | ||
| // ASViewController.m | ||
| // AsyncDisplayKit | ||
| // | ||
| // Created by Huy Nguyen on 16/09/15. | ||
| // Copyright (c) 2015 Facebook. All rights reserved. | ||
| // | ||
|
|
||
| #import "ASViewController.h" | ||
| #import "ASAssert.h" | ||
| #import "ASDimension.h" | ||
|
|
||
| @implementation ASViewController | ||
|
|
||
| - (instancetype)initWithNode:(ASDisplayNode *)node | ||
| { | ||
| if (!(self = [super init])) { | ||
| return nil; | ||
| } | ||
|
|
||
| ASDisplayNodeAssertNotNil(node, @"Node must not be nil"); | ||
| ASDisplayNodeAssertTrue(!node.layerBacked); | ||
| _node = node; | ||
|
|
||
| return self; | ||
| } | ||
|
|
||
| - (void)loadView | ||
| { | ||
| ASDisplayNodeAssertTrue(!_node.layerBacked); | ||
| self.view = _node.view; | ||
| } | ||
|
|
||
| - (void)viewWillLayoutSubviews | ||
| { | ||
| CGSize viewSize = self.view.bounds.size; | ||
| ASSizeRange constrainedSize = ASSizeRangeMake(viewSize, viewSize); | ||
| [_node measureWithSizeRange:constrainedSize]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, I think this effectively does synchronous measurement here and means that just barely in time for -layout, there will be cached sizes. What would happen if measureWithSizeRange were called on a background thread (on the node) and were still running at this point? Interestingly there haven't been other cases in usage (either framework or app) where I have needed to consider that. Lastly, small nit, but always create a local variable for a structure that needs to be accessed twice. CGSize boundsSize = self.view.bounds.size;
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is synchronous measurement. If it is instead asynchronous and still running at this point, the (backing) view will have the correct size (set previously) but look empty, because the node doesn't layout its subnodes. Once layout is done, we can call setNeedsLayout and subviews will appear correctly.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right — it's totally fine for this to be synchronous. Later, we can add the API to allow a user to trigger preparation of the view in advance, which would include layout. My question is - what if a user dispatch_async'd a call to measure:, and then pushed the view controller? The answer may be that we get bad behavior. That's also not a big deal right now, as it would be unusual to manually call measure: in that way and then push the view — but it will be a key part of the final version of 2.0 to support kicking off asynchronous layout, and then having a call that is able to block on the remainder of it (e.g. "-ensureMeasurementComplete" or something — that is probably not a good name). It would return immediately if done, otherwise wait on the background operation.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for misunderstood your question. That's indeed a tricky question. Yeah I think the behaviour would be undefined, at least for the current implementation. My main reason is that the async layout pass can finished right before or after any of these events: VC is pushed, loadView, viewWillLayoutSubviews, etc. If we block on the remainder in, say, viewDidAppear, we may be able to do final set up there in case the background operation missed these events. (There is a high chance that I'm talking non-sense here, please take it with a grain of salt haha) |
||
| [super viewWillLayoutSubviews]; | ||
| } | ||
|
|
||
| - (void)viewWillAppear:(BOOL)animated | ||
| { | ||
| [super viewWillAppear:animated]; | ||
| [_node recursivelyFetchData]; | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // | ||
| // ScreenNode.h | ||
| // Sample | ||
| // | ||
| // Created by Huy Nguyen on 16/09/15. | ||
| // Copyright (c) 2015 Facebook. All rights reserved. | ||
| // | ||
|
|
||
| #import <AsyncDisplayKit/AsyncDisplayKit.h> | ||
|
|
||
| @interface ScreenNode : ASDisplayNode | ||
|
|
||
| @property (nonatomic, strong) ASMultiplexImageNode *imageNode; | ||
| @property (nonatomic, strong) ASTextNode *textNode; | ||
|
|
||
| - (void)start; | ||
| - (void)reload; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| // | ||
| // ScreenNode.m | ||
| // Sample | ||
| // | ||
| // Created by Huy Nguyen on 16/09/15. | ||
| // Copyright (c) 2015 Facebook. All rights reserved. | ||
| // | ||
|
|
||
| #import "ScreenNode.h" | ||
|
|
||
| @interface ScreenNode() <ASMultiplexImageNodeDataSource, ASMultiplexImageNodeDelegate, ASImageDownloaderProtocol> | ||
| @end | ||
|
|
||
| @implementation ScreenNode | ||
|
|
||
| - (instancetype)init | ||
| { | ||
| if (!(self = [super init])) { | ||
| return nil; | ||
| } | ||
|
|
||
| // multiplex image node! | ||
| // NB: we're using a custom downloader with an artificial delay for this demo, but ASBasicImageDownloader works too! | ||
| _imageNode = [[ASMultiplexImageNode alloc] initWithCache:nil downloader:self]; | ||
| _imageNode.dataSource = self; | ||
| _imageNode.delegate = self; | ||
|
|
||
| // placeholder colour | ||
| _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); | ||
|
|
||
| // load low-quality images before high-quality images | ||
| _imageNode.downloadsIntermediateImages = YES; | ||
|
|
||
| // simple status label | ||
| _textNode = [[ASTextNode alloc] init]; | ||
|
|
||
| [self addSubnode:_imageNode]; | ||
| [self addSubnode:_textNode]; | ||
|
|
||
| return self; | ||
| } | ||
|
|
||
| - (void)start | ||
| { | ||
| [self setText:@"loading…"]; | ||
| _textNode.userInteractionEnabled = NO; | ||
| _imageNode.imageIdentifiers = @[ @"best", @"medium", @"worst" ]; // go! | ||
| } | ||
|
|
||
| - (void)reload { | ||
| [self start]; | ||
| [_imageNode reloadImageIdentifierSources]; | ||
| } | ||
|
|
||
| - (void)setText:(NSString *)text | ||
| { | ||
| NSDictionary *attributes = @{NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-Light" size:22.0f]}; | ||
| NSAttributedString *string = [[NSAttributedString alloc] initWithString:text | ||
| attributes:attributes]; | ||
| _textNode.attributedString = string; | ||
| [self setNeedsLayout]; | ||
| } | ||
|
|
||
| - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize | ||
| { | ||
| ASRatioLayoutSpec *imagePlaceholder = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1 child:_imageNode]; | ||
| ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical | ||
| spacing:10 | ||
| justifyContent:ASStackLayoutJustifyContentCenter | ||
| alignItems:ASStackLayoutAlignItemsCenter | ||
| children:@[imagePlaceholder, _textNode]]; | ||
| return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) child:verticalStack]; | ||
| } | ||
|
|
||
| #pragma mark - | ||
| #pragma mark ASMultiplexImageNode data source & delegate. | ||
|
|
||
| - (NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(id)imageIdentifier | ||
| { | ||
| if ([imageIdentifier isEqualToString:@"worst"]) { | ||
| return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples/Multiplex/worst.png"]; | ||
| } | ||
|
|
||
| if ([imageIdentifier isEqualToString:@"medium"]) { | ||
| return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples/Multiplex/medium.png"]; | ||
| } | ||
|
|
||
| if ([imageIdentifier isEqualToString:@"best"]) { | ||
| return [NSURL URLWithString:@"https://raw.githubusercontent.com/facebook/AsyncDisplayKit/master/examples/Multiplex/best.png"]; | ||
| } | ||
|
|
||
| // unexpected identifier | ||
| return nil; | ||
| } | ||
|
|
||
| - (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didFinishDownloadingImageWithIdentifier:(id)imageIdentifier error:(NSError *)error | ||
| { | ||
| [self setText:[NSString stringWithFormat:@"loaded '%@'", imageIdentifier]]; | ||
|
|
||
| if ([imageIdentifier isEqualToString:@"best"]) { | ||
| [self setText:[_textNode.attributedString.string stringByAppendingString:@". tap to reload"]]; | ||
| _textNode.userInteractionEnabled = YES; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| #pragma mark - | ||
| #pragma mark ASImageDownloaderProtocol. | ||
|
|
||
| - (id)downloadImageWithURL:(NSURL *)URL | ||
| callbackQueue:(dispatch_queue_t)callbackQueue | ||
| downloadProgressBlock:(void (^)(CGFloat progress))downloadProgressBlock | ||
| completion:(void (^)(CGImageRef image, NSError *error))completion | ||
| { | ||
| // if no callback queue is supplied, run on the main thread | ||
| if (callbackQueue == nil) { | ||
| callbackQueue = dispatch_get_main_queue(); | ||
| } | ||
|
|
||
| // call completion blocks | ||
| void (^handler)(NSURLResponse *, NSData *, NSError *) = ^(NSURLResponse *response, NSData *data, NSError *connectionError) { | ||
| // add an artificial delay | ||
| usleep(1.0 * USEC_PER_SEC); | ||
|
|
||
| // ASMultiplexImageNode callbacks | ||
| dispatch_async(callbackQueue, ^{ | ||
| if (downloadProgressBlock) { | ||
| downloadProgressBlock(1.0f); | ||
| } | ||
|
|
||
| if (completion) { | ||
| completion([[UIImage imageWithData:data] CGImage], connectionError); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| // let NSURLConnection do the heavy lifting | ||
| NSURLRequest *request = [NSURLRequest requestWithURL:URL]; | ||
| [NSURLConnection sendAsynchronousRequest:request | ||
| queue:[[NSOperationQueue alloc] init] | ||
| completionHandler:handler]; | ||
|
|
||
| // return nil, don't support cancellation | ||
| return nil; | ||
| } | ||
|
|
||
| - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier | ||
| { | ||
| // no-op, don't support cancellation | ||
| } | ||
|
|
||
| @end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can now modify your method signature + parameters to indicate that
nodemust not be nil by doing the following:- (instancetype)initWithNode:(ASDisplayNode *_Nonnull)node( XCode 7+)- (instancetype)initWithNode:(ASDisplayNode * nonnull)node(Xcode 6.4)https://developer.apple.com/swift/blog/?id=25
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's great. Thanks!
P/S: actually
nonnullmust be right after the open parenthesis.And I'm wondering why it is nonnull instead of nonnil?