From 8c8fc3dba0eb692fb8676b0c332343149b90fb15 Mon Sep 17 00:00:00 2001 From: Erekle Date: Sat, 14 May 2016 16:41:32 +0400 Subject: [PATCH] Adding ASVideoPlayerNode --- AsyncDisplayKit.xcodeproj/project.pbxproj | 10 +- AsyncDisplayKit/ASVideoPlayerNode.h | 12 +- AsyncDisplayKit/ASVideoPlayerNode.mm | 67 +++-- .../Private/ASDefaultPlaybackButton.h | 16 ++ .../Private/ASDefaultPlaybackButton.m | 81 ++++++ .../ASDKTube/Sample.xcodeproj/project.pbxproj | 54 ++++ examples/ASDKTube/Sample/AppDelegate.m | 34 ++- .../Controller/VideoFeedNodeController.h | 13 + .../Controller/VideoFeedNodeController.m | 71 ++++++ examples/ASDKTube/Sample/Info.plist | 6 +- examples/ASDKTube/Sample/Models/Utilities.h | 40 +++ examples/ASDKTube/Sample/Models/Utilities.m | 230 ++++++++++++++++++ examples/ASDKTube/Sample/Models/VideoModel.h | 16 ++ examples/ASDKTube/Sample/Models/VideoModel.m | 27 ++ .../ASDKTube/Sample/Nodes/VideoContentCell.h | 14 ++ .../ASDKTube/Sample/Nodes/VideoContentCell.m | 151 ++++++++++++ examples/ASDKTube/Sample/ViewController.h | 2 +- examples/ASDKTube/Sample/ViewController.m | 134 ++++++---- .../Sample/WindowWithStatusBarUnderlay.h | 13 + .../Sample/WindowWithStatusBarUnderlay.m | 39 +++ .../ASDKgram/Sample.xcodeproj/project.pbxproj | 18 +- .../contents.xcworkspacedata | 10 + 22 files changed, 974 insertions(+), 84 deletions(-) create mode 100644 AsyncDisplayKit/Private/ASDefaultPlaybackButton.h create mode 100644 AsyncDisplayKit/Private/ASDefaultPlaybackButton.m create mode 100644 examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h create mode 100644 examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m create mode 100644 examples/ASDKTube/Sample/Models/Utilities.h create mode 100644 examples/ASDKTube/Sample/Models/Utilities.m create mode 100644 examples/ASDKTube/Sample/Models/VideoModel.h create mode 100644 examples/ASDKTube/Sample/Models/VideoModel.m create mode 100644 examples/ASDKTube/Sample/Nodes/VideoContentCell.h create mode 100644 examples/ASDKTube/Sample/Nodes/VideoContentCell.m create mode 100644 examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h create mode 100644 examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m create mode 100644 examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index f9e68c87cf..54eb504046 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -311,6 +311,8 @@ 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */; }; 81EE384F1C8E94F000456208 /* ASRunLoopQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; 81EE38501C8E94F000456208 /* ASRunLoopQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */; }; + 8B0768B31CE752EC002E1453 /* ASDefaultPlaybackButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */; }; + 8B0768B41CE752EC002E1453 /* ASDefaultPlaybackButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */; }; 8BDA5FC51CDBDDE1007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; }; 8BDA5FC61CDBDDE1007D13B2 /* ASVideoPlayerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */; }; 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; }; @@ -824,6 +826,8 @@ 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpecSnapshotTests.mm; sourceTree = ""; }; 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRunLoopQueue.h; path = ../ASRunLoopQueue.h; sourceTree = ""; }; 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRunLoopQueue.mm; path = ../ASRunLoopQueue.mm; sourceTree = ""; }; + 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDefaultPlaybackButton.h; sourceTree = ""; }; + 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDefaultPlaybackButton.m; sourceTree = ""; }; 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVideoPlayerNode.h; sourceTree = ""; }; 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVideoPlayerNode.mm; sourceTree = ""; }; 92074A5F1CC8BA1900918F75 /* ASImageNode+tvOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+tvOS.h"; sourceTree = ""; }; @@ -1340,6 +1344,8 @@ CC3B20881C3F7A5400798563 /* ASWeakSet.m */, DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */, + 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */, + 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */, ); path = Private; sourceTree = ""; @@ -1600,6 +1606,7 @@ B13CA1001C52004900E031AB /* ASCollectionNode+Beta.h in Headers */, 0442850D1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, + 8B0768B31CE752EC002E1453 /* ASDefaultPlaybackButton.h in Headers */, 058D0A59195D05DC00B7D73C /* ASMutableAttributedStringBuilder.h in Headers */, 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */, ACF6ED2B1B17843500DA7C62 /* ASOverlayLayoutSpec.h in Headers */, @@ -2014,6 +2021,7 @@ buildActionMask = 2147483647; files = ( 058D0A22195D050800B7D73C /* _ASAsyncTransaction.mm in Sources */, + 8B0768B41CE752EC002E1453 /* ASDefaultPlaybackButton.m in Sources */, E55D86321CA8A14000A0C26F /* ASLayoutable.mm in Sources */, 68FC85E41CE29B7E00EDD713 /* ASTabBarController.m in Sources */, 058D0A23195D050800B7D73C /* _ASAsyncTransactionContainer.m in Sources */, @@ -2119,8 +2127,6 @@ 058D0A17195D050800B7D73C /* ASTextNode.mm in Sources */, 257754AC1BEE44CD00737CA5 /* ASTextKitRenderer.mm in Sources */, 8BDA5FC61CDBDDE1007D13B2 /* ASVideoPlayerNode.mm in Sources */, - ACC945AB1BA9E7C1005E1FB8 /* ASViewController.m in Sources */, - B0F8805B1BEAEC7500D17647 /* ASTableNode.m in Sources */, 205F0E221B376416007741D0 /* CGRect+ASConvenience.m in Sources */, 257754B21BEE44CD00737CA5 /* ASTextKitShadower.mm in Sources */, 9CFFC6BE1CCAC52B006A6476 /* ASEnvironment.mm in Sources */, diff --git a/AsyncDisplayKit/ASVideoPlayerNode.h b/AsyncDisplayKit/ASVideoPlayerNode.h index 010e31bcb2..41c22eceea 100644 --- a/AsyncDisplayKit/ASVideoPlayerNode.h +++ b/AsyncDisplayKit/ASVideoPlayerNode.h @@ -71,12 +71,12 @@ NS_ASSUME_NONNULL_BEGIN * @abstract Delegate method invoked in layoutSpecThatFits: * @param videoPlayer * @param controls - Dictionary of controls which are used in videoPlayer; Dictionary keys are ASVideoPlayerNodeControlType - * @param constrainedSize - ASSizeRange for ASVideoPlayerNode + * @param maxSize - Maximum size for ASVideoPlayerNode * @discussion - Developer can layout whole ASVideoPlayerNode as he wants. ASVideoNode is locked and it can't be changed */ - (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer forControls:(NSDictionary *)controls - forConstrainedSize:(ASSizeRange)constrainedSize; + forMaximumSize:(CGSize)maxSize; #pragma mark Text delegate methods /** @@ -95,8 +95,16 @@ NS_ASSUME_NONNULL_BEGIN - (UIColor *)videoPlayerNodeScrubberThumbTint:(ASVideoPlayerNode *)videoPlayer; - (UIImage *)videoPlayerNodeScrubberThumbImage:(ASVideoPlayerNode *)videoPlayer; +#pragma mark - Playback button delegate methods +- (UIColor *)videoPlayerNodePlaybackButtonTint:(ASVideoPlayerNode *)videoPlayer; + #pragma mark ASVideoNodeDelegate proxy methods +/** + * @abstract Delegate method invoked when ASVideoPlayerNode playback time is taped. + * @param videoPlayerNode The ASVideoPlayerNode that was tapped. + */ +- (void)videoPlayerNodeWasTapped:(ASVideoPlayerNode *)videoPlayer; /** * @abstract Delegate method invoked when ASVideoNode playback time is updated. * @param videoPlayerNode The video node that was tapped. diff --git a/AsyncDisplayKit/ASVideoPlayerNode.mm b/AsyncDisplayKit/ASVideoPlayerNode.mm index e84665c7bc..e4d3482a40 100644 --- a/AsyncDisplayKit/ASVideoPlayerNode.mm +++ b/AsyncDisplayKit/ASVideoPlayerNode.mm @@ -7,6 +7,7 @@ // #import "ASVideoPlayerNode.h" +#import "ASDefaultPlaybackButton.h" static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; @@ -18,6 +19,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; struct { unsigned int delegateNeededControls:1; + unsigned int delegatePlaybackButtonTint:1; unsigned int delegateScrubberMaximumTrackTintColor:1; unsigned int delegateScrubberMinimumTrackTintColor:1; unsigned int delegateScrubberThumbTintColor:1; @@ -29,6 +31,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; unsigned int delegateVideoNodeWillChangeState:1; unsigned int delegateVideoNodeShouldChangeState:1; unsigned int delegateVideoNodePlaybackDidFinish:1; + unsigned int delegateVideoNodeTapped:1; } _delegateFlags; NSURL *_url; @@ -40,7 +43,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; NSMutableDictionary *_cachedControls; - ASControlNode *_playbackButtonNode; + ASDefaultPlaybackButton *_playbackButtonNode; ASTextNode *_elapsedTextNode; ASTextNode *_durationTextNode; ASDisplayNode *_scrubberNode; @@ -56,6 +59,8 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; BOOL _muted; int32_t _periodicTimeObserverTimescale; NSString *_gravity; + + UIColor *_defaultControlsColor; } @end @@ -103,6 +108,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; - (void)privateInit { + _defaultControlsColor = [UIColor whiteColor]; _cachedControls = [[NSMutableDictionary alloc] init]; _videoNode = [[ASVideoNode alloc] init]; @@ -213,9 +219,13 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; - (void)createPlaybackButton { if (_playbackButtonNode == nil) { - _playbackButtonNode = [[ASControlNode alloc] init]; - _playbackButtonNode.preferredFrameSize = CGSizeMake(20.0, 20.0); - _playbackButtonNode.backgroundColor = [UIColor redColor]; + _playbackButtonNode = [[ASDefaultPlaybackButton alloc] init]; + _playbackButtonNode.preferredFrameSize = CGSizeMake(16.0, 22.0); + if (_delegateFlags.delegatePlaybackButtonTint) { + _playbackButtonNode.tintColor = [_delegate videoPlayerNodePlaybackButtonTint:self]; + } else { + _playbackButtonNode.tintColor = _defaultControlsColor; + } [_playbackButtonNode addTarget:self action:@selector(playbackButtonTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; [_cachedControls setObject:_playbackButtonNode forKey:@(ASVideoPlayerNodeControlTypePlaybackButton)]; } @@ -316,7 +326,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; } else { options = @{ NSFontAttributeName : [UIFont systemFontOfSize:12.0], - NSForegroundColorAttributeName: [UIColor whiteColor] + NSForegroundColorAttributeName: _defaultControlsColor }; } @@ -337,6 +347,12 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; _duration = _videoNode.currentItem.duration; [self updateDurationTimeLabel]; } + + if (toState == ASVideoNodePlayerStatePlaying) { + _playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePause; + } else { + _playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePlay; + } } - (BOOL)videoNode:(ASVideoNode *)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state @@ -376,15 +392,26 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; } } +- (void)videoNodeWasTapped:(ASVideoNode *)videoNode +{ + if (_delegateFlags.delegateVideoNodeTapped) { + [_delegate videoPlayerNodeWasTapped:self]; + } else { + if (videoNode.playerState == ASVideoNodePlayerStatePlaying) { + [videoNode pause]; + } else { + [videoNode play]; + } + } +} + #pragma mark - Actions - (void)playbackButtonTapped:(ASControlNode*)node { if (_videoNode.playerState == ASVideoNodePlayerStatePlaying) { [_videoNode pause]; - _playbackButtonNode.backgroundColor = [UIColor greenColor]; } else { [_videoNode play]; - _playbackButtonNode.backgroundColor = [UIColor redColor]; } } @@ -458,25 +485,35 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; #pragma mark - Layout - (ASLayoutSpec*)layoutSpecThatFits:(ASSizeRange)constrainedSize { - _videoNode.preferredFrameSize = constrainedSize.max; + CGSize maxSize = constrainedSize.max; + if (!CGSizeEqualToSize(self.preferredFrameSize, CGSizeZero)) { + maxSize = self.preferredFrameSize; + } + + // Prevent crashes through if infinite width or height + if (isinf(maxSize.width) || isinf(maxSize.height)) { + ASDisplayNodeAssert(NO, @"Infinite width or height in ASVideoPlayerNode"); + maxSize = CGSizeZero; + } + _videoNode.preferredFrameSize = maxSize; ASLayoutSpec *layoutSpec; if (_delegateFlags.delegateLayoutSpecForControls) { - layoutSpec = [_delegate videoPlayerNodeLayoutSpec:self forControls:_cachedControls forConstrainedSize:constrainedSize]; + layoutSpec = [_delegate videoPlayerNodeLayoutSpec:self forControls:_cachedControls forMaximumSize:maxSize]; } else { - layoutSpec = [self defaultLayoutSpecThatFits:constrainedSize]; + layoutSpec = [self defaultLayoutSpecThatFits:maxSize]; } ASOverlayLayoutSpec *overlaySpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_videoNode overlay:layoutSpec]; - overlaySpec.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(constrainedSize.max); + overlaySpec.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(maxSize); return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[overlaySpec]]; } -- (ASLayoutSpec*)defaultLayoutSpecThatFits:(ASSizeRange)constrainedSize +- (ASLayoutSpec*)defaultLayoutSpecThatFits:(CGSize)maxSize { - _scrubberNode.preferredFrameSize = CGSizeMake(constrainedSize.max.width, 44.0); + _scrubberNode.preferredFrameSize = CGSizeMake(maxSize.width, 44.0); ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; spacer.flexGrow = YES; @@ -521,12 +558,14 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; _delegateFlags.delegateScrubberThumbTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberThumbTint:)]; _delegateFlags.delegateScrubberThumbImage = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberThumbImage:)]; _delegateFlags.delegateTimeLabelAttributes = [_delegate respondsToSelector:@selector(videoPlayerNodeTimeLabelAttributes:timeLabelType:)]; - _delegateFlags.delegateLayoutSpecForControls = [_delegate respondsToSelector:@selector(videoPlayerNodeLayoutSpec:forControls:forConstrainedSize:)]; + _delegateFlags.delegateLayoutSpecForControls = [_delegate respondsToSelector:@selector(videoPlayerNodeLayoutSpec:forControls:forMaximumSize:)]; _delegateFlags.delegateVideoNodeDidPlayToTime = [_delegate respondsToSelector:@selector(videoPlayerNode:didPlayToTime:)]; _delegateFlags.delegateVideoNodeWillChangeState = [_delegate respondsToSelector:@selector(videoPlayerNode:willChangeVideoNodeState:toVideoNodeState:)]; _delegateFlags.delegateVideoNodePlaybackDidFinish = [_delegate respondsToSelector:@selector(videoPlayerNodeDidPlayToEnd:)]; _delegateFlags.delegateVideoNodeShouldChangeState = [_delegate respondsToSelector:@selector(videoPlayerNode:shouldChangeVideoNodeStateTo:)]; _delegateFlags.delegateTimeLabelAttributedString = [_delegate respondsToSelector:@selector(videoPlayerNode:timeStringForTimeLabelType:forTime:)]; + _delegateFlags.delegatePlaybackButtonTint = [_delegate respondsToSelector:@selector(videoPlayerNodePlaybackButtonTint:)]; + _delegateFlags.delegateVideoNodeTapped = [_delegate respondsToSelector:@selector(videoPlayerNodeWasTapped:)]; } } diff --git a/AsyncDisplayKit/Private/ASDefaultPlaybackButton.h b/AsyncDisplayKit/Private/ASDefaultPlaybackButton.h new file mode 100644 index 0000000000..9f502688e1 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDefaultPlaybackButton.h @@ -0,0 +1,16 @@ +// +// ASDefaultPlaybackButton.h +// AsyncDisplayKit +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +typedef enum { + ASDefaultPlaybackButtonTypePlay, + ASDefaultPlaybackButtonTypePause +} ASDefaultPlaybackButtonType; +@interface ASDefaultPlaybackButton : ASControlNode +@property (nonatomic, assign) ASDefaultPlaybackButtonType buttonType; +@end diff --git a/AsyncDisplayKit/Private/ASDefaultPlaybackButton.m b/AsyncDisplayKit/Private/ASDefaultPlaybackButton.m new file mode 100644 index 0000000000..6e9455bde5 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDefaultPlaybackButton.m @@ -0,0 +1,81 @@ +// +// ASDefaultPlaybackButton.m +// AsyncDisplayKit +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASDefaultPlaybackButton.h" +@interface ASDefaultPlaybackButton() +{ + ASDefaultPlaybackButtonType _buttonType; +} +@end + +@implementation ASDefaultPlaybackButton +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + self.opaque = NO; + + return self; +} + +- (void)setButtonType:(ASDefaultPlaybackButtonType)buttonType +{ + ASDefaultPlaybackButtonType oldType = _buttonType; + _buttonType = buttonType; + + if (oldType != _buttonType) { + [self setNeedsDisplay]; + } +} + +- (nullable id)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + return @{ + @"buttonType" : [NSNumber numberWithInt:_buttonType], + @"color" : self.tintColor + }; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + ASDefaultPlaybackButtonType buttonType = [parameters[@"buttonType"] intValue]; + UIColor *color = parameters[@"color"]; + + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + UIBezierPath* bezierPath = [UIBezierPath bezierPath]; + if (buttonType == ASDefaultPlaybackButtonTypePlay) { + [bezierPath moveToPoint: CGPointMake(0, 0)]; + [bezierPath addLineToPoint: CGPointMake(0, bounds.size.height)]; + [bezierPath addLineToPoint: CGPointMake(bounds.size.width, bounds.size.height/2)]; + [bezierPath addLineToPoint: CGPointMake(0, 0)]; + [bezierPath closePath]; + } else if (buttonType == ASDefaultPlaybackButtonTypePause) { + CGFloat pauseSingleLineWidth = bounds.size.width / 3.0; + [bezierPath moveToPoint: CGPointMake(0, bounds.size.height)]; + [bezierPath addLineToPoint: CGPointMake(pauseSingleLineWidth, bounds.size.height)]; + [bezierPath addLineToPoint: CGPointMake(pauseSingleLineWidth, 0)]; + [bezierPath addLineToPoint: CGPointMake(0, 0)]; + [bezierPath addLineToPoint: CGPointMake(0, bounds.size.height)]; + [bezierPath closePath]; + [bezierPath moveToPoint: CGPointMake(pauseSingleLineWidth * 2, 0)]; + [bezierPath addLineToPoint: CGPointMake(pauseSingleLineWidth * 2, bounds.size.height)]; + [bezierPath addLineToPoint: CGPointMake(bounds.size.width, bounds.size.height)]; + [bezierPath addLineToPoint: CGPointMake(bounds.size.width, 0)]; + [bezierPath addLineToPoint: CGPointMake(pauseSingleLineWidth * 2, 0)]; + [bezierPath closePath]; + } + + [color setFill]; + [bezierPath fill]; + + CGContextRestoreGState(context); +} +@end diff --git a/examples/ASDKTube/Sample.xcodeproj/project.pbxproj b/examples/ASDKTube/Sample.xcodeproj/project.pbxproj index e9965d6274..f808ae18f9 100644 --- a/examples/ASDKTube/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKTube/Sample.xcodeproj/project.pbxproj @@ -14,6 +14,11 @@ 5791C5525B690FA54F26ACE8 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A2092CAF5607B3863A3700A2 /* libPods-Sample.a */; }; 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; + 8B0768B81CE7AD03002E1453 /* VideoModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B71CE7AD03002E1453 /* VideoModel.m */; }; + 8B0768BC1CE7B091002E1453 /* VideoContentCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768BB1CE7B091002E1453 /* VideoContentCell.m */; }; + 8B0768BF1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768BE1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.m */; }; + 8B0768C51CE7C707002E1453 /* Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768C41CE7C707002E1453 /* Utilities.m */; }; + 8B0768C91CE7C889002E1453 /* VideoFeedNodeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768C81CE7C889002E1453 /* VideoFeedNodeController.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -27,6 +32,16 @@ 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + 8B0768B61CE7AD03002E1453 /* VideoModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoModel.h; sourceTree = ""; }; + 8B0768B71CE7AD03002E1453 /* VideoModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoModel.m; sourceTree = ""; }; + 8B0768BA1CE7B091002E1453 /* VideoContentCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoContentCell.h; sourceTree = ""; }; + 8B0768BB1CE7B091002E1453 /* VideoContentCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoContentCell.m; sourceTree = ""; }; + 8B0768BD1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowWithStatusBarUnderlay.h; sourceTree = ""; }; + 8B0768BE1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WindowWithStatusBarUnderlay.m; sourceTree = ""; }; + 8B0768C31CE7C707002E1453 /* Utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utilities.h; sourceTree = ""; }; + 8B0768C41CE7C707002E1453 /* Utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utilities.m; sourceTree = ""; }; + 8B0768C71CE7C889002E1453 /* VideoFeedNodeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoFeedNodeController.h; sourceTree = ""; }; + 8B0768C81CE7C889002E1453 /* VideoFeedNodeController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoFeedNodeController.m; sourceTree = ""; }; A2092CAF5607B3863A3700A2 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; CFD6AA1D30516C27DEE5602B /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; E51646FF8D3676A1D826A5AE /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; @@ -68,11 +83,16 @@ 05E2128319D4DB510098F589 /* Sample */ = { isa = PBXGroup; children = ( + 8B0768C61CE7C85F002E1453 /* Controller */, + 8B0768B91CE7B07E002E1453 /* Nodes */, + 8B0768B51CE7ACE8002E1453 /* Models */, 05E2128819D4DB510098F589 /* AppDelegate.h */, 05E2128919D4DB510098F589 /* AppDelegate.m */, 05E2128B19D4DB510098F589 /* ViewController.h */, 05E2128C19D4DB510098F589 /* ViewController.m */, 05E2128419D4DB510098F589 /* Supporting Files */, + 8B0768BD1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.h */, + 8B0768BE1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.m */, ); path = Sample; sourceTree = ""; @@ -106,6 +126,35 @@ name = Pods; sourceTree = ""; }; + 8B0768B51CE7ACE8002E1453 /* Models */ = { + isa = PBXGroup; + children = ( + 8B0768C31CE7C707002E1453 /* Utilities.h */, + 8B0768C41CE7C707002E1453 /* Utilities.m */, + 8B0768B61CE7AD03002E1453 /* VideoModel.h */, + 8B0768B71CE7AD03002E1453 /* VideoModel.m */, + ); + path = Models; + sourceTree = ""; + }; + 8B0768B91CE7B07E002E1453 /* Nodes */ = { + isa = PBXGroup; + children = ( + 8B0768BA1CE7B091002E1453 /* VideoContentCell.h */, + 8B0768BB1CE7B091002E1453 /* VideoContentCell.m */, + ); + path = Nodes; + sourceTree = ""; + }; + 8B0768C61CE7C85F002E1453 /* Controller */ = { + isa = PBXGroup; + children = ( + 8B0768C71CE7C889002E1453 /* VideoFeedNodeController.h */, + 8B0768C81CE7C889002E1453 /* VideoFeedNodeController.m */, + ); + path = Controller; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -228,8 +277,13 @@ buildActionMask = 2147483647; files = ( 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 8B0768C91CE7C889002E1453 /* VideoFeedNodeController.m in Sources */, 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 8B0768BC1CE7B091002E1453 /* VideoContentCell.m in Sources */, + 8B0768BF1CE7C5A1002E1453 /* WindowWithStatusBarUnderlay.m in Sources */, 05E2128719D4DB510098F589 /* main.m in Sources */, + 8B0768C51CE7C707002E1453 /* Utilities.m in Sources */, + 8B0768B81CE7AD03002E1453 /* VideoModel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/ASDKTube/Sample/AppDelegate.m b/examples/ASDKTube/Sample/AppDelegate.m index a8e5594780..947d95ca09 100644 --- a/examples/ASDKTube/Sample/AppDelegate.m +++ b/examples/ASDKTube/Sample/AppDelegate.m @@ -10,18 +10,36 @@ */ #import "AppDelegate.h" +#import "WindowWithStatusBarUnderlay.h" +#import "Utilities.h" +#import "VideoFeedNodeController.h" -#import "ViewController.h" @implementation AppDelegate -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - self.window.backgroundColor = [UIColor whiteColor]; - self.window.rootViewController = [[ViewController alloc] init]; - [self.window makeKeyAndVisible]; +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + // this UIWindow subclass is neccessary to make the status bar opaque + _window = [[WindowWithStatusBarUnderlay alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + _window.backgroundColor = [UIColor whiteColor]; + + + VideoFeedNodeController *asdkHomeFeedVC = [[VideoFeedNodeController alloc] init]; + UINavigationController *asdkHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:asdkHomeFeedVC]; + + + _window.rootViewController = asdkHomeFeedNavCtrl; + [_window makeKeyAndVisible]; + + // Nav Bar appearance + NSDictionary *attributes = @{NSForegroundColorAttributeName:[UIColor whiteColor]}; + [[UINavigationBar appearance] setTitleTextAttributes:attributes]; + [[UINavigationBar appearance] setBarTintColor:[UIColor lighOrangeColor]]; + [[UINavigationBar appearance] setTranslucent:NO]; + + [application setStatusBarStyle:UIStatusBarStyleLightContent]; + + return YES; } - @end diff --git a/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h b/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h new file mode 100644 index 0000000000..28d7758f6b --- /dev/null +++ b/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.h @@ -0,0 +1,13 @@ +// +// VideoFeedNodeController.h +// Sample +// +// Created by Erekle on 5/15/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface VideoFeedNodeController : ASViewController + +@end diff --git a/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m b/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m new file mode 100644 index 0000000000..fea764e18e --- /dev/null +++ b/examples/ASDKTube/Sample/Controller/VideoFeedNodeController.m @@ -0,0 +1,71 @@ +// +// VideoFeedNodeController.m +// Sample +// +// Created by Erekle on 5/15/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "VideoFeedNodeController.h" +#import +#import "VideoModel.h" +#import "VideoContentCell.h" + +@interface VideoFeedNodeController () + +@end + +@implementation VideoFeedNodeController +{ + ASTableNode *_tableNode; + NSMutableArray *_videoFeedData; +} + +- (instancetype)init +{ + self.navigationItem.title = @"Home"; + _tableNode = [[ASTableNode alloc] init]; + _tableNode.delegate = self; + _tableNode.dataSource = self; + + if (!(self = [super initWithNode:_tableNode])) { + return nil; + } + + return self; +} + +- (void)loadView +{ + [super loadView]; + + [self generateFeedData]; + + [_tableNode.view reloadData]; +} + +- (void)generateFeedData +{ + _videoFeedData = [[NSMutableArray alloc] init]; + + for (int i = 0; i < 30; i++) { + [_videoFeedData addObject:[[VideoModel alloc] init]]; + } +} + +#pragma mark - ASCollectionDelegate - ASCollectionDataSource +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ + return _videoFeedData.count; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + VideoModel *videoObject = [_videoFeedData objectAtIndex:indexPath.row]; + VideoContentCell *cellNode = [[VideoContentCell alloc] initWithVideoObject:videoObject]; + return cellNode; +} +@end diff --git a/examples/ASDKTube/Sample/Info.plist b/examples/ASDKTube/Sample/Info.plist index 35d842827b..3b4c6c7839 100644 --- a/examples/ASDKTube/Sample/Info.plist +++ b/examples/ASDKTube/Sample/Info.plist @@ -2,6 +2,8 @@ + UIViewControllerBasedStatusBarAppearance + CFBundleDevelopmentRegion en CFBundleExecutable @@ -26,11 +28,11 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight diff --git a/examples/ASDKTube/Sample/Models/Utilities.h b/examples/ASDKTube/Sample/Models/Utilities.h new file mode 100644 index 0000000000..8928fd9747 --- /dev/null +++ b/examples/ASDKTube/Sample/Models/Utilities.h @@ -0,0 +1,40 @@ +// +// Utilities.h +// ASDKgram +// +// Created by Hannah Troisi on 3/9/16. +// Copyright © 2016 Hannah Troisi. All rights reserved. +// +#include +@interface UIColor (Additions) + ++ (UIColor *)lighOrangeColor; ++ (UIColor *)darkBlueColor; ++ (UIColor *)lightBlueColor; + +@end + +@interface UIImage (Additions) + ++ (UIImage *)followingButtonStretchableImageForCornerRadius:(CGFloat)cornerRadius following:(BOOL)followingEnabled; ++ (void)downloadImageForURL:(NSURL *)url completion:(void (^)(UIImage *))block; + +- (UIImage *)makeCircularImageWithSize:(CGSize)size; + +@end + +@interface NSString (Additions) + +// returns a user friendly elapsed time such as '50s', '6m' or '3w' ++ (NSString *)elapsedTimeStringSinceDate:(NSString *)uploadDateString; + +@end + +@interface NSAttributedString (Additions) + ++ (NSAttributedString *)attributedStringWithString:(NSString *)string + fontSize:(CGFloat)size + color:(UIColor *)color + firstWordColor:(UIColor *)firstWordColor; + +@end \ No newline at end of file diff --git a/examples/ASDKTube/Sample/Models/Utilities.m b/examples/ASDKTube/Sample/Models/Utilities.m new file mode 100644 index 0000000000..79b393c106 --- /dev/null +++ b/examples/ASDKTube/Sample/Models/Utilities.m @@ -0,0 +1,230 @@ +// +// Utilities.m +// ASDKgram +// +// Created by Hannah Troisi on 3/9/16. +// Copyright © 2016 Hannah Troisi. All rights reserved. +// + +#import "Utilities.h" + +#define StrokeRoundedImages 0 + +@implementation UIColor (Additions) + ++ (UIColor *)lighOrangeColor +{ + return [UIColor colorWithRed:1 green:0.506 blue:0.384 alpha:1]; +} + ++ (UIColor *)darkBlueColor +{ + return [UIColor colorWithRed:70.0/255.0 green:102.0/255.0 blue:118.0/255.0 alpha:1.0]; +} + ++ (UIColor *)lightBlueColor +{ + return [UIColor colorWithRed:70.0/255.0 green:165.0/255.0 blue:196.0/255.0 alpha:1.0]; +} + +@end + +@implementation UIImage (Additions) + ++ (UIImage *)followingButtonStretchableImageForCornerRadius:(CGFloat)cornerRadius following:(BOOL)followingEnabled +{ + CGSize unstretchedSize = CGSizeMake(2 * cornerRadius + 1, 2 * cornerRadius + 1); + CGRect rect = (CGRect) {CGPointZero, unstretchedSize}; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; + + // create a graphics context for the following status button + UIGraphicsBeginImageContextWithOptions(unstretchedSize, NO, 0); + + [path addClip]; + + if (followingEnabled) { + + [[UIColor whiteColor] setFill]; + [path fill]; + + path.lineWidth = 3; + [[UIColor lightBlueColor] setStroke]; + [path stroke]; + + } else { + + [[UIColor lightBlueColor] setFill]; + [path fill]; + } + + UIImage *followingBtnImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + UIImage *followingBtnImageStretchable = [followingBtnImage stretchableImageWithLeftCapWidth:cornerRadius + topCapHeight:cornerRadius]; + return followingBtnImageStretchable; +} + ++ (void)downloadImageForURL:(NSURL *)url completion:(void (^)(UIImage *))block +{ + static NSCache *simpleImageCache = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + simpleImageCache = [[NSCache alloc] init]; + simpleImageCache.countLimit = 10; + }); + + if (!block) { + return; + } + + // check if image is cached + UIImage *image = [simpleImageCache objectForKey:url]; + if (image) { + dispatch_async(dispatch_get_main_queue(), ^{ + block(image); + }); + } else { + // else download image + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (data) { + UIImage *image = [UIImage imageWithData:data]; + dispatch_async(dispatch_get_main_queue(), ^{ + block(image); + }); + } + }]; + [task resume]; + } +} + +- (UIImage *)makeCircularImageWithSize:(CGSize)size +{ + // make a CGRect with the image's size + CGRect circleRect = (CGRect) {CGPointZero, size}; + + // begin the image context since we're not in a drawRect: + UIGraphicsBeginImageContextWithOptions(circleRect.size, NO, 0); + + // create a UIBezierPath circle + UIBezierPath *circle = [UIBezierPath bezierPathWithRoundedRect:circleRect cornerRadius:circleRect.size.width/2]; + + // clip to the circle + [circle addClip]; + + // draw the image in the circleRect *AFTER* the context is clipped + [self drawInRect:circleRect]; + + // create a border (for white background pictures) +#if StrokeRoundedImages + circle.lineWidth = 1; + [[UIColor darkGrayColor] set]; + [circle stroke]; +#endif + + // get an image from the image context + UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext(); + + // end the image context since we're not in a drawRect: + UIGraphicsEndImageContext(); + + return roundedImage; +} + +@end + +@implementation NSString (Additions) + +// Returns a user-visible date time string that corresponds to the +// specified RFC 3339 date time string. Note that this does not handle +// all possible RFC 3339 date time strings, just one of the most common +// styles. ++ (NSDate *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString +{ + NSDateFormatter * rfc3339DateFormatter; + NSLocale * enUSPOSIXLocale; + + // Convert the RFC 3339 date time string to an NSDate. + + rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + + enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + + [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; + [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ'"]; + [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + return [rfc3339DateFormatter dateFromString:rfc3339DateTimeString]; +} + ++ (NSString *)elapsedTimeStringSinceDate:(NSString *)uploadDateString +{ + // early return if no post date string + if (!uploadDateString) + { + return @"NO POST DATE"; + } + + NSDate *postDate = [self userVisibleDateTimeStringForRFC3339DateTimeString:uploadDateString]; + + if (!postDate) { + return @"DATE CONVERSION ERROR"; + } + + NSDate *currentDate = [NSDate date]; + + NSCalendar *calendar = [NSCalendar currentCalendar]; + + NSUInteger seconds = [[calendar components:NSCalendarUnitSecond fromDate:postDate toDate:currentDate options:0] second]; + NSUInteger minutes = [[calendar components:NSCalendarUnitMinute fromDate:postDate toDate:currentDate options:0] minute]; + NSUInteger hours = [[calendar components:NSCalendarUnitHour fromDate:postDate toDate:currentDate options:0] hour]; + NSUInteger days = [[calendar components:NSCalendarUnitDay fromDate:postDate toDate:currentDate options:0] day]; + + NSString *elapsedTime; + + if (days > 7) { + elapsedTime = [NSString stringWithFormat:@"%luw", (long)ceil(days/7.0)]; + } else if (days > 0) { + elapsedTime = [NSString stringWithFormat:@"%lud", (long)days]; + } else if (hours > 0) { + elapsedTime = [NSString stringWithFormat:@"%luh", (long)hours]; + } else if (minutes > 0) { + elapsedTime = [NSString stringWithFormat:@"%lum", (long)minutes]; + } else if (seconds > 0) { + elapsedTime = [NSString stringWithFormat:@"%lus", (long)seconds]; + } else if (seconds == 0) { + elapsedTime = @"1s"; + } else { + elapsedTime = @"ERROR"; + } + + return elapsedTime; +} + +@end + +@implementation NSAttributedString (Additions) + ++ (NSAttributedString *)attributedStringWithString:(NSString *)string fontSize:(CGFloat)size + color:(nullable UIColor *)color firstWordColor:(nullable UIColor *)firstWordColor +{ + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init]; + + if (string) { + NSDictionary *attributes = @{NSForegroundColorAttributeName: color ? : [UIColor blackColor], + NSFontAttributeName: [UIFont systemFontOfSize:size]}; + attributedString = [[NSMutableAttributedString alloc] initWithString:string]; + [attributedString addAttributes:attributes range:NSMakeRange(0, string.length)]; + + if (firstWordColor) { + NSRange firstSpaceRange = [string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]; + NSRange firstWordRange = NSMakeRange(0, firstSpaceRange.location); + [attributedString addAttribute:NSForegroundColorAttributeName value:firstWordColor range:firstWordRange]; + } + } + + return attributedString; +} + +@end diff --git a/examples/ASDKTube/Sample/Models/VideoModel.h b/examples/ASDKTube/Sample/Models/VideoModel.h new file mode 100644 index 0000000000..63e7f48236 --- /dev/null +++ b/examples/ASDKTube/Sample/Models/VideoModel.h @@ -0,0 +1,16 @@ +// +// VideoModel.h +// Sample +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface VideoModel : NSObject +@property (nonatomic, strong, readonly) NSString* title; +@property (nonatomic, strong, readonly) NSURL *url; +@property (nonatomic, strong, readonly) NSString *userName; +@property (nonatomic, strong, readonly) NSURL *avatarUrl; +@end diff --git a/examples/ASDKTube/Sample/Models/VideoModel.m b/examples/ASDKTube/Sample/Models/VideoModel.m new file mode 100644 index 0000000000..2f233d59ce --- /dev/null +++ b/examples/ASDKTube/Sample/Models/VideoModel.m @@ -0,0 +1,27 @@ +// +// VideoModel.m +// Sample +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "VideoModel.h" + +@implementation VideoModel +- (instancetype)init +{ + self = [super init]; + if (self) { + NSString *videoUrlString = @"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"; + NSString *avatarUrlString = [NSString stringWithFormat:@"https://api.adorable.io/avatars/50/%@",[[NSProcessInfo processInfo] globallyUniqueString]]; + + _title = @"Demo title"; + _url = [NSURL URLWithString:videoUrlString]; + _userName = @"Random User"; + _avatarUrl = [NSURL URLWithString:avatarUrlString]; + } + + return self; +} +@end diff --git a/examples/ASDKTube/Sample/Nodes/VideoContentCell.h b/examples/ASDKTube/Sample/Nodes/VideoContentCell.h new file mode 100644 index 0000000000..ca6bb3256e --- /dev/null +++ b/examples/ASDKTube/Sample/Nodes/VideoContentCell.h @@ -0,0 +1,14 @@ +// +// VideoContentCell.h +// Sample +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import "VideoModel.h" + +@interface VideoContentCell : ASCellNode +- (instancetype)initWithVideoObject:(VideoModel *)video; +@end diff --git a/examples/ASDKTube/Sample/Nodes/VideoContentCell.m b/examples/ASDKTube/Sample/Nodes/VideoContentCell.m new file mode 100644 index 0000000000..d5db4a84e4 --- /dev/null +++ b/examples/ASDKTube/Sample/Nodes/VideoContentCell.m @@ -0,0 +1,151 @@ +// +// VideoContentCell.m +// Sample +// +// Created by Erekle on 5/14/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "VideoContentCell.h" +#import "ASVideoPlayerNode.h" +#import "Utilities.h" + +#define AVATAR_IMAGE_HEIGHT 30 +#define HORIZONTAL_BUFFER 10 +#define VERTICAL_BUFFER 5 + +@interface VideoContentCell () + +@end + +@implementation VideoContentCell +{ + VideoModel *_videoModel; + ASTextNode *_titleNode; + ASNetworkImageNode *_avatarNode; + ASVideoPlayerNode *_videoPlayerNode; + ASControlNode *_likeButtonNode; +} + +- (instancetype)initWithVideoObject:(VideoModel *)video +{ + self = [super init]; + if (self) { + + _videoModel = video; + + _titleNode = [[ASTextNode alloc] init]; + _titleNode.attributedText = [[NSAttributedString alloc] initWithString:_videoModel.title attributes:[self titleNodeStringOptions]]; + _titleNode.flexGrow = YES; + [self addSubnode:_titleNode]; + + _avatarNode = [[ASNetworkImageNode alloc] init]; + _avatarNode.URL = _videoModel.avatarUrl; + + [_avatarNode setImageModificationBlock:^UIImage *(UIImage *image) { + CGSize profileImageSize = CGSizeMake(AVATAR_IMAGE_HEIGHT, AVATAR_IMAGE_HEIGHT); + return [image makeCircularImageWithSize:profileImageSize]; + }]; + + [self addSubnode:_avatarNode]; + + _likeButtonNode = [[ASControlNode alloc] init]; + _likeButtonNode.backgroundColor = [UIColor redColor]; + [self addSubnode:_likeButtonNode]; + + _videoPlayerNode = [[ASVideoPlayerNode alloc] initWithUrl:_videoModel.url]; + _videoPlayerNode.delegate = self; + [self addSubnode:_videoPlayerNode]; + } + return self; +} + +- (NSDictionary*)titleNodeStringOptions +{ + return @{ + NSFontAttributeName : [UIFont systemFontOfSize:14.0], + NSForegroundColorAttributeName: [UIColor blackColor] + }; +} + +- (ASLayoutSpec*)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + CGFloat fullWidth = [UIScreen mainScreen].bounds.size.width; + _videoPlayerNode.preferredFrameSize = CGSizeMake(fullWidth, fullWidth * 9 / 16); + _avatarNode.preferredFrameSize = CGSizeMake(AVATAR_IMAGE_HEIGHT, AVATAR_IMAGE_HEIGHT); + _likeButtonNode.preferredFrameSize = CGSizeMake(50.0, 26.0); + + ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; + headerStack.spacing = HORIZONTAL_BUFFER; + headerStack.alignItems = ASStackLayoutAlignItemsCenter; + [headerStack setChildren:@[ _avatarNode, _titleNode]]; + + UIEdgeInsets headerInsets = UIEdgeInsetsMake(HORIZONTAL_BUFFER, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER); + ASInsetLayoutSpec *headerInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:headerInsets child:headerStack]; + + ASStackLayoutSpec *bottomControlsStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; + bottomControlsStack.spacing = HORIZONTAL_BUFFER; + bottomControlsStack.alignItems = ASStackLayoutAlignItemsCenter; + [bottomControlsStack setChildren:@[ _likeButtonNode]]; + + UIEdgeInsets bottomControlsInsets = UIEdgeInsetsMake(HORIZONTAL_BUFFER, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER); + ASInsetLayoutSpec *bottomControlsInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:bottomControlsInsets child:bottomControlsStack]; + + + ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStack.alignItems = ASStackLayoutAlignItemsStretch; + [verticalStack setChildren:@[ headerInset, _videoPlayerNode, bottomControlsInset ]]; + return verticalStack; +} + +#pragma mark - ASVideoPlayerNodeDelegate +- (void)videoPlayerNodeWasTapped:(ASVideoPlayerNode *)videoPlayer +{ + if (_videoPlayerNode.playerState == ASVideoNodePlayerStatePlaying) { + NSLog(@"TRANSITION"); + } else { + [_videoPlayerNode play]; + } +} + +- (NSArray *)videoPlayerNodeNeededControls:(ASVideoPlayerNode *)videoPlayer +{ + return @[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]; +} + +- (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer forControls:(NSDictionary *)controls forMaximumSize:(CGSize)maxSize +{ + NSMutableArray *bottomControls = [[NSMutableArray alloc] init]; + + ASDisplayNode *playbackButtonNode = controls[@(ASVideoPlayerNodeControlTypePlaybackButton)]; + + if (playbackButtonNode) { + [bottomControls addObject:playbackButtonNode]; + } + + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.flexGrow = YES; + + ASStackLayoutSpec *controlbarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:10.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsCenter + children:bottomControls]; + controlbarSpec.alignSelf = ASStackLayoutAlignSelfStretch; + + UIEdgeInsets insets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); + + ASInsetLayoutSpec *controlbarInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:controlbarSpec]; + + controlbarInsetSpec.alignSelf = ASStackLayoutAlignSelfStretch; + + ASStackLayoutSpec *mainVerticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + children:@[ spacer, controlbarInsetSpec ]]; + + + return mainVerticalStack; +} +@end diff --git a/examples/ASDKTube/Sample/ViewController.h b/examples/ASDKTube/Sample/ViewController.h index 8b16b1c332..d4ec993c7b 100644 --- a/examples/ASDKTube/Sample/ViewController.h +++ b/examples/ASDKTube/Sample/ViewController.h @@ -8,8 +8,8 @@ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import -#import @interface ViewController : ASViewController diff --git a/examples/ASDKTube/Sample/ViewController.m b/examples/ASDKTube/Sample/ViewController.m index 0e0e038fa6..adb64991c1 100644 --- a/examples/ASDKTube/Sample/ViewController.m +++ b/examples/ASDKTube/Sample/ViewController.m @@ -10,31 +10,73 @@ */ #import "ViewController.h" +#import +#import +#import "VideoModel.h" +#import "VideoContentCell.h" -@interface ViewController() +@interface ViewController() @property (nonatomic, strong) ASVideoPlayerNode *videoPlayerNode; @end @implementation ViewController +{ + ASTableNode *_tableNode; + NSMutableArray *_videoFeedData; +} - (instancetype)init { - if (!(self = [super initWithNode:self.videoPlayerNode])) { + _tableNode = [[ASTableNode alloc] init]; + _tableNode.delegate = self; + _tableNode.dataSource = self; + + if (!(self = [super initWithNode:_tableNode])) { return nil; } return self; } +- (void)loadView +{ + [super loadView]; + + _videoFeedData = [[NSMutableArray alloc] initWithObjects:[[VideoModel alloc] init], [[VideoModel alloc] init], nil]; + + [_tableNode.view reloadData]; +} + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - + //[self.view addSubnode:self.videoPlayerNode]; //[self.videoPlayerNode setNeedsLayout]; } +#pragma mark - ASCollectionDelegate - ASCollectionDataSource +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ + return _videoFeedData.count; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + VideoModel *videoObject = [_videoFeedData objectAtIndex:indexPath.row]; + VideoContentCell *cellNode = [[VideoContentCell alloc] initWithVideoObject:videoObject]; + return cellNode; +} + +//- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath{ +// CGFloat fullWidth = [UIScreen mainScreen].bounds.size.width; +// return ASSizeRangeMake(CGSizeMake(fullWidth, 0.0), CGSizeMake(fullWidth, 400.0)); +//} + - (ASVideoPlayerNode *)videoPlayerNode; { if (_videoPlayerNode) { @@ -57,49 +99,49 @@ } #pragma mark - ASVideoPlayerNodeDelegate -- (NSArray *)videoPlayerNodeNeededControls:(ASVideoPlayerNode *)videoPlayer -{ - return @[ @(ASVideoPlayerNodeControlTypePlaybackButton), - @(ASVideoPlayerNodeControlTypeElapsedText), - @(ASVideoPlayerNodeControlTypeScrubber), - @(ASVideoPlayerNodeControlTypeDurationText) ]; -} +//- (NSArray *)videoPlayerNodeNeededControls:(ASVideoPlayerNode *)videoPlayer +//{ +// return @[ @(ASVideoPlayerNodeControlTypePlaybackButton), +// @(ASVideoPlayerNodeControlTypeElapsedText), +// @(ASVideoPlayerNodeControlTypeScrubber), +// @(ASVideoPlayerNodeControlTypeDurationText) ]; +//} +// +//- (UIColor *)videoPlayerNodeScrubberMaximumTrackTint:(ASVideoPlayerNode *)videoPlayer +//{ +// return [UIColor colorWithRed:1 green:1 blue:1 alpha:0.3]; +//} +// +//- (UIColor *)videoPlayerNodeScrubberMinimumTrackTint:(ASVideoPlayerNode *)videoPlayer +//{ +// return [UIColor whiteColor]; +//} +// +//- (UIColor *)videoPlayerNodeScrubberThumbTint:(ASVideoPlayerNode *)videoPlayer +//{ +// return [UIColor whiteColor]; +//} +// +//- (NSDictionary *)videoPlayerNodeTimeLabelAttributes:(ASVideoPlayerNode *)videoPlayerNode timeLabelType:(ASVideoPlayerNodeControlType)timeLabelType +//{ +// NSDictionary *options; +// +// if (timeLabelType == ASVideoPlayerNodeControlTypeElapsedText) { +// options = @{ +// NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:16.0], +// NSForegroundColorAttributeName: [UIColor orangeColor] +// }; +// } else if (timeLabelType == ASVideoPlayerNodeControlTypeDurationText) { +// options = @{ +// NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:16.0], +// NSForegroundColorAttributeName: [UIColor redColor] +// }; +// } +// +// return options; +//} -- (UIColor *)videoPlayerNodeScrubberMaximumTrackTint:(ASVideoPlayerNode *)videoPlayer -{ - return [UIColor colorWithRed:1 green:1 blue:1 alpha:0.3]; -} - -- (UIColor *)videoPlayerNodeScrubberMinimumTrackTint:(ASVideoPlayerNode *)videoPlayer -{ - return [UIColor whiteColor]; -} - -- (UIColor *)videoPlayerNodeScrubberThumbTint:(ASVideoPlayerNode *)videoPlayer -{ - return [UIColor whiteColor]; -} - -- (NSDictionary *)videoPlayerNodeTimeLabelAttributes:(ASVideoPlayerNode *)videoPlayerNode timeLabelType:(ASVideoPlayerNodeControlType)timeLabelType -{ - NSDictionary *options; - - if (timeLabelType == ASVideoPlayerNodeControlTypeElapsedText) { - options = @{ - NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:16.0], - NSForegroundColorAttributeName: [UIColor orangeColor] - }; - } else if (timeLabelType == ASVideoPlayerNodeControlTypeDurationText) { - options = @{ - NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:16.0], - NSForegroundColorAttributeName: [UIColor redColor] - }; - } - - return options; -} - -- (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer +/*- (ASLayoutSpec *)videoPlayerNodeLayoutSpec:(ASVideoPlayerNode *)videoPlayer forControls:(NSDictionary *)controls forConstrainedSize:(ASSizeRange)constrainedSize { @@ -166,6 +208,6 @@ return mainVerticalStack; -} +}*/ @end \ No newline at end of file diff --git a/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h b/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h new file mode 100644 index 0000000000..43ad266374 --- /dev/null +++ b/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.h @@ -0,0 +1,13 @@ +// +// WindowWithStatusBarUnderlay.h +// Sample +// +// Created by Hannah Troisi on 4/10/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface WindowWithStatusBarUnderlay : UIWindow + +@end diff --git a/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m b/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m new file mode 100644 index 0000000000..d50df6b2df --- /dev/null +++ b/examples/ASDKTube/Sample/WindowWithStatusBarUnderlay.m @@ -0,0 +1,39 @@ +// +// WindowWithStatusBarUnderlay.m +// Sample +// +// Created by Erekle on 5/15/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "WindowWithStatusBarUnderlay.h" +#import "Utilities.h" + +@implementation WindowWithStatusBarUnderlay +{ + UIView *_statusBarOpaqueUnderlayView; +} + +-(instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + _statusBarOpaqueUnderlayView = [[UIView alloc] init]; + _statusBarOpaqueUnderlayView.backgroundColor = [UIColor lighOrangeColor]; + [self addSubview:_statusBarOpaqueUnderlayView]; + } + return self; +} + +-(void)layoutSubviews +{ + [super layoutSubviews]; + + [self bringSubviewToFront:_statusBarOpaqueUnderlayView]; + + CGRect statusBarFrame = CGRectZero; + statusBarFrame.size.width = [[UIScreen mainScreen] bounds].size.width; + statusBarFrame.size.height = [[UIApplication sharedApplication] statusBarFrame].size.height; + _statusBarOpaqueUnderlayView.frame = statusBarFrame; +} +@end diff --git a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj index 9307b60a63..4aba4f49ff 100644 --- a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj @@ -254,12 +254,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 06770D39D4186D6446B1BDD5 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */, + 06770D39D4186D6446B1BDD5 /* 📦 Embed Pods Frameworks */, ); buildRules = ( ); @@ -319,14 +319,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 06770D39D4186D6446B1BDD5 /* Embed Pods Frameworks */ = { + 06770D39D4186D6446B1BDD5 /* 📦 Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "📦 Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -334,14 +334,14 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Check Pods Manifest.lock"; + name = "📦 Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -349,14 +349,14 @@ shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "📦 Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; diff --git a/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + +