diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 5c12f1ca..cd8d5a07 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -251,9 +251,7 @@ 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */; }; - 68355B301CB5799E001D4E68 /* ASImageNode+AnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 68355B2D1CB5799E001D4E68 /* ASImageNode+AnimatedImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B311CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; - 68355B331CB579AD001D4E68 /* ASImageNode+AnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 68355B2D1CB5799E001D4E68 /* ASImageNode+AnimatedImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; 68355B3A1CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */; }; 68355B3B1CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h in Headers */ = {isa = PBXBuildFile; fileRef = 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -759,7 +757,6 @@ 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASFlowLayoutController.h; sourceTree = ""; }; 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASFlowLayoutController.mm; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; - 68355B2D1CB5799E001D4E68 /* ASImageNode+AnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+AnimatedImage.h"; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPINRemoteImageDownloader.m; sourceTree = ""; }; 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; @@ -1048,7 +1045,6 @@ 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */, 058D09DD195D050800B7D73C /* ASImageNode.h */, 058D09DE195D050800B7D73C /* ASImageNode.mm */, - 68355B2D1CB5799E001D4E68 /* ASImageNode+AnimatedImage.h */, 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */, 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, @@ -1536,7 +1532,6 @@ 257754C31BEE458E00737CA5 /* ASTextNodeTypes.h in Headers */, 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */, 69E1006D1CA89CB600D88C1B /* ASEnvironmentInternal.h in Headers */, - 68355B301CB5799E001D4E68 /* ASImageNode+AnimatedImage.h in Headers */, AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */, CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, ACF6ED2F1B17843500DA7C62 /* ASStackLayoutSpec.h in Headers */, @@ -1649,7 +1644,6 @@ 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */, 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */, 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */, - 68355B331CB579AD001D4E68 /* ASImageNode+AnimatedImage.h in Headers */, DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */, DBDB83951C6E879900D0098C /* ASPagerFlowLayout.h in Headers */, 34EFC7691B701CE100AD841F /* ASLayoutable.h in Headers */, diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 56f7f854..cc9000af 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -83,6 +83,13 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); } +// For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - we have to be sure to set certain properties +// like setFrame: and setBackgroundColor: directly to the UIView and not apply it to the layer only. +BOOL ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(ASDisplayNodeFlags flags) +{ + return flags.synchronous && !flags.layerBacked; +} + _ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) { ASDN::MutexLocker l(node->_propertyLock); @@ -954,8 +961,8 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (self.layerBacked) { [_pendingViewState applyToLayer:self.layer]; } else { - BOOL setFrameDirectly = (_flags.synchronous && !_flags.layerBacked); - [_pendingViewState applyToView:self.view setFrameDirectly:setFrameDirectly]; + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(_flags); + [_pendingViewState applyToView:self.view withSpecialPropertiesHandling:specialPropertiesHandling]; } [_pendingViewState clearChanges]; diff --git a/AsyncDisplayKit/ASImageNode+AnimatedImage.h b/AsyncDisplayKit/ASImageNode+AnimatedImage.h deleted file mode 100644 index ae6ee4e7..00000000 --- a/AsyncDisplayKit/ASImageNode+AnimatedImage.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// ASImageNode+AnimatedImage.h -// AsyncDisplayKit -// -// Created by Garrett Moon on 3/22/16. -// Copyright © 2016 Facebook. All rights reserved. -// - -#import "ASImageNode.h" -#import "ASImageProtocols.h" - -@interface ASImageNode (AnimatedImage) -@property (atomic, assign) BOOL animatedImagePaused; -@property (nullable, atomic, strong) id animatedImage; -@end diff --git a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm index ae296ab6..ffebb963 100644 --- a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm +++ b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm @@ -6,7 +6,7 @@ // Copyright © 2016 Facebook. All rights reserved. // -#import "ASImageNode+AnimatedImage.h" +#import "ASImageNode.h" #import "ASAssert.h" #import "ASImageProtocols.h" @@ -25,9 +25,12 @@ - (void)setAnimatedImage:(id )animatedImage { ASDN::MutexLocker l(_animatedImageLock); - if (!ASObjectIsEqual(_animatedImage, animatedImage)) { - _animatedImage = animatedImage; + if (ASObjectIsEqual(_animatedImage, animatedImage)) { + return; } + + _animatedImage = animatedImage; + if (animatedImage != nil) { __weak ASImageNode *weakSelf = self; if ([animatedImage respondsToSelector:@selector(setCoverImageReadyCallback:)]) { @@ -50,7 +53,7 @@ - (void)setAnimatedImagePaused:(BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImagePausedLock); + ASDN::MutexLocker l(_animatedImageLock); _animatedImagePaused = animatedImagePaused; ASPerformBlockOnMainThread(^{ if (animatedImagePaused) { @@ -63,7 +66,7 @@ - (BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImagePausedLock); + ASDN::MutexLocker l(_animatedImageLock); return _animatedImagePaused; } @@ -84,7 +87,7 @@ - (void)animatedImageFileReady { - dispatch_async(dispatch_get_main_queue(), ^{ + ASPerformBlockOnMainThread(^{ [self startAnimating]; }); } @@ -147,18 +150,6 @@ } } -- (void)__enterHierarchy -{ - [super __enterHierarchy]; - [self startAnimating]; -} - -- (void)__exitHierarchy -{ - [super __exitHierarchy]; - [self stopAnimating]; -} - - (void)displayLinkFired:(CADisplayLink *)displayLink { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h index 8ce01b63..9e4a8024 100644 --- a/AsyncDisplayKit/ASImageNode.h +++ b/AsyncDisplayKit/ASImageNode.h @@ -8,6 +8,8 @@ #import +#import "ASImageProtocols.h" + NS_ASSUME_NONNULL_BEGIN /** @@ -110,6 +112,22 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); */ - (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock; +/** + * @abstract The animated image to playback + * + * @discussion Set this to an object which conforms to ASAnimatedImageProtocol + * to have the ASImageNode playback an animated image. + */ +@property (nullable, atomic, strong) id animatedImage; + +/** + * @abstract Pause the playback of an animated image. + * + * @discussion Set to YES to pause playback of an animated image and NO to resume + * playback. + */ +@property (atomic, assign) BOOL animatedImagePaused; + @end diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 7bb5eaa7..0fddf08a 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -16,7 +16,6 @@ #import #import #import -#import #import #import "ASImageNode+CGExtras.h" diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index 55dbf008..04e268e2 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -221,7 +221,6 @@ - (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."); _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 6a8e37cb..c3b05421 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -15,7 +15,6 @@ #import "ASThread.h" #import "ASInternalHelpers.h" #import "ASImageContainerProtocolCategories.h" -#import "ASImageNode+AnimatedImage.h" #if PIN_REMOTE_IMAGE #import "ASPINRemoteImageDownloader.h" diff --git a/AsyncDisplayKit/ASTableNode.m b/AsyncDisplayKit/ASTableNode.m index 366209bf..22b6c392 100644 --- a/AsyncDisplayKit/ASTableNode.m +++ b/AsyncDisplayKit/ASTableNode.m @@ -20,7 +20,7 @@ @end @interface ASTableNode () -@property (nonatomic) _ASTablePendingState *pendingState; +@property (nonatomic, strong) _ASTablePendingState *pendingState; @end @interface ASTableView () @@ -68,7 +68,7 @@ ASTableView *view = self.view; view.tableNode = self; - + if (_pendingState) { _ASTablePendingState *pendingState = _pendingState; self.pendingState = nil; diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index cb73a9c2..cefc9960 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -11,6 +11,8 @@ @class AVAsset, AVPlayer, AVPlayerItem; @protocol ASVideoNodeDelegate; +NS_ASSUME_NONNULL_BEGIN + // IMPORTANT NOTES: // 1. Applications using ASVideoNode must link AVFoundation! (this provides the AV* classes below) // 2. This is a relatively new component of AsyncDisplayKit. It has many useful features, but @@ -19,34 +21,47 @@ @interface ASVideoNode : ASControlNode -- (instancetype)init; // ASVideoNode is created with a simple alloc/init. - - (void)play; - (void)pause; - (BOOL)isPlaying; -@property (atomic, strong, readwrite) AVAsset *asset; +@property (nullable, atomic, strong, readwrite) AVAsset *asset; -@property (atomic, strong, readonly) AVPlayer *player; -@property (atomic, strong, readonly) AVPlayerItem *currentItem; +@property (nullable, atomic, strong, readonly) AVPlayer *player; +@property (nullable, atomic, strong, readonly) AVPlayerItem *currentItem; -// When autoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState. -// If it leaves the visible interfaceState it will pause but will resume once it has returned +/** + * When shouldAutoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState. + * If it leaves the visible interfaceState it will pause but will resume once it has returned. + */ @property (nonatomic, assign, readwrite) BOOL shouldAutoplay; @property (nonatomic, assign, readwrite) BOOL shouldAutorepeat; @property (nonatomic, assign, readwrite) BOOL muted; +//! Defaults to AVLayerVideoGravityResizeAspect @property (atomic) NSString *gravity; -@property (atomic) ASButtonNode *playButton; -@property (atomic, weak, readwrite) id delegate; +//! Defaults to an ASDefaultPlayButton instance. +@property (nullable, atomic) ASButtonNode *playButton; + +@property (nullable, atomic, weak, readwrite) id delegate; @end @protocol ASVideoNodeDelegate @optional +/** + * @abstract Delegate method invoked when the node's video has played to its end time. + * @param videoNode The video node has played to its end time. + */ - (void)videoPlaybackDidFinish:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked the node is tapped. + * @param videoNode The video node that was tapped. + * @discussion The video's play state is toggled if this method is not implemented. + */ - (void)videoNodeWasTapped:(ASVideoNode *)videoNode; @end +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 4f32cb97..599b07dc 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -9,6 +9,27 @@ #import "ASVideoNode.h" #import "ASDefaultPlayButton.h" +static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) { + return ASObjectIsEqual(asset1, asset2) + || ([asset1 isKindOfClass:[AVURLAsset class]] + && [asset2 isKindOfClass:[AVURLAsset class]] + && ASObjectIsEqual(((AVURLAsset *)asset1).URL, ((AVURLAsset *)asset2).URL)); +} + +static UIViewContentMode ASContentModeFromVideoGravity(NSString *videoGravity) { + if ([videoGravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) { + return UIViewContentModeScaleAspectFill; + } else if ([videoGravity isEqualToString:AVLayerVideoGravityResize]) { + return UIViewContentModeScaleToFill; + } else { + return UIViewContentModeScaleAspectFit; + } +} + +static void *ASVideoNodeContext = &ASVideoNodeContext; +static NSString * const kPlaybackLikelyToKeepUpKey = @"playbackLikelyToKeepUp"; +static NSString * const kStatus = @"status"; + @interface ASVideoNode () { ASDN::RecursiveMutex _videoLock; @@ -64,64 +85,60 @@ - (ASDisplayNode *)constructPlayerNode { - ASDisplayNode * playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ - ASDN::MutexLocker l(_videoLock); - + ASVideoNode * __weak weakSelf = self; + + return [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; - if (!_player) { - [self constructCurrentPlayerItemFromInitData]; - _player = [AVPlayer playerWithPlayerItem:_currentPlayerItem]; - _player.muted = _muted; - } - playerLayer.player = _player; - playerLayer.videoGravity = [self gravity]; + playerLayer.player = weakSelf.player; + playerLayer.videoGravity = weakSelf.gravity; return playerLayer; }]; - - return playerNode; } -- (void)constructCurrentPlayerItemFromInitData +- (AVPlayerItem *)constructPlayerItem { ASDN::MutexLocker l(_videoLock); ASDisplayNodeAssert(_asset, @"ASVideoNode must be initialized with an AVAsset"); - [self removePlayerItemObservers]; + + AVPlayerItem *playerItem = nil; - if (_asset) { - _currentPlayerItem = [[AVPlayerItem alloc] initWithAsset:_asset]; - } - - if (_currentPlayerItem) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:_currentPlayerItem]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:_currentPlayerItem]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemNewErrorLogEntryNotification object:_currentPlayerItem]; + if (_asset != nil) { + if (_asset.tracks.count > 0) { + playerItem = [[AVPlayerItem alloc] initWithAsset:_asset]; + } else if ([_asset isKindOfClass:[AVURLAsset class]]) { + playerItem = [[AVPlayerItem alloc] initWithURL:((AVURLAsset *)_asset).URL]; + } } + + return playerItem; } -- (void)removePlayerItemObservers +- (void)addPlayerItemObservers:(AVPlayerItem *)playerItem { - ASDN::MutexLocker l(_videoLock); - - if (_currentPlayerItem) { - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:nil]; - } + [playerItem addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + [playerItem addObserver:self forKeyPath:kPlaybackLikelyToKeepUpKey options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; } -- (void)didLoad +- (void)removePlayerItemObservers:(AVPlayerItem *)playerItem { - [super didLoad]; - - ASDN::MutexLocker l(_videoLock); - - if (_shouldBePlaying) { - _playerNode = [self constructPlayerNode]; - [self insertSubnode:_playerNode atIndex:0]; - } else if (_asset) { - [self setPlaceholderImagefromAsset:_asset]; + @try { + [playerItem removeObserver:self forKeyPath:kStatus context:ASVideoNodeContext]; + [playerItem removeObserver:self forKeyPath:kPlaybackLikelyToKeepUpKey context:ASVideoNodeContext]; } + @catch (NSException * __unused exception) { + NSLog(@"Unnecessary KVO removal"); + } + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [notificationCenter removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; + [notificationCenter removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; } - (void)layout @@ -144,102 +161,89 @@ _spinner.position = CGPointMake(bounds.size.width/2, bounds.size.height/2); } -- (void)setPlaceholderImagefromAsset:(AVAsset*)asset +- (void)generatePlaceholderImage +{ + ASVideoNode * __weak weakSelf = self; + AVAsset * __weak asset = self.asset; + + [self imageAtTime:kCMTimeZero completionHandler:^(UIImage *image) { + ASPerformBlockOnMainThread(^{ + // Ensure the asset hasn't changed since the image request was made + if (ASAssetIsEqual(weakSelf.asset, asset)) { + [weakSelf setVideoPlaceholderImage:image]; + } + }); + }]; +} + +- (void)imageAtTime:(CMTime)imageTime completionHandler:(void(^)(UIImage *image))completionHandler { ASPerformBlockOnBackgroundThread(^{ ASDN::MutexLocker l(_videoLock); - - AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset]; - imageGenerator.appliesPreferredTrackTransform = YES; - NSArray *times = @[[NSValue valueWithCMTime:CMTimeMake(0, 1)]]; - - [imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) { - - ASDN::MutexLocker l(_videoLock); - - // Unfortunately it's not possible to generate a preview image for an HTTP live stream asset, so we'll give up here - // http://stackoverflow.com/questions/32112205/m3u8-file-avassetimagegenerator-error - if (image && _placeholderImageNode.image == nil) { - UIImage *theImage = [UIImage imageWithCGImage:image]; - - if (!_placeholderImageNode) { - _placeholderImageNode = [[ASImageNode alloc] init]; - _placeholderImageNode.layerBacked = YES; - } - - _placeholderImageNode.image = theImage; - - if ([_gravity isEqualToString:AVLayerVideoGravityResize]) { - _placeholderImageNode.contentMode = UIViewContentModeRedraw; - } - else if ([_gravity isEqualToString:AVLayerVideoGravityResizeAspect]) { - _placeholderImageNode.contentMode = UIViewContentModeScaleAspectFit; - } - else if ([_gravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) { - _placeholderImageNode.contentMode = UIViewContentModeScaleAspectFill; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - ASDN::MutexLocker l(_videoLock); - - [self insertSubnode:_placeholderImageNode atIndex:0]; - [self setNeedsLayout]; - }); - } - }]; + + // Skip the asset image generation if we don't have any tracks available that are capable of supporting it + NSArray* visualAssetArray = [_asset tracksWithMediaCharacteristic:AVMediaCharacteristicVisual]; + if (visualAssetArray.count == 0) { + completionHandler(nil); + return; + } + + AVAssetImageGenerator *previewImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_asset]; + previewImageGenerator.appliesPreferredTrackTransform = YES; + + [previewImageGenerator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:imageTime]] + completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { + if (error != nil && result != AVAssetImageGeneratorCancelled) { + NSLog(@"Asset preview image generation failed with error: %@", error); + } + completionHandler(image ? [UIImage imageWithCGImage:image] : nil); + }]; }); } -- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +- (void)setVideoPlaceholderImage:(UIImage *)image { - [super interfaceStateDidChange:newState fromState:oldState]; - - BOOL nowVisible = ASInterfaceStateIncludesVisible(newState); - BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); - ASDN::MutexLocker l(_videoLock); - - if (!nowVisible) { - if (wasVisible) { - if (_shouldBePlaying) { - [self pause]; - _shouldBePlaying = YES; - } - [(UIActivityIndicatorView *)_spinner.view stopAnimating]; - [_spinner removeFromSupernode]; - } - } else { - if (_shouldBePlaying) { - [self play]; - } + + if (_placeholderImageNode == nil && image != nil) { + _placeholderImageNode = [[ASImageNode alloc] init]; + _placeholderImageNode.layerBacked = YES; + _placeholderImageNode.contentMode = ASContentModeFromVideoGravity(_gravity); } + + _placeholderImageNode.image = image; + + ASPerformBlockOnMainThread(^{ + ASDN::MutexLocker l(_videoLock); + + if (_placeholderImageNode != nil) { + [self insertSubnode:_placeholderImageNode atIndex:0]; + [self setNeedsLayout]; + } + }); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { ASDN::MutexLocker l(_videoLock); - - if (object == _currentPlayerItem && [keyPath isEqualToString:@"status"]) { - if (_currentPlayerItem.status == AVPlayerItemStatusReadyToPlay) { - if ([self.subnodes containsObject:_spinner]) { - [_spinner removeFromSupernode]; - _spinner = nil; - } - + + if (object != _currentPlayerItem) { + return; + } + + if ([keyPath isEqualToString:kStatus]) { + if ([change[NSKeyValueChangeNewKey] integerValue] == AVPlayerItemStatusReadyToPlay) { + [_spinner removeFromSupernode]; + _spinner = nil; + // If we don't yet have a placeholder image update it now that we should have data available for it if (_placeholderImageNode.image == nil) { - if (_currentPlayerItem && - _currentPlayerItem.tracks.count > 0 && - _currentPlayerItem.tracks[0].assetTrack && - _currentPlayerItem.tracks[0].assetTrack.asset) { - _asset = _currentPlayerItem.tracks[0].assetTrack.asset; - [self setPlaceholderImagefromAsset:_asset]; - [self setNeedsLayout]; - } + [self generatePlaceholderImage]; } - - } else if (_currentPlayerItem.status == AVPlayerItemStatusFailed) { - + } + } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUpKey]) { + if (_shouldBePlaying && [change[NSKeyValueChangeNewKey] boolValue] == true && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [self play]; // autoresume after buffer catches up } } } @@ -261,23 +265,20 @@ { [super fetchData]; - @try { - [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; - } - @catch (NSException * __unused exception) { - NSLog(@"unnecessary removal in fetch data"); - } - { ASDN::MutexLocker l(_videoLock); - [self constructCurrentPlayerItemFromInitData]; - [_currentPlayerItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; - - if (_player) { - [_player replaceCurrentItemWithPlayerItem:_currentPlayerItem]; + + AVPlayerItem *playerItem = [self constructPlayerItem]; + self.currentItem = playerItem; + + if (_player != nil) { + [_player replaceCurrentItemWithPlayerItem:playerItem]; } else { - _player = [[AVPlayer alloc] initWithPlayerItem:_currentPlayerItem]; - _player.muted = _muted; + self.player = [AVPlayer playerWithPlayerItem:playerItem]; + } + + if (_placeholderImageNode.image == nil) { + [self generatePlaceholderImage]; } } } @@ -288,8 +289,10 @@ { ASDN::MutexLocker l(_videoLock); - ((AVPlayerLayer *)_playerNode.layer).player = nil; - _player = nil; + + self.player = nil; + self.currentItem = nil; + _placeholderImageNode.image = nil; } } @@ -299,24 +302,13 @@ ASDN::MutexLocker l(_videoLock); - if (_shouldAutoplay && _playerNode.isNodeLoaded) { - [self play]; - } else if (_shouldAutoplay) { - _shouldBePlaying = YES; - } if (isVisible) { - if (_playerNode.isNodeLoaded) { - if (!_player) { - [self constructCurrentPlayerItemFromInitData]; - _player = [AVPlayer playerWithPlayerItem:_currentPlayerItem]; - _player.muted = _muted; - } - ((AVPlayerLayer *)_playerNode.layer).player = _player; - } - - if (_shouldBePlaying) { + if (_shouldBePlaying || _shouldAutoplay) { [self play]; } + } else if (_shouldBePlaying) { + [self pause]; + _shouldBePlaying = YES; } } @@ -326,7 +318,10 @@ - (void)setPlayButton:(ASButtonNode *)playButton { ASDN::MutexLocker l(_videoLock); - + + [_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; + [_playButton removeFromSupernode]; + _playButton = playButton; [self addSubnode:playButton]; @@ -345,18 +340,18 @@ { ASDN::MutexLocker l(_videoLock); - if (ASObjectIsEqual(asset, _asset) - || ([asset isKindOfClass:[AVURLAsset class]] - && [_asset isKindOfClass:[AVURLAsset class]] - && ASObjectIsEqual(((AVURLAsset *)asset).URL, ((AVURLAsset *)_asset).URL))) { - return; - } - + if (ASAssetIsEqual(asset, _asset)) { + return; + } + + [self clearFetchedData]; + _asset = asset; - - // FIXME: Adopt -setNeedsFetchData when it is available - if (self.interfaceState & ASInterfaceStateFetchData) { - [self fetchData]; + + [self setNeedsDataFetch]; + + if (_shouldAutoplay) { + [self play]; } } @@ -378,6 +373,7 @@ if (_playerNode.isNodeLoaded) { ((AVPlayerLayer *)_playerNode.layer).videoGravity = gravity; } + _placeholderImageNode.contentMode = ASContentModeFromVideoGravity(gravity); _gravity = gravity; } @@ -408,20 +404,15 @@ - (void)play { ASDN::MutexLocker l(_videoLock); - - if (!_spinner) { - _spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ - UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init]; - spinnnerView.color = [UIColor whiteColor]; - - return spinnnerView; - }]; + + if (_player == nil) { + [self setNeedsDataFetch]; } - - if (!_playerNode) { + + if (_playerNode == nil) { _playerNode = [self constructPlayerNode]; - - if ([self.subnodes containsObject:_playButton]) { + + if (_playButton.supernode == self) { [self insertSubnode:_playerNode belowSubnode:_playButton]; } else { [self addSubnode:_playerNode]; @@ -434,9 +425,19 @@ [UIView animateWithDuration:0.15 animations:^{ _playButton.alpha = 0.0; }]; - - if (![self ready] && _shouldBePlaying && ASInterfaceStateIncludesVisible(self.interfaceState)) { - [self addSubnode:_spinner]; + + if (![self ready]) { + if (!_spinner) { + _spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init]; + spinnnerView.color = [UIColor whiteColor]; + + return spinnnerView; + }]; + + [self addSubnode:_spinner]; + } + [(UIActivityIndicatorView *)_spinner.view startAnimating]; } } @@ -473,8 +474,8 @@ if ([_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]) { [_delegate videoPlaybackDidFinish:self]; } - [_player seekToTime:CMTimeMakeWithSeconds(0, 1)]; - + [_player seekToTime:kCMTimeZero]; + if (_shouldAutorepeat) { [self play]; } else { @@ -486,21 +487,20 @@ { if ([notification.name isEqualToString:AVPlayerItemFailedToPlayToEndTimeNotification]) { NSLog(@"Failed to play video"); - } - else if ([notification.name isEqualToString:AVPlayerItemNewErrorLogEntryNotification]) { - AVPlayerItem* item = (AVPlayerItem*)notification.object; - AVPlayerItemErrorLogEvent* logEvent = item.errorLog.events.lastObject; + } else if ([notification.name isEqualToString:AVPlayerItemNewErrorLogEntryNotification]) { + AVPlayerItem *item = (AVPlayerItem *)notification.object; + AVPlayerItemErrorLogEvent *logEvent = item.errorLog.events.lastObject; NSLog(@"AVPlayerItem error log entry added for video with error %@ status %@", item.error, (item.status == AVPlayerItemStatusFailed ? @"FAILED" : [NSString stringWithFormat:@"%ld", (long)item.status])); NSLog(@"Item is %@", item); - if (logEvent) + if (logEvent) { NSLog(@"Log code %ld domain %@ comment %@", (long)logEvent.errorStatusCode, logEvent.errorDomain, logEvent.errorComment); + } } } - -#pragma mark - Property Accessors for Tests +#pragma mark - Internal Properties - (ASDisplayNode *)spinner { @@ -508,6 +508,12 @@ return _spinner; } +- (ASImageNode *)placeholderImageNode +{ + ASDN::MutexLocker l(_videoLock); + return _placeholderImageNode; +} + - (AVPlayerItem *)currentItem { ASDN::MutexLocker l(_videoLock); @@ -517,7 +523,12 @@ - (void)setCurrentItem:(AVPlayerItem *)currentItem { ASDN::MutexLocker l(_videoLock); + + [self removePlayerItemObservers:_currentPlayerItem]; + _currentPlayerItem = currentItem; + + [self addPlayerItemObservers:currentItem]; } - (ASDisplayNode *)playerNode @@ -526,24 +537,38 @@ return _playerNode; } +- (void)setPlayerNode:(ASDisplayNode *)playerNode +{ + ASDN::MutexLocker l(_videoLock); + _playerNode = playerNode; +} + +- (void)setPlayer:(AVPlayer *)player +{ + ASDN::MutexLocker l(_videoLock); + _player = player; + player.muted = _muted; + ((AVPlayerLayer *)_playerNode.layer).player = player; +} + - (BOOL)shouldBePlaying { ASDN::MutexLocker l(_videoLock); return _shouldBePlaying; } +- (void)setShouldBePlaying:(BOOL)shouldBePlaying +{ + ASDN::MutexLocker l(_videoLock); + _shouldBePlaying = shouldBePlaying; +} + #pragma mark - Lifecycle - (void)dealloc { - [self removePlayerItemObservers]; - - @try { - [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; - } - @catch (NSException * __unused exception) { - NSLog(@"unnecessary removal in dealloc"); - } + [_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; + [self removePlayerItemObservers:_currentPlayerItem]; } @end diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index ad24302d..4035c604 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -11,7 +11,6 @@ #import #import -#import #import #import #import diff --git a/AsyncDisplayKit/Details/ASDelegateProxy.m b/AsyncDisplayKit/Details/ASDelegateProxy.m index 3034a83d..b4f69775 100644 --- a/AsyncDisplayKit/Details/ASDelegateProxy.m +++ b/AsyncDisplayKit/Details/ASDelegateProxy.m @@ -131,6 +131,31 @@ } } +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + // Check for a compiled definition for the selector + NSMethodSignature *methodSignature = nil; + if ([self interceptsSelector:aSelector]) { + methodSignature = [[_interceptor class] instanceMethodSignatureForSelector:aSelector]; + } else { + methodSignature = [[_target class] instanceMethodSignatureForSelector:aSelector]; + } + + // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature + // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found. + // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method + // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines + // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will + // suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector, + // the dud NSMethodSignature simply gets us around the exception. + return methodSignature ?: [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + // If we are down here this means _interceptor and _target where nil. Just don't do anything to prevent a crash +} + - (BOOL)interceptsSelector:(SEL)selector { ASDisplayNodeAssert(NO, @"This method must be overridden by subclasses."); diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index 3dc5568d..c5079760 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -136,21 +136,62 @@ withDownloadIdentifier:(id)downloadIdentifier; @protocol ASAnimatedImageProtocol +/** + @abstract Should be called when the objects cover image is ready. + @param coverImageReadyCallback a block which receives the cover image. + */ @property (nonatomic, strong, readwrite) void (^coverImageReadyCallback)(UIImage *coverImage); @required +/** + @abstract Return the objects's cover image. + */ @property (nonatomic, readonly) UIImage *coverImage; +/** + @abstract Return a boolean to indicate that the cover image is ready. + */ @property (nonatomic, readonly) BOOL coverImageReady; +/** + @abstract Return the total duration of the animated image's playback. + */ @property (nonatomic, readonly) CFTimeInterval totalDuration; +/** + @abstract Return the interval at which playback should occur. Will be set to a CADisplayLink's frame interval. + */ @property (nonatomic, readonly) NSUInteger frameInterval; +/** + @abstract Return the total number of loops the animated image should play or 0 to loop infinitely. + */ @property (nonatomic, readonly) size_t loopCount; +/** + @abstract Return the total number of frames in the animated image. + */ @property (nonatomic, readonly) size_t frameCount; +/** + @abstract Return YES when playback is ready to occur. + */ @property (nonatomic, readonly) BOOL playbackReady; +/** + @abstract Return any error that has occured. Playback will be paused if this returns non-nil. + */ +@property (nonatomic, readonly) NSError *error; +/** + @abstract Should be called when playback is ready. + */ @property (nonatomic, strong, readwrite) dispatch_block_t playbackReadyCallback; +/** + @abstract Return the image at a given index. + */ - (CGImageRef)imageAtIndex:(NSUInteger)index; +/** + @abstract Return the duration at a given index. + */ - (CFTimeInterval)durationAtIndex:(NSUInteger)index; +/** + @abstract Clear any cached data. Called when playback is paused. + */ - (void)clearAnimatedImageCache; @end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index eaf0cc9f..1ec0bc57 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -9,16 +9,10 @@ #import "_ASCoreAnimationExtras.h" #import "_ASPendingState.h" #import "ASInternalHelpers.h" -#import "ASAssert.h" #import "ASDisplayNodeInternal.h" -#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" -#import "ASDisplayNode+Beta.h" -#import "ASEqualityHelpers.h" #import "ASPendingStateController.h" -#import "ASThread.h" -#import "ASTextNode.h" /** * The following macros are conveniences to help in the common tasks related to the bridging that ASDisplayNode does to UIView and CALayer. @@ -239,11 +233,11 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: struct ASDisplayNodeFlags flags = _flags; - BOOL setFrameDirectly = flags.synchronous && !flags.layerBacked; + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(flags); BOOL nodeLoaded = __loaded(self); BOOL isMainThread = ASDisplayNodeThreadIsMain(); - if (!setFrameDirectly) { + if (!specialPropertiesHandling) { BOOL canReadProperties = isMainThread || !nodeLoaded; if (canReadProperties) { // We don't have to set frame directly, and we can read current properties. @@ -582,7 +576,14 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo if (shouldApply) { CGColorRef oldBackgroundCGColor = _layer.backgroundColor; - _layer.backgroundColor = newBackgroundCGColor; + + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(_flags); + if (specialPropertiesHandling) { + _view.backgroundColor = newBackgroundColor; + } else { + _layer.backgroundColor = newBackgroundCGColor; + } + if (!CGColorEqualToColor(oldBackgroundCGColor, newBackgroundCGColor)) { [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index ae3f3c47..4828b8be 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -25,8 +25,10 @@ @class _ASDisplayLayer; @class _ASPendingState; @class ASSentinel; +struct ASDisplayNodeFlags; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); +BOOL ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(ASDisplayNodeFlags flags); /// Get the pending view state for the node, creating one if needed. _ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node); diff --git a/AsyncDisplayKit/Private/ASImageNode+AnimatedImagePrivate.h b/AsyncDisplayKit/Private/ASImageNode+AnimatedImagePrivate.h index 2814ef9d..d122c4e5 100644 --- a/AsyncDisplayKit/Private/ASImageNode+AnimatedImagePrivate.h +++ b/AsyncDisplayKit/Private/ASImageNode+AnimatedImagePrivate.h @@ -11,7 +11,6 @@ @interface ASImageNode () { ASDN::RecursiveMutex _animatedImageLock; - ASDN::RecursiveMutex _animatedImagePausedLock; ASDN::Mutex _displayLinkLock; id _animatedImage; BOOL _animatedImagePaused; diff --git a/AsyncDisplayKit/Private/ASSentinel.m b/AsyncDisplayKit/Private/ASSentinel.m index bf9ee479..ed252380 100644 --- a/AsyncDisplayKit/Private/ASSentinel.m +++ b/AsyncDisplayKit/Private/ASSentinel.m @@ -22,7 +22,7 @@ - (int32_t)increment { - return OSAtomicIncrement32(&_value); + return OSAtomicAdd32(1, &_value); } @end diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.m b/AsyncDisplayKit/Private/NSArray+Diffing.m index f0989f8d..837d1ed2 100644 --- a/AsyncDisplayKit/Private/NSArray+Diffing.m +++ b/AsyncDisplayKit/Private/NSArray+Diffing.m @@ -50,30 +50,36 @@ - (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison { NSAssert(comparison != nil, @"Comparison block is required"); - NSInteger lengths[self.count+1][array.count+1]; - for (NSInteger i = self.count; i >= 0; i--) { - for (NSInteger j = array.count; j >= 0; j--) { - if (i == self.count || j == array.count) { + + NSInteger selfCount = self.count; + NSInteger arrayCount = array.count; + + NSInteger lengths[selfCount+1][arrayCount+1]; + for (NSInteger i = 0; i <= selfCount; i++) { + for (NSInteger j = 0; j <= arrayCount; j++) { + if (i == 0 || j == 0) { lengths[i][j] = 0; - } else if (comparison(self[i], array[j])) { - lengths[i][j] = 1 + lengths[i+1][j+1]; + } else if (comparison(self[i-1], array[j-1])) { + lengths[i][j] = 1 + lengths[i-1][j-1]; } else { - lengths[i][j] = MAX(lengths[i+1][j], lengths[i][j+1]); + lengths[i][j] = MAX(lengths[i-1][j], lengths[i][j-1]); } } } NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; - for (NSInteger i = 0, j = 0; i < self.count && j < array.count;) { - if (comparison(self[i], array[j])) { - [common addIndex:i]; - i++; j++; - } else if (lengths[i+1][j] >= lengths[i][j+1]) { - i++; + NSInteger i = selfCount, j = arrayCount; + while(i > 0 && j > 0) { + if (comparison(self[i-1], array[j-1])) { + [common addIndex:(i-1)]; + i--; j--; + } else if (lengths[i-1][j] > lengths[i][j-1]) { + i--; } else { - j++; + j--; } } + return common; } diff --git a/AsyncDisplayKit/Private/_ASPendingState.h b/AsyncDisplayKit/Private/_ASPendingState.h index 8f670246..40986b18 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.h +++ b/AsyncDisplayKit/Private/_ASPendingState.h @@ -24,7 +24,7 @@ // Supports all of the properties included in the ASDisplayNodeViewProperties protocol -- (void)applyToView:(UIView *)view setFrameDirectly:(BOOL)setFrameDirectly; +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)setFrameDirectly; - (void)applyToLayer:(CALayer *)layer; + (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer; diff --git a/AsyncDisplayKit/Private/_ASPendingState.mm b/AsyncDisplayKit/Private/_ASPendingState.mm index b85cbb57..97017907 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.mm +++ b/AsyncDisplayKit/Private/_ASPendingState.mm @@ -745,7 +745,7 @@ static UIColor *defaultTintColor = nil; ASPendingStateApplyMetricsToLayer(self, layer); } -- (void)applyToView:(UIView *)view setFrameDirectly:(BOOL)setFrameDirectly +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling { /* Use our convenience setters blah here instead of layer.blah @@ -789,8 +789,15 @@ static UIColor *defaultTintColor = nil; if (flags.setClipsToBounds) view.clipsToBounds = clipsToBounds; - if (flags.setBackgroundColor) - layer.backgroundColor = backgroundColor; + if (flags.setBackgroundColor) { + // We have to make sure certain nodes get the background color call directly set + if (specialPropertiesHandling) { + view.backgroundColor = [UIColor colorWithCGColor:backgroundColor]; + } else { + // Set the background color to the layer as in the UIView bridge we use this value as background color + layer.backgroundColor = backgroundColor; + } + } if (flags.setTintColor) view.tintColor = self.tintColor; @@ -907,8 +914,7 @@ static UIColor *defaultTintColor = nil; if (flags.setAccessibilityPath) view.accessibilityPath = accessibilityPath; - // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: - if (flags.setFrame && setFrameDirectly) { + if (flags.setFrame && specialPropertiesHandling) { // Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform #if DEBUG // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index 59407a08..3effa243 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -24,25 +24,14 @@ ASDisplayNode *_playerNode; AVPlayer *_player; } -@property (atomic) ASInterfaceState interfaceState; -@property (atomic) ASDisplayNode *spinner; -@property (atomic) ASDisplayNode *playerNode; -@property (atomic) BOOL shouldBePlaying; +@property (atomic, readwrite) ASInterfaceState interfaceState; +@property (atomic, readonly) ASDisplayNode *spinner; +@property (atomic, readonly) ASImageNode *placeholderImageNode; +@property (atomic, readwrite) ASDisplayNode *playerNode; +@property (atomic, readwrite) AVPlayer *player; +@property (atomic, readwrite) BOOL shouldBePlaying; -- (void)setPlayerNode:(ASDisplayNode *)playerNode; -@end - -@implementation ASVideoNode (Test) - -- (void)setPlayerNode:(ASDisplayNode *)playerNode -{ - _playerNode = playerNode; -} - -- (void)setPlayer:(AVPlayer *)player -{ - _player = player; -} +- (void)setVideoPlaceholderImage:(UIImage *)image; @end @@ -124,7 +113,7 @@ _videoNode.interfaceState = ASInterfaceStateFetchData; [_videoNode play]; - [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; + [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{NSKeyValueChangeNewKey : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); } @@ -293,4 +282,107 @@ XCTAssertFalse(_videoNode.player.muted); } +- (void)testVideoThatDoesNotAutorepeatsShouldPauseOnPlaybackEnd +{ + _videoNode.asset = _firstAsset; + _videoNode.shouldAutorepeat = NO; + + [_videoNode didLoad]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode play]; + + XCTAssertTrue(_videoNode.isPlaying); + + [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; + + XCTAssertFalse(_videoNode.isPlaying); + XCTAssertEqual(0, CMTimeGetSeconds(_videoNode.player.currentTime)); +} + +- (void)testVideoThatAutorepeatsShouldRepeatOnPlaybackEnd +{ + _videoNode.asset = _firstAsset; + _videoNode.shouldAutorepeat = YES; + + [_videoNode didLoad]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode play]; + + [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; + + XCTAssertTrue(_videoNode.isPlaying); +} + +- (void)testVideoResumedWhenBufferIsLikelyToKeepUp +{ + _videoNode.asset = _firstAsset; + + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode pause]; + _videoNode.shouldBePlaying = YES; + + XCTAssertFalse(_videoNode.isPlaying); + + [_videoNode observeValueForKeyPath:@"playbackLikelyToKeepUp" ofObject:[_videoNode currentItem] change:@{NSKeyValueChangeNewKey : @YES} context:NULL]; + + XCTAssertTrue(_videoNode.isPlaying); +} + +- (void)testSettingVideoGravityChangesPlaceholderContentMode +{ + [_videoNode setVideoPlaceholderImage:[[UIImage alloc] init]]; + XCTAssertEqual(UIViewContentModeScaleAspectFit, _videoNode.placeholderImageNode.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResize; + XCTAssertEqual(UIViewContentModeScaleToFill, _videoNode.placeholderImageNode.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResizeAspect; + XCTAssertEqual(UIViewContentModeScaleAspectFit, _videoNode.placeholderImageNode.contentMode); + + _videoNode.gravity = AVLayerVideoGravityResizeAspectFill; + XCTAssertEqual(UIViewContentModeScaleAspectFill, _videoNode.placeholderImageNode.contentMode); +} + +- (void)testChangingPlayButtonPerformsProperCleanup +{ + ASButtonNode *firstButton = _videoNode.playButton; + XCTAssertTrue([firstButton.allTargets containsObject:_videoNode]); + + ASButtonNode *secondButton = [[ASButtonNode alloc] init]; + _videoNode.playButton = secondButton; + + XCTAssertTrue([secondButton.allTargets containsObject:_videoNode]); + XCTAssertEqual(_videoNode, secondButton.supernode); + + XCTAssertFalse([firstButton.allTargets containsObject:_videoNode]); + XCTAssertNotEqual(_videoNode, firstButton.supernode); +} + +- (void)testChangingAssetsChangesPlaceholderImage +{ + UIImage *firstImage = [[UIImage alloc] init]; + + _videoNode.asset = _firstAsset; + [_videoNode setVideoPlaceholderImage:firstImage]; + XCTAssertEqual(firstImage, _videoNode.placeholderImageNode.image); + + _videoNode.asset = _secondAsset; + XCTAssertNotEqual(firstImage, _videoNode.placeholderImageNode.image); +} + +- (void)testClearingFetchedContentShouldClearAssetData +{ + _videoNode.asset = _firstAsset; + [_videoNode fetchData]; + [_videoNode setVideoPlaceholderImage:[[UIImage alloc] init]]; + XCTAssertNotNil(_videoNode.player); + XCTAssertNotNil(_videoNode.currentItem); + XCTAssertNotNil(_videoNode.placeholderImageNode.image); + + [_videoNode clearFetchedData]; + XCTAssertNil(_videoNode.player); + XCTAssertNil(_videoNode.currentItem); + XCTAssertNil(_videoNode.placeholderImageNode.image); +} + @end diff --git a/AsyncDisplayKitTests/ArrayDiffingTests.m b/AsyncDisplayKitTests/ArrayDiffingTests.m index 636af908..aaac8e4c 100644 --- a/AsyncDisplayKitTests/ArrayDiffingTests.m +++ b/AsyncDisplayKitTests/ArrayDiffingTests.m @@ -10,13 +10,58 @@ #import "NSArray+Diffing.h" +@interface NSArray (ArrayDiffingTests) +- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison; +@end + @interface ArrayDiffingTests : XCTestCase @end @implementation ArrayDiffingTests -- (void)testDiffing { +- (void)testDiffingCommonIndexes +{ + NSArray *tests = @[ + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice", @"dave", @"gary"], + @[@0, @1, @2] + ], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"gary", @"dave"], + @[@0, @2] + ], + @[ + @[@"bob", @"alice"], + @[@"gary", @"dave"], + @[], + ], + @[ + @[@"bob", @"alice", @"dave"], + @[], + @[], + ], + @[ + @[], + @[@"bob", @"alice", @"dave"], + @[], + ], + ]; + + for (NSArray *test in tests) { + NSIndexSet *indexSet = [test[0] _asdk_commonIndexesWithArray:test[1] compareBlock:^BOOL(id lhs, id rhs) { + return [lhs isEqual:rhs]; + }]; + + for (NSNumber *index in (NSArray *)test[2]) { + XCTAssert([indexSet containsIndex:[index integerValue]]); + } + } +} + +- (void)testDiffingInsertionsAndDeletions { NSArray *tests = @[ @[ @[@"bob", @"alice", @"dave"], diff --git a/examples/ASAnimatedImage/ASAnimatedImage/AppDelegate.m b/examples/ASAnimatedImage/ASAnimatedImage/AppDelegate.m deleted file mode 100644 index b4a601de..00000000 --- a/examples/ASAnimatedImage/ASAnimatedImage/AppDelegate.m +++ /dev/null @@ -1,45 +0,0 @@ -// -// AppDelegate.m -// ASAnimatedImage -// -// Created by Garrett Moon on 3/22/16. -// Copyright © 2016 Facebook, Inc. All rights reserved. -// - -#import "AppDelegate.h" - -@interface AppDelegate () - -@end - -@implementation AppDelegate - - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Override point for customization after application launch. - return YES; -} - -- (void)applicationWillResignActive:(UIApplication *)application { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. -} - -- (void)applicationDidEnterBackground:(UIApplication *)application { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. -} - -- (void)applicationWillEnterForeground:(UIApplication *)application { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. -} - -- (void)applicationWillTerminate:(UIApplication *)application { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. -} - -@end diff --git a/examples/ASAnimatedImage/ASAnimatedImage/AppDelegate.h b/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.h similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/AppDelegate.h rename to examples/AnimatedGIF/ASAnimatedImage/AppDelegate.h diff --git a/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m b/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m new file mode 100644 index 00000000..fc2016ef --- /dev/null +++ b/examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m @@ -0,0 +1,23 @@ +// +// AppDelegate.m +// ASAnimatedImage +// +// Created by Garrett Moon on 3/22/16. +// Copyright © 2016 Facebook, Inc. All rights reserved. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +@end diff --git a/examples/ASAnimatedImage/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/AnimatedGIF/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json rename to examples/AnimatedGIF/ASAnimatedImage/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/examples/ASAnimatedImage/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard b/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard rename to examples/AnimatedGIF/ASAnimatedImage/Base.lproj/LaunchScreen.storyboard diff --git a/examples/ASAnimatedImage/ASAnimatedImage/Base.lproj/Main.storyboard b/examples/AnimatedGIF/ASAnimatedImage/Base.lproj/Main.storyboard similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/Base.lproj/Main.storyboard rename to examples/AnimatedGIF/ASAnimatedImage/Base.lproj/Main.storyboard diff --git a/examples/ASAnimatedImage/ASAnimatedImage/Info.plist b/examples/AnimatedGIF/ASAnimatedImage/Info.plist similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/Info.plist rename to examples/AnimatedGIF/ASAnimatedImage/Info.plist diff --git a/examples/ASAnimatedImage/ASAnimatedImage/ViewController.h b/examples/AnimatedGIF/ASAnimatedImage/ViewController.h similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/ViewController.h rename to examples/AnimatedGIF/ASAnimatedImage/ViewController.h diff --git a/examples/ASAnimatedImage/ASAnimatedImage/ViewController.m b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m similarity index 87% rename from examples/ASAnimatedImage/ASAnimatedImage/ViewController.m rename to examples/AnimatedGIF/ASAnimatedImage/ViewController.m index 9087dfdd..658a93cb 100644 --- a/examples/ASAnimatedImage/ASAnimatedImage/ViewController.m +++ b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -29,9 +29,4 @@ [self.view addSubnode:imageNode]; } -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - @end diff --git a/examples/ASAnimatedImage/ASAnimatedImage/main.m b/examples/AnimatedGIF/ASAnimatedImage/main.m similarity index 100% rename from examples/ASAnimatedImage/ASAnimatedImage/main.m rename to examples/AnimatedGIF/ASAnimatedImage/main.m diff --git a/examples/ASAnimatedImage/Podfile b/examples/AnimatedGIF/Podfile similarity index 100% rename from examples/ASAnimatedImage/Podfile rename to examples/AnimatedGIF/Podfile diff --git a/examples/ASAnimatedImage/Sample.xcodeproj/project.pbxproj b/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj similarity index 100% rename from examples/ASAnimatedImage/Sample.xcodeproj/project.pbxproj rename to examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj diff --git a/examples/ASAnimatedImage/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/AnimatedGIF/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from examples/ASAnimatedImage/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to examples/AnimatedGIF/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/examples/ASAnimatedImage/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/AnimatedGIF/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme similarity index 100% rename from examples/ASAnimatedImage/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme rename to examples/AnimatedGIF/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme diff --git a/examples/ASAnimatedImage/Sample.xcworkspace/contents.xcworkspacedata b/examples/AnimatedGIF/Sample.xcworkspace/contents.xcworkspacedata similarity index 100% rename from examples/ASAnimatedImage/Sample.xcworkspace/contents.xcworkspacedata rename to examples/AnimatedGIF/Sample.xcworkspace/contents.xcworkspacedata