mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-06-16 01:44:23 +08:00
Merge remote-tracking branch 'facebook/master'
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASFlowLayoutController.mm; sourceTree = "<group>"; };
|
||||
4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = "<group>"; };
|
||||
68355B2D1CB5799E001D4E68 /* ASImageNode+AnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+AnimatedImage.h"; sourceTree = "<group>"; };
|
||||
68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = "<group>"; };
|
||||
68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPINRemoteImageDownloader.m; sourceTree = "<group>"; };
|
||||
68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = "<group>"; };
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 <ASAnimatedImageProtocol> animatedImage;
|
||||
@end
|
||||
@@ -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 <ASAnimatedImageProtocol>)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();
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#import <AsyncDisplayKit/ASControlNode.h>
|
||||
|
||||
#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 <ASAnimatedImageProtocol> 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
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
|
||||
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
|
||||
#import <AsyncDisplayKit/ASTextNode.h>
|
||||
#import <AsyncDisplayKit/ASImageNode+AnimatedImage.h>
|
||||
#import <AsyncDisplayKit/ASImageNode+AnimatedImagePrivate.h>
|
||||
|
||||
#import "ASImageNode+CGExtras.h"
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#import "ASThread.h"
|
||||
#import "ASInternalHelpers.h"
|
||||
#import "ASImageContainerProtocolCategories.h"
|
||||
#import "ASImageNode+AnimatedImage.h"
|
||||
|
||||
#if PIN_REMOTE_IMAGE
|
||||
#import "ASPINRemoteImageDownloader.h"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ASVideoNodeDelegate> delegate;
|
||||
//! Defaults to an ASDefaultPlayButton instance.
|
||||
@property (nullable, atomic) ASButtonNode *playButton;
|
||||
|
||||
@property (nullable, atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
@protocol ASVideoNodeDelegate <NSObject>
|
||||
@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
|
||||
|
||||
@@ -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<AVAssetTrack *>* 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
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#import <AsyncDisplayKit/ASControlNode.h>
|
||||
#import <AsyncDisplayKit/ASImageNode.h>
|
||||
#import <AsyncDisplayKit/ASImageNode+AnimatedImage.h>
|
||||
#import <AsyncDisplayKit/ASTextNode.h>
|
||||
#import <AsyncDisplayKit/ASButtonNode.h>
|
||||
#import <AsyncDisplayKit/ASMapNode.h>
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -136,21 +136,62 @@ withDownloadIdentifier:(id)downloadIdentifier;
|
||||
|
||||
@protocol ASAnimatedImageProtocol <NSObject>
|
||||
|
||||
/**
|
||||
@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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
@interface ASImageNode ()
|
||||
{
|
||||
ASDN::RecursiveMutex _animatedImageLock;
|
||||
ASDN::RecursiveMutex _animatedImagePausedLock;
|
||||
ASDN::Mutex _displayLinkLock;
|
||||
id <ASAnimatedImageProtocol> _animatedImage;
|
||||
BOOL _animatedImagePaused;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
- (int32_t)increment
|
||||
{
|
||||
return OSAtomicIncrement32(&_value);
|
||||
return OSAtomicAdd32(1, &_value);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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<NSArray *> *tests = @[
|
||||
@[
|
||||
@[@"bob", @"alice", @"dave"],
|
||||
|
||||
@@ -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
|
||||
23
examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m
Normal file
23
examples/AnimatedGIF/ASAnimatedImage/AppDelegate.m
Normal file
@@ -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
|
||||
@@ -29,9 +29,4 @@
|
||||
[self.view addSubnode:imageNode];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user